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