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