ConversationItemView.java revision 2838681e487508af50e8d682728726c0722d00ec
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mail.browse; 19 20import android.animation.Animator; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.AnimatorSet; 23import android.animation.ObjectAnimator; 24import android.content.ClipData; 25import android.content.ClipData.Item; 26import android.content.Context; 27import android.content.res.Resources; 28import android.graphics.Bitmap; 29import android.graphics.BitmapFactory; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.LinearGradient; 33import android.graphics.Matrix; 34import android.graphics.Paint; 35import android.graphics.Point; 36import android.graphics.Rect; 37import android.graphics.Shader; 38import android.graphics.Typeface; 39import android.graphics.drawable.Drawable; 40import android.text.Layout.Alignment; 41import android.text.Spannable; 42import android.text.SpannableString; 43import android.text.SpannableStringBuilder; 44import android.text.StaticLayout; 45import android.text.TextPaint; 46import android.text.TextUtils; 47import android.text.TextUtils.TruncateAt; 48import android.text.format.DateUtils; 49import android.text.style.CharacterStyle; 50import android.text.style.ForegroundColorSpan; 51import android.text.style.TextAppearanceSpan; 52import android.text.util.Rfc822Token; 53import android.text.util.Rfc822Tokenizer; 54import android.util.SparseArray; 55import android.util.TypedValue; 56import android.view.DragEvent; 57import android.view.MotionEvent; 58import android.view.View; 59import android.view.ViewGroup; 60import android.view.ViewParent; 61import android.view.animation.DecelerateInterpolator; 62import android.view.animation.LinearInterpolator; 63import android.widget.AbsListView; 64import android.widget.AbsListView.OnScrollListener; 65import android.widget.TextView; 66 67import com.android.mail.R; 68import com.android.mail.R.drawable; 69import com.android.mail.R.integer; 70import com.android.mail.R.string; 71import com.android.mail.bitmap.AttachmentDrawable; 72import com.android.mail.bitmap.AttachmentGridDrawable; 73import com.android.mail.browse.ConversationItemViewModel.SenderFragment; 74import com.android.mail.perf.Timer; 75import com.android.mail.photomanager.ContactPhotoManager; 76import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier; 77import com.android.mail.photomanager.PhotoManager.PhotoIdentifier; 78import com.android.mail.providers.Address; 79import com.android.mail.providers.Attachment; 80import com.android.mail.providers.Conversation; 81import com.android.mail.providers.Folder; 82import com.android.mail.providers.UIProvider; 83import com.android.mail.providers.UIProvider.AttachmentRendition; 84import com.android.mail.providers.UIProvider.ConversationColumns; 85import com.android.mail.providers.UIProvider.ConversationListIcon; 86import com.android.mail.providers.UIProvider.FolderType; 87import com.android.mail.ui.AnimatedAdapter; 88import com.android.mail.ui.AnimatedAdapter.ConversationListListener; 89import com.android.mail.ui.ControllableActivity; 90import com.android.mail.ui.ConversationSelectionSet; 91import com.android.mail.ui.DividedImageCanvas; 92import com.android.mail.ui.DividedImageCanvas.InvalidateCallback; 93import com.android.mail.ui.FolderDisplayer; 94import com.android.mail.ui.SwipeableItemView; 95import com.android.mail.ui.SwipeableListView; 96import com.android.mail.ui.ViewMode; 97import com.android.mail.utils.FolderUri; 98import com.android.mail.utils.HardwareLayerEnabler; 99import com.android.mail.utils.LogTag; 100import com.android.mail.utils.LogUtils; 101import com.android.mail.utils.Utils; 102import com.google.common.annotations.VisibleForTesting; 103import com.google.common.collect.Lists; 104 105import java.util.ArrayList; 106import java.util.List; 107 108public class ConversationItemView extends View 109 implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener { 110 111 // Timer. 112 private static int sLayoutCount = 0; 113 private static Timer sTimer; // Create the sTimer here if you need to do 114 // perf analysis. 115 private static final int PERF_LAYOUT_ITERATIONS = 50; 116 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 117 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 118 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 119 private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; 120 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 121 private static final String LOG_TAG = LogTag.getLogTag(); 122 123 // Static bitmaps. 124 private static Bitmap STAR_OFF; 125 private static Bitmap STAR_ON; 126 private static Bitmap CHECK; 127 private static Bitmap ATTACHMENT; 128 private static Bitmap ONLY_TO_ME; 129 private static Bitmap TO_ME_AND_OTHERS; 130 private static Bitmap IMPORTANT_ONLY_TO_ME; 131 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 132 private static Bitmap IMPORTANT_TO_OTHERS; 133 private static Bitmap STATE_REPLIED; 134 private static Bitmap STATE_FORWARDED; 135 private static Bitmap STATE_REPLIED_AND_FORWARDED; 136 private static Bitmap STATE_CALENDAR_INVITE; 137 private static Bitmap VISIBLE_CONVERSATION_CARET; 138 private static Drawable RIGHT_EDGE_TABLET; 139 private static Drawable PLACEHOLDER; 140 private static Drawable PROGRESS_BAR; 141 142 private static String sSendersSplitToken; 143 private static String sElidedPaddingToken; 144 private static String sOverflowCountFormat; 145 146 // Static colors. 147 private static int sSendersTextColorRead; 148 private static int sSendersTextColorUnread; 149 private static int sDateTextColor; 150 private static int sStarTouchSlop; 151 private static int sSenderImageTouchSlop; 152 private static int sShrinkAnimationDuration; 153 private static int sSlideAnimationDuration; 154 private static int sOverflowCountMax; 155 private static int sCabAnimationDuration; 156 157 // Static paints. 158 private static final TextPaint sPaint = new TextPaint(); 159 private static final TextPaint sFoldersPaint = new TextPaint(); 160 private static final Paint sCheckBackgroundPaint = new Paint(); 161 162 // Backgrounds for different states. 163 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 164 165 // Dimensions and coordinates. 166 private int mViewWidth = -1; 167 /** The view mode at which we calculated mViewWidth previously. */ 168 private int mPreviousMode; 169 170 private int mInfoIconX; 171 private int mDateX; 172 private int mPaperclipX; 173 private int mSendersWidth; 174 175 /** Whether we are on a tablet device or not */ 176 private final boolean mTabletDevice; 177 /** Whether we are on an expansive tablet */ 178 private final boolean mIsExpansiveTablet; 179 /** When in conversation mode, true if the list is hidden */ 180 private final boolean mListCollapsible; 181 182 @VisibleForTesting 183 ConversationItemViewCoordinates mCoordinates; 184 185 private ConversationItemViewCoordinates.Config mConfig; 186 187 private final Context mContext; 188 189 public ConversationItemViewModel mHeader; 190 private boolean mDownEvent; 191 private boolean mSelected = false; 192 private ConversationSelectionSet mSelectedConversationSet; 193 private Folder mDisplayedFolder; 194 private boolean mStarEnabled; 195 private boolean mSwipeEnabled; 196 private int mLastTouchX; 197 private int mLastTouchY; 198 private AnimatedAdapter mAdapter; 199 private float mAnimatedHeightFraction = 1.0f; 200 private final String mAccount; 201 private ControllableActivity mActivity; 202 private ConversationListListener mConversationListListener; 203 private final TextView mSubjectTextView; 204 private final TextView mSendersTextView; 205 private int mGadgetMode; 206 private final DividedImageCanvas mContactImagesHolder; 207 private static ContactPhotoManager sContactPhotoManager; 208 209 private static int sFoldersLeftPadding; 210 private static TextAppearanceSpan sSubjectTextUnreadSpan; 211 private static TextAppearanceSpan sSubjectTextReadSpan; 212 private static ForegroundColorSpan sSnippetTextUnreadSpan; 213 private static ForegroundColorSpan sSnippetTextReadSpan; 214 private static int sScrollSlop; 215 private static CharacterStyle sActivatedTextSpan; 216 217 private final AttachmentGridDrawable mAttachmentsView; 218 219 private static final boolean CONVLIST_ATTACHMENT_PREVIEWS_ENABLED = true; 220 221 private final Matrix mPhotoFlipMatrix = new Matrix(); 222 private final Matrix mCheckMatrix = new Matrix(); 223 224 private final CabAnimator mPhotoFlipAnimator; 225 226 /** 227 * The conversation id, if this conversation was selected the last time we were in a selection 228 * mode. This is reset after any animations complete upon exiting the selection mode. 229 */ 230 private long mLastSelectedId = -1; 231 232 /** The resource id of the color to use to override the background. */ 233 private int mBackgroundOverrideResId = -1; 234 /** The bitmap to use, or <code>null</code> for the default */ 235 private Bitmap mPhotoBitmap = null; 236 private Rect mPhotoRect = null; 237 238 static { 239 sPaint.setAntiAlias(true); 240 sFoldersPaint.setAntiAlias(true); 241 242 sCheckBackgroundPaint.setColor(Color.GRAY); 243 } 244 245 public static void setScrollStateChanged(final int scrollState) { 246 if (sContactPhotoManager == null) { 247 return; 248 } 249 final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE; 250 final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING; 251 252 // TODO: add support for yielding attachment bitmap allocation if/when needed 253 254 if (flinging) { 255 sContactPhotoManager.pause(); 256 } else { 257 sContactPhotoManager.resume(); 258 } 259 } 260 261 /** 262 * Handles displaying folders in a conversation header view. 263 */ 264 static class ConversationItemFolderDisplayer extends FolderDisplayer { 265 266 private int mFoldersCount; 267 268 public ConversationItemFolderDisplayer(Context context) { 269 super(context); 270 } 271 272 @Override 273 public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri, 274 final int ignoreFolderType) { 275 super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType); 276 mFoldersCount = mFoldersSortedSet.size(); 277 } 278 279 @Override 280 public void reset() { 281 super.reset(); 282 mFoldersCount = 0; 283 } 284 285 public boolean hasVisibleFolders() { 286 return mFoldersCount > 0; 287 } 288 289 private int measureFolders(int availableSpace, int cellSize) { 290 int totalWidth = 0; 291 boolean firstTime = true; 292 for (Folder f : mFoldersSortedSet) { 293 final String folderString = f.name; 294 int width = (int) sFoldersPaint.measureText(folderString) + cellSize; 295 if (firstTime) { 296 firstTime = false; 297 } else { 298 width += sFoldersLeftPadding; 299 } 300 totalWidth += width; 301 if (totalWidth > availableSpace) { 302 break; 303 } 304 } 305 306 return totalWidth; 307 } 308 309 public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) { 310 if (mFoldersCount == 0) { 311 return; 312 } 313 final int xMinStart = coordinates.foldersX; 314 final int xEnd = coordinates.foldersXEnd; 315 final int y = coordinates.foldersY; 316 final int height = coordinates.foldersHeight; 317 int textBottomPadding = coordinates.foldersTextBottomPadding; 318 319 sFoldersPaint.setTextSize(coordinates.foldersFontSize); 320 sFoldersPaint.setTypeface(coordinates.foldersTypeface); 321 322 // Initialize space and cell size based on the current mode. 323 int availableSpace = xEnd - xMinStart; 324 int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth(); 325 int foldersCount = Math.min(mFoldersCount, maxFoldersCount); 326 int averageWidth = availableSpace / foldersCount; 327 int cellSize = coordinates.getFolderCellWidth(); 328 329 // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that. 330 // Extra credit: maybe cache results across items as long as font size doesn't change. 331 332 final int totalWidth = measureFolders(availableSpace, cellSize); 333 int xStart = xEnd - Math.min(availableSpace, totalWidth); 334 final boolean overflow = totalWidth > availableSpace; 335 336 // Second pass to draw folders. 337 int i = 0; 338 for (Folder f : mFoldersSortedSet) { 339 if (availableSpace <= 0) { 340 break; 341 } 342 final String folderString = f.name; 343 final int fgColor = f.getForegroundColor(mDefaultFgColor); 344 final int bgColor = f.getBackgroundColor(mDefaultBgColor); 345 boolean labelTooLong = false; 346 final int textW = (int) sFoldersPaint.measureText(folderString); 347 int width = textW + cellSize + sFoldersLeftPadding; 348 349 if (overflow && width > averageWidth) { 350 if (i < foldersCount - 1) { 351 width = averageWidth; 352 } else { 353 // allow the last label to take all remaining space 354 // (and don't let it make room for padding) 355 width = availableSpace + sFoldersLeftPadding; 356 } 357 labelTooLong = true; 358 } 359 360 // TODO (mindyp): how to we get this? 361 final boolean isMuted = false; 362 // labelValues.folderId == 363 // sGmail.getFolderMap(mAccount).getFolderIdIgnored(); 364 365 // Draw the box. 366 sFoldersPaint.setColor(bgColor); 367 sFoldersPaint.setStyle(Paint.Style.FILL); 368 canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding, 369 y + height, sFoldersPaint); 370 371 // Draw the text. 372 final int padding = cellSize / 2; 373 sFoldersPaint.setColor(fgColor); 374 sFoldersPaint.setStyle(Paint.Style.FILL); 375 if (labelTooLong) { 376 final int rightBorder = xStart + width - sFoldersLeftPadding - padding; 377 final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, 378 y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP); 379 sFoldersPaint.setShader(shader); 380 } 381 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, 382 sFoldersPaint); 383 if (labelTooLong) { 384 sFoldersPaint.setShader(null); 385 } 386 387 availableSpace -= width; 388 xStart += width; 389 i++; 390 } 391 } 392 } 393 394 public ConversationItemView(Context context, String account) { 395 super(context); 396 Utils.traceBeginSection("CIVC constructor"); 397 setClickable(true); 398 setLongClickable(true); 399 mContext = context.getApplicationContext(); 400 final Resources res = mContext.getResources(); 401 mTabletDevice = Utils.useTabletUI(res); 402 mIsExpansiveTablet = 403 mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false; 404 mListCollapsible = res.getBoolean(R.bool.list_collapsible); 405 mAccount = account; 406 407 if (STAR_OFF == null) { 408 // Initialize static bitmaps. 409 STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off); 410 STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on); 411 CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check); 412 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 413 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 414 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 415 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 416 R.drawable.ic_email_caret_double_important_unread); 417 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 418 R.drawable.ic_email_caret_single_important_unread); 419 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 420 R.drawable.ic_email_caret_none_important_unread); 421 STATE_REPLIED = 422 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light); 423 STATE_FORWARDED = 424 BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light); 425 STATE_REPLIED_AND_FORWARDED = 426 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light); 427 STATE_CALENDAR_INVITE = 428 BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light); 429 VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, 430 R.drawable.ic_carrot_holo); 431 RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet); 432 PLACEHOLDER = res.getDrawable(drawable.ic_attachment_load); 433 PROGRESS_BAR = res.getDrawable(drawable.progress_holo); 434 435 // Initialize colors. 436 sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan( 437 res.getColor(R.color.senders_text_color_read))); 438 sSendersTextColorRead = res.getColor(R.color.senders_text_color_read); 439 sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread); 440 sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext, 441 R.style.SubjectAppearanceUnreadStyle); 442 sSubjectTextReadSpan = new TextAppearanceSpan(mContext, 443 R.style.SubjectAppearanceReadStyle); 444 sSnippetTextUnreadSpan = 445 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread)); 446 sSnippetTextReadSpan = 447 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read)); 448 sDateTextColor = res.getColor(R.color.date_text_color); 449 sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop); 450 sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop); 451 sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); 452 sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration); 453 // Initialize static color. 454 sSendersSplitToken = res.getString(R.string.senders_split_token); 455 sElidedPaddingToken = res.getString(R.string.elided_padding_token); 456 sOverflowCountFormat = res.getString(string.ap_overflow_format); 457 sScrollSlop = res.getInteger(R.integer.swipeScrollSlop); 458 sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding); 459 sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); 460 sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count); 461 sCabAnimationDuration = 462 res.getInteger(R.integer.conv_item_view_cab_anim_duration); 463 } 464 465 mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2, 466 sCabAnimationDuration) { 467 @Override 468 public void invalidateArea() { 469 final int left = mCoordinates.contactImagesX; 470 final int right = left + mContactImagesHolder.getWidth(); 471 final int top = mCoordinates.contactImagesY; 472 final int bottom = top + mContactImagesHolder.getHeight(); 473 invalidate(left, top, right, bottom); 474 } 475 }; 476 477 mSendersTextView = new TextView(mContext); 478 mSendersTextView.setIncludeFontPadding(false); 479 480 mSubjectTextView = new TextView(mContext); 481 mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END); 482 mSubjectTextView.setIncludeFontPadding(false); 483 484 mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() { 485 @Override 486 public void invalidate() { 487 if (mCoordinates == null) { 488 return; 489 } 490 ConversationItemView.this.invalidate(mCoordinates.contactImagesX, 491 mCoordinates.contactImagesY, 492 mCoordinates.contactImagesX + mCoordinates.contactImagesWidth, 493 mCoordinates.contactImagesY + mCoordinates.contactImagesHeight); 494 } 495 }); 496 497 mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR); 498 mAttachmentsView.setCallback(this); 499 500 Utils.traceEndSection(); 501 } 502 503 public void bind(Conversation conversation, ControllableActivity activity, 504 final ConversationListListener conversationListListener, 505 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 506 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 507 Utils.traceBeginSection("CIVC.bind"); 508 bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, 509 conversationListListener, set, folder, checkboxOrSenderImage, swipeEnabled, 510 priorityArrowEnabled, adapter, -1 /* backgroundOverrideResId */, 511 null /* photoBitmap */); 512 Utils.traceEndSection(); 513 } 514 515 public void bindAd(final ConversationItemViewModel conversationItemViewModel, 516 final ControllableActivity activity, 517 final ConversationListListener conversationListListener, final Folder folder, 518 final AnimatedAdapter adapter, final int backgroundOverrideResId, 519 final Bitmap photoBitmap) { 520 Utils.traceBeginSection("CIVC.bindAd"); 521 bind(conversationItemViewModel, activity, conversationListListener, null /* set */, folder, 522 ConversationListIcon.SENDER_IMAGE, true /* swipeEnabled */, 523 false /* priorityArrowEnabled */, adapter, backgroundOverrideResId, photoBitmap); 524 Utils.traceEndSection(); 525 } 526 527 private void bind(ConversationItemViewModel header, ControllableActivity activity, 528 final ConversationListListener conversationListListener, 529 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 530 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter, 531 final int backgroundOverrideResId, final Bitmap photoBitmap) { 532 mBackgroundOverrideResId = backgroundOverrideResId; 533 mPhotoBitmap = photoBitmap; 534 535 if (mHeader != null) { 536 // If this was previously bound to a different conversation, remove any contact photo 537 // manager requests. 538 if (header.conversation.id != mHeader.conversation.id || 539 (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames 540 .equals(header.displayableSenderNames))) { 541 ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds(); 542 if (divisionIds != null) { 543 mContactImagesHolder.reset(); 544 for (int pos = 0; pos < divisionIds.size(); pos++) { 545 sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash( 546 mContactImagesHolder, pos, divisionIds.get(pos))); 547 } 548 } 549 } 550 551 // If this was previously bound to a different conversation, 552 // remove any attachment preview manager requests. 553 if (header.conversation.id != mHeader.conversation.id 554 || header.conversation.attachmentPreviewsCount 555 != mHeader.conversation.attachmentPreviewsCount 556 || !header.conversation.getAttachmentPreviewUris() 557 .equals(mHeader.conversation.getAttachmentPreviewUris())) { 558 559 // unbind the attachments view (releasing bitmap references) 560 // (this also cancels all async tasks) 561 for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) { 562 mAttachmentsView.getOrCreateDrawable(i).unbind(); 563 } 564 // reset the grid, as the newly bound item may have a different attachment count 565 mAttachmentsView.setCount(0); 566 } 567 } 568 mCoordinates = null; 569 mHeader = header; 570 mActivity = activity; 571 mConversationListListener = conversationListListener; 572 mSelectedConversationSet = set; 573 mDisplayedFolder = folder; 574 mStarEnabled = folder != null && !folder.isTrash(); 575 mSwipeEnabled = swipeEnabled; 576 mAdapter = adapter; 577 mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache()); 578 mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator()); 579 580 if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) { 581 mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO; 582 } else { 583 mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE; 584 } 585 586 // Initialize folder displayer. 587 if (mHeader.folderDisplayer == null) { 588 mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext); 589 } else { 590 mHeader.folderDisplayer.reset(); 591 } 592 593 final int ignoreFolderType; 594 if (mDisplayedFolder.isInbox()) { 595 ignoreFolderType = FolderType.INBOX; 596 } else { 597 ignoreFolderType = -1; 598 } 599 600 mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, 601 mDisplayedFolder.folderUri, ignoreFolderType); 602 603 if (mHeader.dateOverrideText == null) { 604 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 605 mHeader.conversation.dateMs); 606 } else { 607 mHeader.dateText = mHeader.dateOverrideText; 608 } 609 610 mConfig = new ConversationItemViewCoordinates.Config() 611 .withGadget(mGadgetMode) 612 .withAttachmentPreviews(getAttachmentPreviewsMode()); 613 if (header.folderDisplayer.hasVisibleFolders()) { 614 mConfig.showFolders(); 615 } 616 if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) { 617 mConfig.showReplyState(); 618 } 619 if (mHeader.conversation.color != 0) { 620 mConfig.showColorBlock(); 621 } 622 // Personal level. 623 mHeader.personalLevelBitmap = null; 624 if (true) { // TODO: hook this up to a setting 625 final int personalLevel = mHeader.conversation.personalLevel; 626 final boolean isImportant = 627 mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT; 628 final boolean useImportantMarkers = isImportant && priorityArrowEnabled; 629 630 if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { 631 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME 632 : ONLY_TO_ME; 633 } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { 634 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS 635 : TO_ME_AND_OTHERS; 636 } else if (useImportantMarkers) { 637 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; 638 } 639 } 640 if (mHeader.personalLevelBitmap != null) { 641 mConfig.showPersonalIndicator(); 642 } 643 644 final int overflowCount = Math.min(getOverflowCount(), sOverflowCountMax); 645 mHeader.overflowText = (overflowCount > 0) ? 646 String.format(sOverflowCountFormat, overflowCount) : null; 647 648 mAttachmentsView.setOverflowText(mHeader.overflowText); 649 650 setContentDescription(); 651 requestLayout(); 652 } 653 654 @Override 655 public void invalidateDrawable(Drawable who) { 656 boolean handled = false; 657 if (mCoordinates != null) { 658 if (mAttachmentsView.equals(who)) { 659 final Rect r = new Rect(who.getBounds()); 660 r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); 661 ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom); 662 handled = true; 663 } 664 } 665 if (!handled) { 666 super.invalidateDrawable(who); 667 } 668 } 669 670 /** 671 * Get the Conversation object associated with this view. 672 */ 673 public Conversation getConversation() { 674 return mHeader.conversation; 675 } 676 677 private static void startTimer(String tag) { 678 if (sTimer != null) { 679 sTimer.start(tag); 680 } 681 } 682 683 private static void pauseTimer(String tag) { 684 if (sTimer != null) { 685 sTimer.pause(tag); 686 } 687 } 688 689 @Override 690 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 691 Utils.traceBeginSection("CIVC.measure"); 692 final int wSize = MeasureSpec.getSize(widthMeasureSpec); 693 694 final int currentMode = mActivity.getViewMode().getMode(); 695 if (wSize != mViewWidth || mPreviousMode != currentMode) { 696 mViewWidth = wSize; 697 mPreviousMode = currentMode; 698 } 699 mHeader.viewWidth = mViewWidth; 700 701 mConfig.updateWidth(wSize).setViewMode(currentMode); 702 703 Resources res = getResources(); 704 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 705 706 mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig, 707 mAdapter.getCoordinatesCache()); 708 709 if (mPhotoBitmap != null) { 710 mPhotoRect = new Rect(0, 0, mCoordinates.contactImagesWidth, 711 mCoordinates.contactImagesHeight); 712 } 713 714 final int h = (mAnimatedHeightFraction != 1.0f) ? 715 Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height; 716 setMeasuredDimension(mConfig.getWidth(), h); 717 Utils.traceEndSection(); 718 } 719 720 @Override 721 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 722 startTimer(PERF_TAG_LAYOUT); 723 Utils.traceBeginSection("CIVC.layout"); 724 725 super.onLayout(changed, left, top, right, bottom); 726 727 Utils.traceBeginSection("text and bitmaps"); 728 calculateTextsAndBitmaps(); 729 Utils.traceEndSection(); 730 731 Utils.traceBeginSection("coordinates"); 732 calculateCoordinates(); 733 Utils.traceEndSection(); 734 735 // Subject. 736 createSubject(mHeader.unread); 737 738 if (!mHeader.isLayoutValid()) { 739 setContentDescription(); 740 } 741 mHeader.validate(); 742 743 pauseTimer(PERF_TAG_LAYOUT); 744 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 745 sTimer.dumpResults(); 746 sTimer = new Timer(); 747 sLayoutCount = 0; 748 } 749 Utils.traceEndSection(); 750 } 751 752 private void setContentDescription() { 753 if (mActivity.isAccessibilityEnabled()) { 754 mHeader.resetContentDescription(); 755 setContentDescription(mHeader.getContentDescription(mContext)); 756 } 757 } 758 759 @Override 760 public void setBackgroundResource(int resourceId) { 761 Utils.traceBeginSection("set background resource"); 762 Drawable drawable = mBackgrounds.get(resourceId); 763 if (drawable == null) { 764 drawable = getResources().getDrawable(resourceId); 765 mBackgrounds.put(resourceId, drawable); 766 } 767 if (getBackground() != drawable) { 768 super.setBackgroundDrawable(drawable); 769 } 770 Utils.traceEndSection(); 771 } 772 773 private void calculateTextsAndBitmaps() { 774 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 775 776 if (mSelectedConversationSet != null) { 777 mSelected = mSelectedConversationSet.contains(mHeader.conversation); 778 } 779 setSelected(mSelected); 780 mHeader.gadgetMode = mGadgetMode; 781 782 final boolean isUnread = mHeader.unread; 783 updateBackground(isUnread); 784 785 mHeader.sendersDisplayText = new SpannableStringBuilder(); 786 mHeader.styledSendersString = null; 787 788 // Parse senders fragments. 789 if (mHeader.conversation.conversationInfo != null) { 790 // This is Gmail 791 Context context = getContext(); 792 mHeader.messageInfoString = SendersView 793 .createMessageInfo(context, mHeader.conversation, true); 794 int maxChars = ConversationItemViewCoordinates.getSendersLength(context, 795 mCoordinates.getMode(), mHeader.conversation.hasAttachments); 796 mHeader.displayableSenderEmails = new ArrayList<String>(); 797 mHeader.displayableSenderNames = new ArrayList<String>(); 798 mHeader.styledSenders = new ArrayList<SpannableString>(); 799 SendersView.format(context, mHeader.conversation.conversationInfo, 800 mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders, 801 mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount, 802 true); 803 // If we have displayable senders, load their thumbnails 804 loadSenderImages(); 805 } else { 806 // This is Email 807 SendersView.formatSenders(mHeader, getContext(), true); 808 if (!TextUtils.isEmpty(mHeader.conversation.senders)) { 809 mHeader.displayableSenderEmails = new ArrayList<String>(); 810 mHeader.displayableSenderNames = new ArrayList<String>(); 811 812 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders); 813 for (int i = 0; i < tokens.length;i++) { 814 final Rfc822Token token = tokens[i]; 815 final String senderName = Address.decodeAddressName(token.getName()); 816 final String senderAddress = token.getAddress(); 817 mHeader.displayableSenderEmails.add(senderAddress); 818 mHeader.displayableSenderNames.add( 819 !TextUtils.isEmpty(senderName) ? senderName : senderAddress); 820 } 821 loadSenderImages(); 822 } 823 } 824 825 if (isAttachmentPreviewsEnabled()) { 826 loadAttachmentPreviews(); 827 } 828 829 if (mHeader.isLayoutValid()) { 830 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 831 return; 832 } 833 startTimer(PERF_TAG_CALCULATE_FOLDERS); 834 835 836 pauseTimer(PERF_TAG_CALCULATE_FOLDERS); 837 838 // Paper clip icon. 839 mHeader.paperclip = null; 840 if (mHeader.conversation.hasAttachments) { 841 mHeader.paperclip = ATTACHMENT; 842 } 843 844 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 845 846 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 847 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 848 } 849 850 private boolean isAttachmentPreviewsEnabled() { 851 return CONVLIST_ATTACHMENT_PREVIEWS_ENABLED 852 && !mHeader.conversation.getAttachmentPreviewUris().isEmpty(); 853 } 854 855 private int getOverflowCount() { 856 return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation 857 .getAttachmentPreviewUris().size(); 858 } 859 860 private int getAttachmentPreviewsMode() { 861 if (isAttachmentPreviewsEnabled()) { 862 return mHeader.conversation.read 863 ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ 864 : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD; 865 } else { 866 return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE; 867 } 868 } 869 870 // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which 871 // is immutable. 872 private void loadSenderImages() { 873 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 874 && mHeader.displayableSenderEmails != null 875 && mHeader.displayableSenderEmails.size() > 0) { 876 if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) { 877 LogUtils.w(LOG_TAG, 878 "Contact image width(%d) or height(%d) is 0 for mode: (%d).", 879 mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, 880 mCoordinates.getMode()); 881 return; 882 } 883 884 int size = mHeader.displayableSenderEmails.size(); 885 final List<Object> keys = Lists.newArrayListWithCapacity(size); 886 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 887 keys.add(mHeader.displayableSenderEmails.get(i)); 888 } 889 890 mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth, 891 mCoordinates.contactImagesHeight); 892 mContactImagesHolder.setDivisionIds(keys); 893 String emailAddress; 894 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 895 emailAddress = mHeader.displayableSenderEmails.get(i); 896 PhotoIdentifier photoIdentifier = new ContactIdentifier( 897 mHeader.displayableSenderNames.get(i), emailAddress, i); 898 sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder); 899 } 900 } 901 } 902 903 private void loadAttachmentPreviews() { 904 if (mCoordinates.attachmentPreviewsWidth <= 0 905 || mCoordinates.attachmentPreviewsHeight <= 0) { 906 LogUtils.w(LOG_TAG, 907 "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).", 908 mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight, 909 mCoordinates.getMode(), getAttachmentPreviewsMode()); 910 return; 911 } 912 Utils.traceBeginSection("attachment previews"); 913 914 Utils.traceBeginSection("Setup load attachment previews"); 915 916 LogUtils.d(LOG_TAG, 917 "loadAttachmentPreviews: Loading attachment previews for conversation %s", 918 mHeader.conversation); 919 920 // Get list of attachments and states from conversation 921 final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris(); 922 final int previewStates = mHeader.conversation.attachmentPreviewStates; 923 final int displayCount = Math.min( 924 attachmentUris.size(), AttachmentGridDrawable.MAX_VISIBLE_ATTACHMENT_COUNT); 925 Utils.traceEndSection(); 926 927 mAttachmentsView.setCoordinates(mCoordinates); 928 mAttachmentsView.setCount(displayCount); 929 930 final int decodeHeight; 931 // if parallax is enabled, increase the desired vertical size of attachment bitmaps 932 // so we have extra pixels to scroll within 933 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 934 decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight 935 * SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER); 936 } else { 937 decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight; 938 } 939 940 // set the bounds before binding inner drawables so they can decode right away 941 // (they need the their bounds set to know whether to decode to 1x1 or 2x1 dimens) 942 mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth, 943 mCoordinates.attachmentPreviewsHeight); 944 945 for (int i = 0; i < displayCount; i++) { 946 Utils.traceBeginSection("setup single attachment preview"); 947 final String uri = attachmentUris.get(i); 948 949 // Find the rendition to load based on availability. 950 LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ", 951 Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST), 952 Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE), 953 uri); 954 int bestAvailableRendition = -1; 955 // BEST first, else use less preferred renditions 956 for (final int rendition : AttachmentRendition.PREFERRED_RENDITIONS) { 957 if (Attachment.getPreviewState(previewStates, i, rendition)) { 958 bestAvailableRendition = rendition; 959 break; 960 } 961 } 962 963 LogUtils.d(LOG_TAG, 964 "creating/setting drawable region in CIV=%s canvas=%s rend=%s uri=%s", 965 this, mAttachmentsView, bestAvailableRendition, uri); 966 final AttachmentDrawable drawable = mAttachmentsView.getOrCreateDrawable(i); 967 drawable.setDecodeDimensions(mCoordinates.attachmentPreviewsWidth, decodeHeight); 968 if (bestAvailableRendition != -1) { 969 drawable.bind(getContext(), uri, bestAvailableRendition); 970 } else { 971 drawable.showStaticPlaceholder(); 972 } 973 974 Utils.traceEndSection(); 975 } 976 977 Utils.traceEndSection(); 978 } 979 980 private static int makeExactSpecForSize(int size) { 981 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 982 } 983 984 private static void layoutViewExactly(View v, int w, int h) { 985 v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h)); 986 v.layout(0, 0, w, h); 987 } 988 989 private void layoutSenders() { 990 if (mHeader.styledSendersString != null) { 991 if (isActivated() && showActivatedText()) { 992 mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0, 993 mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 994 } else { 995 mHeader.styledSendersString.removeSpan(sActivatedTextSpan); 996 } 997 998 final int w = mSendersWidth; 999 final int h = mCoordinates.sendersHeight; 1000 mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h)); 1001 mSendersTextView.setMaxLines(mCoordinates.sendersLineCount); 1002 mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize); 1003 layoutViewExactly(mSendersTextView, w, h); 1004 1005 mSendersTextView.setText(mHeader.styledSendersString); 1006 } 1007 } 1008 1009 private void createSubject(final boolean isUnread) { 1010 final String subject = filterTag(mHeader.conversation.subject); 1011 final String snippet = mHeader.conversation.getSnippet(); 1012 final Spannable displayedStringBuilder = new SpannableString( 1013 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet)); 1014 1015 // since spans affect text metrics, add spans to the string before measure/layout or fancy 1016 // ellipsizing 1017 final int subjectTextLength = (subject != null) ? subject.length() : 0; 1018 if (!TextUtils.isEmpty(subject)) { 1019 displayedStringBuilder.setSpan(TextAppearanceSpan.wrap( 1020 isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength, 1021 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1022 } 1023 if (!TextUtils.isEmpty(snippet)) { 1024 final int startOffset = subjectTextLength; 1025 // Start after the end of the subject text; since the subject may be 1026 // "" or null, this could start at the 0th character in the subjectText string 1027 displayedStringBuilder.setSpan(ForegroundColorSpan.wrap( 1028 isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset, 1029 displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1030 } 1031 if (isActivated() && showActivatedText()) { 1032 displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(), 1033 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 1034 } 1035 1036 final int subjectWidth = mCoordinates.subjectWidth; 1037 final int subjectHeight = mCoordinates.subjectHeight; 1038 mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight)); 1039 mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount); 1040 mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize); 1041 layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight); 1042 1043 mSubjectTextView.setText(displayedStringBuilder); 1044 } 1045 1046 private boolean showActivatedText() { 1047 // For activated elements in tablet in conversation mode, we show an activated color, since 1048 // the background is dark blue for activated versus gray for non-activated. 1049 return mTabletDevice && !mListCollapsible; 1050 } 1051 1052 private boolean canFitFragment(int width, int line, int fixedWidth) { 1053 if (line == mCoordinates.sendersLineCount) { 1054 return width + fixedWidth <= mSendersWidth; 1055 } else { 1056 return width <= mSendersWidth; 1057 } 1058 } 1059 1060 private void calculateCoordinates() { 1061 startTimer(PERF_TAG_CALCULATE_COORDINATES); 1062 1063 sPaint.setTextSize(mCoordinates.dateFontSize); 1064 sPaint.setTypeface(Typeface.DEFAULT); 1065 1066 if (mHeader.infoIcon != null) { 1067 mInfoIconX = mCoordinates.infoIconXEnd - mHeader.infoIcon.getWidth(); 1068 1069 // If we have an info icon, we start drawing the date text: 1070 // At the end of the date TextView minus the width of the date text 1071 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText( 1072 mHeader.dateText != null ? mHeader.dateText.toString() : ""); 1073 } else { 1074 // If there is no info icon, we start drawing the date text: 1075 // At the end of the info icon ImageView minus the width of the date text 1076 // We use the info icon ImageView for positioning, since we want the date text to be 1077 // at the right, since there is no info icon 1078 mDateX = mCoordinates.infoIconXEnd - (int) sPaint.measureText( 1079 mHeader.dateText != null ? mHeader.dateText.toString() : ""); 1080 } 1081 1082 mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft; 1083 1084 if (mCoordinates.isWide()) { 1085 // In wide mode, the end of the senders should align with 1086 // the start of the subject and is based on a max width. 1087 mSendersWidth = mCoordinates.sendersWidth; 1088 } else { 1089 // In normal mode, the width is based on where the date/attachment icon start. 1090 final int dateAttachmentStart; 1091 // Have this end near the paperclip or date, not the folders. 1092 if (mHeader.paperclip != null) { 1093 dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft; 1094 } else { 1095 dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft; 1096 } 1097 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX; 1098 } 1099 1100 // Second pass to layout each fragment. 1101 sPaint.setTextSize(mCoordinates.sendersFontSize); 1102 sPaint.setTypeface(Typeface.DEFAULT); 1103 1104 if (mHeader.styledSenders != null) { 1105 ellipsizeStyledSenders(); 1106 layoutSenders(); 1107 } else { 1108 // First pass to calculate width of each fragment. 1109 int totalWidth = 0; 1110 int fixedWidth = 0; 1111 for (SenderFragment senderFragment : mHeader.senderFragments) { 1112 CharacterStyle style = senderFragment.style; 1113 int start = senderFragment.start; 1114 int end = senderFragment.end; 1115 style.updateDrawState(sPaint); 1116 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 1117 boolean isFixed = senderFragment.isFixed; 1118 if (isFixed) { 1119 fixedWidth += senderFragment.width; 1120 } 1121 totalWidth += senderFragment.width; 1122 } 1123 1124 if (mSendersWidth < 0) { 1125 mSendersWidth = 0; 1126 } 1127 totalWidth = ellipsize(fixedWidth); 1128 mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint, 1129 mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 1130 } 1131 1132 if (mSendersWidth < 0) { 1133 mSendersWidth = 0; 1134 } 1135 1136 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 1137 } 1138 1139 // The rules for displaying ellipsized senders are as follows: 1140 // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown 1141 // 2) If senders do not fit, ellipsize the last one that does fit, and stop 1142 // appending new senders 1143 private int ellipsizeStyledSenders() { 1144 SpannableStringBuilder builder = new SpannableStringBuilder(); 1145 float totalWidth = 0; 1146 boolean ellipsize = false; 1147 float width; 1148 SpannableStringBuilder messageInfoString = mHeader.messageInfoString; 1149 if (messageInfoString.length() > 0) { 1150 CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(), 1151 CharacterStyle.class); 1152 // There is only 1 character style span; make sure we apply all the 1153 // styles to the paint object before measuring. 1154 if (spans.length > 0) { 1155 spans[0].updateDrawState(sPaint); 1156 } 1157 // Paint the message info string to see if we lose space. 1158 float messageInfoWidth = sPaint.measureText(messageInfoString.toString()); 1159 totalWidth += messageInfoWidth; 1160 } 1161 SpannableString prevSender = null; 1162 SpannableString ellipsizedText; 1163 for (SpannableString sender : mHeader.styledSenders) { 1164 // There may be null sender strings if there were dupes we had to remove. 1165 if (sender == null) { 1166 continue; 1167 } 1168 // No more width available, we'll only show fixed fragments. 1169 if (ellipsize) { 1170 break; 1171 } 1172 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 1173 // There is only 1 character style span. 1174 if (spans.length > 0) { 1175 spans[0].updateDrawState(sPaint); 1176 } 1177 // If there are already senders present in this string, we need to 1178 // make sure we prepend the dividing token 1179 if (SendersView.sElidedString.equals(sender.toString())) { 1180 prevSender = sender; 1181 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 1182 } else if (builder.length() > 0 1183 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 1184 .toString()))) { 1185 prevSender = sender; 1186 sender = copyStyles(spans, sSendersSplitToken + sender); 1187 } else { 1188 prevSender = sender; 1189 } 1190 if (spans.length > 0) { 1191 spans[0].updateDrawState(sPaint); 1192 } 1193 // Measure the width of the current sender and make sure we have space 1194 width = (int) sPaint.measureText(sender.toString()); 1195 if (width + totalWidth > mSendersWidth) { 1196 // The text is too long, new line won't help. We have to 1197 // ellipsize text. 1198 ellipsize = true; 1199 width = mSendersWidth - totalWidth; // ellipsis width? 1200 ellipsizedText = copyStyles(spans, 1201 TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END)); 1202 width = (int) sPaint.measureText(ellipsizedText.toString()); 1203 } else { 1204 ellipsizedText = null; 1205 } 1206 totalWidth += width; 1207 1208 final CharSequence fragmentDisplayText; 1209 if (ellipsizedText != null) { 1210 fragmentDisplayText = ellipsizedText; 1211 } else { 1212 fragmentDisplayText = sender; 1213 } 1214 builder.append(fragmentDisplayText); 1215 } 1216 mHeader.styledMessageInfoStringOffset = builder.length(); 1217 builder.append(messageInfoString); 1218 mHeader.styledSendersString = builder; 1219 return (int)totalWidth; 1220 } 1221 1222 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 1223 SpannableString s = new SpannableString(newText); 1224 if (spans != null && spans.length > 0) { 1225 s.setSpan(spans[0], 0, s.length(), 0); 1226 } 1227 return s; 1228 } 1229 1230 private int ellipsize(int fixedWidth) { 1231 int totalWidth = 0; 1232 int currentLine = 1; 1233 boolean ellipsize = false; 1234 for (SenderFragment senderFragment : mHeader.senderFragments) { 1235 CharacterStyle style = senderFragment.style; 1236 int start = senderFragment.start; 1237 int end = senderFragment.end; 1238 int width = senderFragment.width; 1239 boolean isFixed = senderFragment.isFixed; 1240 style.updateDrawState(sPaint); 1241 1242 // No more width available, we'll only show fixed fragments. 1243 if (ellipsize && !isFixed) { 1244 senderFragment.shouldDisplay = false; 1245 continue; 1246 } 1247 1248 // New line and ellipsize text if needed. 1249 senderFragment.ellipsizedText = null; 1250 if (isFixed) { 1251 fixedWidth -= width; 1252 } 1253 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 1254 // The text is too long, new line won't help. We have to 1255 // ellipsize text. 1256 if (totalWidth == 0) { 1257 ellipsize = true; 1258 } else { 1259 // New line. 1260 if (currentLine < mCoordinates.sendersLineCount) { 1261 currentLine++; 1262 totalWidth = 0; 1263 // The text is still too long, we have to ellipsize 1264 // text. 1265 if (totalWidth + width > mSendersWidth) { 1266 ellipsize = true; 1267 } 1268 } else { 1269 ellipsize = true; 1270 } 1271 } 1272 1273 if (ellipsize) { 1274 width = mSendersWidth - totalWidth; 1275 // No more new line, we have to reserve width for fixed 1276 // fragments. 1277 if (currentLine == mCoordinates.sendersLineCount) { 1278 width -= fixedWidth; 1279 } 1280 senderFragment.ellipsizedText = TextUtils.ellipsize( 1281 mHeader.sendersText.substring(start, end), sPaint, width, 1282 TruncateAt.END).toString(); 1283 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 1284 } 1285 } 1286 senderFragment.shouldDisplay = true; 1287 totalWidth += width; 1288 1289 final CharSequence fragmentDisplayText; 1290 if (senderFragment.ellipsizedText != null) { 1291 fragmentDisplayText = senderFragment.ellipsizedText; 1292 } else { 1293 fragmentDisplayText = mHeader.sendersText.substring(start, end); 1294 } 1295 final int spanStart = mHeader.sendersDisplayText.length(); 1296 mHeader.sendersDisplayText.append(fragmentDisplayText); 1297 mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart, 1298 mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1299 } 1300 return totalWidth; 1301 } 1302 1303 /** 1304 * If the subject contains the tag of a mailing-list (text surrounded with 1305 * []), return the subject with that tag ellipsized, e.g. 1306 * "[android-gmail-team] Hello" -> "[andr...] Hello" 1307 */ 1308 private String filterTag(String subject) { 1309 String result = subject; 1310 String formatString = getContext().getResources().getString(R.string.filtered_tag); 1311 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 1312 int end = subject.indexOf(']'); 1313 if (end > 0) { 1314 String tag = subject.substring(1, end); 1315 result = String.format(formatString, Utils.ellipsize(tag, 7), 1316 subject.substring(end + 1)); 1317 } 1318 } 1319 return result; 1320 } 1321 1322 @Override 1323 public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1324 int totalItemCount) { 1325 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 1326 final View listItemView = unwrap(); 1327 if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) { 1328 return; 1329 } 1330 1331 invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY, 1332 mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth, 1333 mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight); 1334 } 1335 } 1336 1337 @Override 1338 public void onScrollStateChanged(AbsListView view, int scrollState) { 1339 } 1340 1341 @Override 1342 protected void onDraw(Canvas canvas) { 1343 Utils.traceBeginSection("CIVC.draw"); 1344 1345 // Contact photo 1346 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) { 1347 canvas.save(); 1348 drawContactImageArea(canvas); 1349 canvas.restore(); 1350 } 1351 1352 // Senders. 1353 boolean isUnread = mHeader.unread; 1354 // Old style senders; apply text colors/ sizes/ styling. 1355 canvas.save(); 1356 if (mHeader.sendersDisplayLayout != null) { 1357 sPaint.setTextSize(mCoordinates.sendersFontSize); 1358 sPaint.setTypeface(SendersView.getTypeface(isUnread)); 1359 sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead); 1360 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY 1361 + mHeader.sendersDisplayLayout.getTopPadding()); 1362 mHeader.sendersDisplayLayout.draw(canvas); 1363 } else { 1364 drawSenders(canvas); 1365 } 1366 canvas.restore(); 1367 1368 1369 // Subject. 1370 sPaint.setTypeface(Typeface.DEFAULT); 1371 canvas.save(); 1372 drawSubject(canvas); 1373 canvas.restore(); 1374 1375 // Folders. 1376 if (mConfig.areFoldersVisible()) { 1377 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates); 1378 } 1379 1380 // If this folder has a color (combined view/Email), show it here 1381 if (mConfig.isColorBlockVisible()) { 1382 sFoldersPaint.setColor(mHeader.conversation.color); 1383 sFoldersPaint.setStyle(Paint.Style.FILL); 1384 canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY, 1385 mCoordinates.colorBlockX + mCoordinates.colorBlockWidth, 1386 mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint); 1387 } 1388 1389 // Draw the reply state. Draw nothing if neither replied nor forwarded. 1390 if (mConfig.isReplyStateVisible()) { 1391 if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) { 1392 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX, 1393 mCoordinates.replyStateY, null); 1394 } else if (mHeader.hasBeenRepliedTo) { 1395 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX, 1396 mCoordinates.replyStateY, null); 1397 } else if (mHeader.hasBeenForwarded) { 1398 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX, 1399 mCoordinates.replyStateY, null); 1400 } else if (mHeader.isInvite) { 1401 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX, 1402 mCoordinates.replyStateY, null); 1403 } 1404 } 1405 1406 if (mConfig.isPersonalIndicatorVisible()) { 1407 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX, 1408 mCoordinates.personalIndicatorY, null); 1409 } 1410 1411 // Info icon 1412 if (mHeader.infoIcon != null) { 1413 canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint); 1414 } 1415 1416 // Date. 1417 sPaint.setTextSize(mCoordinates.dateFontSize); 1418 sPaint.setTypeface(Typeface.DEFAULT); 1419 sPaint.setColor(sDateTextColor); 1420 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, 1421 sPaint); 1422 1423 // Paper clip icon. 1424 if (mHeader.paperclip != null) { 1425 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 1426 } 1427 1428 if (mStarEnabled) { 1429 // Star. 1430 canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint); 1431 } 1432 1433 // Attachment previews 1434 if (isAttachmentPreviewsEnabled()) { 1435 canvas.save(); 1436 drawAttachmentPreviews(canvas); 1437 canvas.restore(); 1438 } 1439 1440 // right-side edge effect when in tablet conversation mode and the list is not collapsed 1441 if (mTabletDevice && !mListCollapsible && 1442 ViewMode.isConversationMode(mConfig.getViewMode())) { 1443 RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0, 1444 getWidth(), getHeight()); 1445 RIGHT_EDGE_TABLET.draw(canvas); 1446 1447 if (isActivated()) { 1448 // draw caret on the right, centered vertically 1449 final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth(); 1450 final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2; 1451 canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null); 1452 } 1453 } 1454 Utils.traceEndSection(); 1455 } 1456 1457 /** 1458 * Draws the contact images or check, in the correct animated state. 1459 */ 1460 private void drawContactImageArea(final Canvas canvas) { 1461 if (isSelected()) { 1462 mLastSelectedId = mHeader.conversation.id; 1463 1464 // Since this is selected, we draw the checkbox if the animation is not running, or if 1465 // it's running, and is past the half-way point 1466 if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) { 1467 // Flash in the check 1468 drawCheckbox(canvas); 1469 } else { 1470 // Flip out the contact photo 1471 drawContactImages(canvas); 1472 } 1473 } else { 1474 if ((mConversationListListener.isExitingSelectionMode() 1475 && mLastSelectedId == mHeader.conversation.id) 1476 || mPhotoFlipAnimator.isStarted()) { 1477 // Animate back to the photo 1478 if (!mPhotoFlipAnimator.isStarted()) { 1479 mPhotoFlipAnimator.startAnimation(true /* reverse */); 1480 } 1481 1482 if (mPhotoFlipAnimator.getValue() > 1) { 1483 // Flash out the check 1484 drawCheckbox(canvas); 1485 } else { 1486 // Flip in the contact photo 1487 drawContactImages(canvas); 1488 } 1489 } else { 1490 mLastSelectedId = -1; // We don't care anymore 1491 mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state 1492 1493 // Contact photos 1494 drawContactImages(canvas); 1495 } 1496 } 1497 } 1498 1499 private void drawContactImages(final Canvas canvas) { 1500 // mPhotoFlipFraction goes from 0 to 1 1501 final float value = mPhotoFlipAnimator.getValue(); 1502 1503 final float scale = 1f - value; 1504 final float xOffset = mContactImagesHolder.getWidth() * value / 2; 1505 1506 mPhotoFlipMatrix.reset(); 1507 mPhotoFlipMatrix.postScale(scale, 1); 1508 1509 canvas.translate(mCoordinates.contactImagesX + xOffset, mCoordinates.contactImagesY); 1510 1511 if (mPhotoBitmap == null) { 1512 mContactImagesHolder.draw(canvas, mPhotoFlipMatrix); 1513 } else { 1514 canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint); 1515 } 1516 } 1517 1518 private void drawCheckbox(final Canvas canvas) { 1519 // mPhotoFlipFraction goes from 1 to 2 1520 1521 // Draw the background 1522 canvas.save(); 1523 canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); 1524 canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, 1525 sCheckBackgroundPaint); 1526 canvas.restore(); 1527 1528 final int x = mCoordinates.contactImagesX 1529 + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2; 1530 final int y = mCoordinates.contactImagesY 1531 + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2; 1532 1533 final float value = mPhotoFlipAnimator.getValue(); 1534 final float scale; 1535 1536 if (!mPhotoFlipAnimator.isStarted()) { 1537 // We're not animating 1538 scale = 1; 1539 } else if (value < 1.9) { 1540 // 1.0 to 1.9 will scale 0 to 1 1541 scale = (value - 1f) / 0.9f; 1542 } else if (value < 1.95) { 1543 // 1.9 to 1.95 will scale 1 to 19/18 1544 scale = (value - 1f) / 0.9f; 1545 } else { 1546 // 1.95 to 2.0 will scale 19/18 to 1 1547 scale = (0.95f - (value - 1.95f)) / 0.9f; 1548 } 1549 1550 final float xOffset = CHECK.getWidth() * (1f - scale) / 2f; 1551 final float yOffset = CHECK.getHeight() * (1f - scale) / 2f; 1552 1553 mCheckMatrix.reset(); 1554 mCheckMatrix.postScale(scale, scale); 1555 1556 canvas.translate(x + xOffset, y + yOffset); 1557 1558 canvas.drawBitmap(CHECK, mCheckMatrix, sPaint); 1559 } 1560 1561 private void drawAttachmentPreviews(Canvas canvas) { 1562 canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); 1563 final float fraction; 1564 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 1565 final View listView = getListView(); 1566 final View listItemView = unwrap(); 1567 fraction = 1 - (float) listItemView.getBottom() 1568 / (listView.getHeight() + listItemView.getHeight()); 1569 } else { 1570 // Vertically center the preview crop, which has already been decoded at 1/3. 1571 fraction = 0.5f; 1572 } 1573 mAttachmentsView.setParallaxFraction(fraction); 1574 mAttachmentsView.draw(canvas); 1575 } 1576 1577 private void drawSubject(Canvas canvas) { 1578 canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY); 1579 mSubjectTextView.draw(canvas); 1580 } 1581 1582 private void drawSenders(Canvas canvas) { 1583 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY); 1584 mSendersTextView.draw(canvas); 1585 } 1586 1587 private Bitmap getStarBitmap() { 1588 return mHeader.conversation.starred ? STAR_ON : STAR_OFF; 1589 } 1590 1591 private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 1592 canvas.drawText(s, 0, s.length(), x, y, paint); 1593 } 1594 1595 /** 1596 * Set the background for this item based on: 1597 * 1. Read / Unread (unread messages have a lighter background) 1598 * 2. Tablet / Phone 1599 * 3. Checkbox checked / Unchecked (controls CAB color for item) 1600 * 4. Activated / Not activated (controls the blue highlight on tablet) 1601 * @param isUnread 1602 */ 1603 private void updateBackground(boolean isUnread) { 1604 final int background; 1605 if (mBackgroundOverrideResId > 0) { 1606 background = mBackgroundOverrideResId; 1607 } else if (isUnread) { 1608 background = R.drawable.conversation_unread_selector; 1609 } else { 1610 background = R.drawable.conversation_read_selector; 1611 } 1612 setBackgroundResource(background); 1613 } 1614 1615 /** 1616 * Toggle the check mark on this view and update the conversation or begin 1617 * drag, if drag is enabled. 1618 */ 1619 @Override 1620 public void toggleSelectedStateOrBeginDrag() { 1621 ViewMode mode = mActivity.getViewMode(); 1622 if (mIsExpansiveTablet && mode.isListMode()) { 1623 beginDragMode(); 1624 } else { 1625 toggleSelectedState(); 1626 } 1627 } 1628 1629 @Override 1630 public void toggleSelectedState() { 1631 if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) { 1632 mSelected = !mSelected; 1633 setSelected(mSelected); 1634 Conversation conv = mHeader.conversation; 1635 // Set the list position of this item in the conversation 1636 SwipeableListView listView = getListView(); 1637 conv.position = mSelected && listView != null ? listView.getPositionForView(this) 1638 : Conversation.NO_POSITION; 1639 if (mSelectedConversationSet != null) { 1640 mSelectedConversationSet.toggle(conv); 1641 } 1642 if (mSelectedConversationSet.isEmpty()) { 1643 listView.commitDestructiveActions(true); 1644 } 1645 1646 final boolean reverse = !mSelected; 1647 mPhotoFlipAnimator.startAnimation(reverse); 1648 mPhotoFlipAnimator.invalidateArea(); 1649 1650 // We update the background after the checked state has changed 1651 // now that we have a selected background asset. Setting the background 1652 // usually waits for a layout pass, but we don't need a full layout, 1653 // just an update to the background. 1654 requestLayout(); 1655 } 1656 } 1657 1658 /** 1659 * Toggle the star on this view and update the conversation. 1660 */ 1661 public void toggleStar() { 1662 mHeader.conversation.starred = !mHeader.conversation.starred; 1663 Bitmap starBitmap = getStarBitmap(); 1664 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 1665 + starBitmap.getWidth(), 1666 mCoordinates.starY + starBitmap.getHeight()); 1667 ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor(); 1668 if (cursor != null) { 1669 // TODO(skennedy) What about ads? 1670 cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED, 1671 mHeader.conversation.starred); 1672 } 1673 } 1674 1675 private boolean isTouchInContactPhoto(float x, float y) { 1676 // Everything before the right edge of contact photo 1677 1678 final int threshold = mCoordinates.contactImagesX + mCoordinates.contactImagesWidth 1679 + sSenderImageTouchSlop; 1680 1681 // Allow touching a little right of the contact photo when we're already in selection mode 1682 final float extra; 1683 if (mSelectedConversationSet == null || mSelectedConversationSet.isEmpty()) { 1684 extra = 0; 1685 } else { 1686 extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, 1687 getResources().getDisplayMetrics()); 1688 } 1689 1690 return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 1691 && x < (threshold + extra) 1692 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY); 1693 } 1694 1695 private boolean isTouchInStar(float x, float y) { 1696 // Everything after the star and include a touch slop. 1697 return mStarEnabled 1698 && x > mCoordinates.starX - sStarTouchSlop 1699 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY); 1700 } 1701 1702 @Override 1703 public boolean canChildBeDismissed() { 1704 return true; 1705 } 1706 1707 @Override 1708 public void dismiss() { 1709 SwipeableListView listView = getListView(); 1710 if (listView != null) { 1711 getListView().dismissChild(this); 1712 } 1713 } 1714 1715 private boolean onTouchEventNoSwipe(MotionEvent event) { 1716 Utils.traceBeginSection("on touch event no swipe"); 1717 boolean handled = false; 1718 1719 int x = (int) event.getX(); 1720 int y = (int) event.getY(); 1721 mLastTouchX = x; 1722 mLastTouchY = y; 1723 switch (event.getAction()) { 1724 case MotionEvent.ACTION_DOWN: 1725 if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)) { 1726 mDownEvent = true; 1727 handled = true; 1728 } 1729 break; 1730 1731 case MotionEvent.ACTION_CANCEL: 1732 mDownEvent = false; 1733 break; 1734 1735 case MotionEvent.ACTION_UP: 1736 if (mDownEvent) { 1737 if (isTouchInContactPhoto(x, y)) { 1738 // Touch on the check mark 1739 toggleSelectedState(); 1740 } else if (isTouchInStar(x, y)) { 1741 // Touch on the star 1742 toggleStar(); 1743 } 1744 handled = true; 1745 } 1746 break; 1747 } 1748 1749 if (!handled) { 1750 handled = super.onTouchEvent(event); 1751 } 1752 1753 Utils.traceEndSection(); 1754 return handled; 1755 } 1756 1757 /** 1758 * ConversationItemView is given the first chance to handle touch events. 1759 */ 1760 @Override 1761 public boolean onTouchEvent(MotionEvent event) { 1762 Utils.traceBeginSection("on touch event"); 1763 int x = (int) event.getX(); 1764 int y = (int) event.getY(); 1765 mLastTouchX = x; 1766 mLastTouchY = y; 1767 if (!mSwipeEnabled) { 1768 Utils.traceEndSection(); 1769 return onTouchEventNoSwipe(event); 1770 } 1771 switch (event.getAction()) { 1772 case MotionEvent.ACTION_DOWN: 1773 if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)) { 1774 mDownEvent = true; 1775 Utils.traceEndSection(); 1776 return true; 1777 } 1778 break; 1779 case MotionEvent.ACTION_UP: 1780 if (mDownEvent) { 1781 if (isTouchInContactPhoto(x, y)) { 1782 // Touch on the check mark 1783 Utils.traceEndSection(); 1784 mDownEvent = false; 1785 toggleSelectedState(); 1786 Utils.traceEndSection(); 1787 return true; 1788 } else if (isTouchInStar(x, y)) { 1789 // Touch on the star 1790 toggleStar(); 1791 Utils.traceEndSection(); 1792 return true; 1793 } 1794 } 1795 break; 1796 } 1797 // Let View try to handle it as well. 1798 boolean handled = super.onTouchEvent(event); 1799 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1800 Utils.traceEndSection(); 1801 return true; 1802 } 1803 Utils.traceEndSection(); 1804 return handled; 1805 } 1806 1807 @Override 1808 public boolean performClick() { 1809 final boolean handled = super.performClick(); 1810 final SwipeableListView list = getListView(); 1811 if (!handled && list != null && list.getAdapter() != null) { 1812 final int pos = list.findConversation(this, mHeader.conversation); 1813 list.performItemClick(this, pos, mHeader.conversation.id); 1814 } 1815 return handled; 1816 } 1817 1818 private SwipeableConversationItemView unwrap() { 1819 final ViewParent vp = getParent(); 1820 if (vp == null || !(vp instanceof SwipeableConversationItemView)) { 1821 return null; 1822 } 1823 return (SwipeableConversationItemView) vp; 1824 } 1825 1826 private SwipeableListView getListView() { 1827 SwipeableListView v = null; 1828 final SwipeableConversationItemView wrapper = unwrap(); 1829 if (wrapper != null) { 1830 v = (SwipeableListView) wrapper.getListView(); 1831 } 1832 if (v == null) { 1833 v = mAdapter.getListView(); 1834 } 1835 return v; 1836 } 1837 1838 /** 1839 * Reset any state associated with this conversation item view so that it 1840 * can be reused. 1841 */ 1842 public void reset() { 1843 Utils.traceBeginSection("reset"); 1844 setAlpha(1f); 1845 setTranslationX(0f); 1846 mAnimatedHeightFraction = 1.0f; 1847 Utils.traceEndSection(); 1848 } 1849 1850 @SuppressWarnings("deprecation") 1851 @Override 1852 public void setTranslationX(float translationX) { 1853 super.setTranslationX(translationX); 1854 1855 // When a list item is being swiped or animated, ensure that the hosting view has a 1856 // background color set. We only enable the background during the X-translation effect to 1857 // reduce overdraw during normal list scrolling. 1858 final SwipeableConversationItemView parent = unwrap(); 1859 if (parent == null) { 1860 LogUtils.w(LOG_TAG, 1861 "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s", 1862 getParent(), translationX); 1863 } 1864 1865 if (translationX != 0f) { 1866 parent.setBackgroundResource(R.color.swiped_bg_color); 1867 } else { 1868 parent.setBackgroundDrawable(null); 1869 } 1870 } 1871 1872 /** 1873 * Grow the height of the item and fade it in when bringing a conversation 1874 * back from a destructive action. 1875 */ 1876 public Animator createSwipeUndoAnimation() { 1877 ObjectAnimator undoAnimator = createTranslateXAnimation(true); 1878 return undoAnimator; 1879 } 1880 1881 /** 1882 * Grow the height of the item and fade it in when bringing a conversation 1883 * back from a destructive action. 1884 */ 1885 public Animator createUndoAnimation() { 1886 ObjectAnimator height = createHeightAnimation(true); 1887 Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f); 1888 fade.setDuration(sShrinkAnimationDuration); 1889 fade.setInterpolator(new DecelerateInterpolator(2.0f)); 1890 AnimatorSet transitionSet = new AnimatorSet(); 1891 transitionSet.playTogether(height, fade); 1892 transitionSet.addListener(new HardwareLayerEnabler(this)); 1893 return transitionSet; 1894 } 1895 1896 /** 1897 * Grow the height of the item and fade it in when bringing a conversation 1898 * back from a destructive action. 1899 */ 1900 public Animator createDestroyWithSwipeAnimation() { 1901 ObjectAnimator slide = createTranslateXAnimation(false); 1902 ObjectAnimator height = createHeightAnimation(false); 1903 AnimatorSet transitionSet = new AnimatorSet(); 1904 transitionSet.playSequentially(slide, height); 1905 return transitionSet; 1906 } 1907 1908 private ObjectAnimator createTranslateXAnimation(boolean show) { 1909 SwipeableListView parent = getListView(); 1910 // If we can't get the parent...we have bigger problems. 1911 int width = parent != null ? parent.getMeasuredWidth() : 0; 1912 final float start = show ? width : 0f; 1913 final float end = show ? 0f : width; 1914 ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end); 1915 slide.setInterpolator(new DecelerateInterpolator(2.0f)); 1916 slide.setDuration(sSlideAnimationDuration); 1917 return slide; 1918 } 1919 1920 public Animator createDestroyAnimation() { 1921 return createHeightAnimation(false); 1922 } 1923 1924 private ObjectAnimator createHeightAnimation(boolean show) { 1925 final float start = show ? 0f : 1.0f; 1926 final float end = show ? 1.0f : 0f; 1927 ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end); 1928 height.setInterpolator(new DecelerateInterpolator(2.0f)); 1929 height.setDuration(sShrinkAnimationDuration); 1930 return height; 1931 } 1932 1933 // Used by animator 1934 public void setAnimatedHeightFraction(float height) { 1935 mAnimatedHeightFraction = height; 1936 requestLayout(); 1937 } 1938 1939 @Override 1940 public SwipeableView getSwipeableView() { 1941 return SwipeableView.from(this); 1942 } 1943 1944 /** 1945 * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag. 1946 */ 1947 private void beginDragMode() { 1948 if (mLastTouchX < 0 || mLastTouchY < 0) { 1949 return; 1950 } 1951 // If this is already checked, don't bother unchecking it! 1952 if (!mSelected) { 1953 toggleSelectedState(); 1954 } 1955 1956 // Clip data has form: [conversations_uri, conversationId1, 1957 // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] 1958 final int count = mSelectedConversationSet.size(); 1959 String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count); 1960 1961 final ClipData data = ClipData.newUri(mContext.getContentResolver(), description, 1962 Conversation.MOVE_CONVERSATIONS_URI); 1963 for (Conversation conversation : mSelectedConversationSet.values()) { 1964 data.addItem(new Item(String.valueOf(conversation.position))); 1965 } 1966 // Protect against non-existent views: only happens for monkeys 1967 final int width = this.getWidth(); 1968 final int height = this.getHeight(); 1969 final boolean isDimensionNegative = (width < 0) || (height < 0); 1970 if (isDimensionNegative) { 1971 LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: " 1972 + "width=%d, height=%d", width, height); 1973 return; 1974 } 1975 mActivity.startDragMode(); 1976 // Start drag mode 1977 startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0); 1978 } 1979 1980 /** 1981 * Handles the drag event. 1982 * 1983 * @param event the drag event to be handled 1984 */ 1985 @Override 1986 public boolean onDragEvent(DragEvent event) { 1987 switch (event.getAction()) { 1988 case DragEvent.ACTION_DRAG_ENDED: 1989 mActivity.stopDragMode(); 1990 return true; 1991 } 1992 return false; 1993 } 1994 1995 private class ShadowBuilder extends DragShadowBuilder { 1996 private final Drawable mBackground; 1997 1998 private final View mView; 1999 private final String mDragDesc; 2000 private final int mTouchX; 2001 private final int mTouchY; 2002 private int mDragDescX; 2003 private int mDragDescY; 2004 2005 public ShadowBuilder(View view, int count, int touchX, int touchY) { 2006 super(view); 2007 mView = view; 2008 mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo); 2009 mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count); 2010 mTouchX = touchX; 2011 mTouchY = touchY; 2012 } 2013 2014 @Override 2015 public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 2016 final int width = mView.getWidth(); 2017 final int height = mView.getHeight(); 2018 2019 sPaint.setTextSize(mCoordinates.subjectFontSize); 2020 mDragDescX = mCoordinates.sendersX; 2021 mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ; 2022 shadowSize.set(width, height); 2023 shadowTouchPoint.set(mTouchX, mTouchY); 2024 } 2025 2026 @Override 2027 public void onDrawShadow(Canvas canvas) { 2028 mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight()); 2029 mBackground.draw(canvas); 2030 sPaint.setTextSize(mCoordinates.subjectFontSize); 2031 canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint); 2032 } 2033 } 2034 2035 @Override 2036 public float getMinAllowScrollDistance() { 2037 return sScrollSlop; 2038 } 2039 2040 private abstract class CabAnimator { 2041 private ObjectAnimator mAnimator = null; 2042 2043 private final String mPropertyName; 2044 2045 private float mValue; 2046 2047 private final float mStartValue; 2048 private final float mEndValue; 2049 2050 private final long mDuration; 2051 2052 public CabAnimator(final String propertyName, final float startValue, final float endValue, 2053 final long duration) { 2054 mPropertyName = propertyName; 2055 2056 mStartValue = startValue; 2057 mEndValue = endValue; 2058 2059 mDuration = duration; 2060 } 2061 2062 private ObjectAnimator createAnimator() { 2063 final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this, 2064 mPropertyName, mStartValue, mEndValue); 2065 animator.setDuration(mDuration); 2066 animator.setInterpolator(new LinearInterpolator()); 2067 animator.addListener(new AnimatorListenerAdapter() { 2068 @Override 2069 public void onAnimationEnd(final Animator animation) { 2070 invalidateArea(); 2071 } 2072 }); 2073 return animator; 2074 } 2075 2076 public abstract void invalidateArea(); 2077 2078 public void setValue(final float fraction) { 2079 if (mValue == fraction) { 2080 return; 2081 } 2082 mValue = fraction; 2083 invalidateArea(); 2084 } 2085 2086 public float getValue() { 2087 return mValue; 2088 } 2089 2090 /** 2091 * @param reverse <code>true</code> to animate in reverse 2092 */ 2093 public void startAnimation(final boolean reverse) { 2094 if (mAnimator != null) { 2095 mAnimator.cancel(); 2096 } 2097 2098 mAnimator = createAnimator(); 2099 2100 if (reverse) { 2101 mAnimator.reverse(); 2102 } else { 2103 mAnimator.start(); 2104 } 2105 } 2106 2107 public void stopAnimation() { 2108 if (mAnimator != null) { 2109 mAnimator.cancel(); 2110 mAnimator = null; 2111 } 2112 2113 setValue(0); 2114 } 2115 2116 public boolean isStarted() { 2117 return mAnimator != null && mAnimator.isStarted(); 2118 } 2119 } 2120 2121 public void setPhotoFlipFraction(final float fraction) { 2122 mPhotoFlipAnimator.setValue(fraction); 2123 } 2124} 2125