ConversationItemView.java revision da954c000080442f655ce250b3780d1bf249f024
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.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.content.ClipData; 24import android.content.ClipData.Item; 25import android.content.Context; 26import android.content.res.Resources; 27import android.graphics.Bitmap; 28import android.graphics.BitmapFactory; 29import android.graphics.Canvas; 30import android.graphics.LinearGradient; 31import android.graphics.Paint; 32import android.graphics.Point; 33import android.graphics.Shader; 34import android.graphics.Typeface; 35import android.graphics.drawable.Drawable; 36import android.net.Uri; 37import android.text.Layout.Alignment; 38import android.text.Spannable; 39import android.text.SpannableString; 40import android.text.SpannableStringBuilder; 41import android.text.StaticLayout; 42import android.text.TextPaint; 43import android.text.TextUtils; 44import android.text.TextUtils.TruncateAt; 45import android.text.format.DateUtils; 46import android.text.style.CharacterStyle; 47import android.text.style.ForegroundColorSpan; 48import android.text.style.TextAppearanceSpan; 49import android.text.util.Rfc822Token; 50import android.text.util.Rfc822Tokenizer; 51import android.util.SparseArray; 52import android.util.TypedValue; 53import android.view.DragEvent; 54import android.view.MotionEvent; 55import android.view.View; 56import android.view.ViewGroup; 57import android.view.ViewParent; 58import android.view.animation.DecelerateInterpolator; 59import android.widget.TextView; 60 61import com.android.mail.R; 62import com.android.mail.browse.ConversationItemViewModel.SenderFragment; 63import com.android.mail.perf.Timer; 64import com.android.mail.photomanager.ContactPhotoManager; 65import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier; 66import com.android.mail.photomanager.PhotoManager.PhotoIdentifier; 67import com.android.mail.providers.Address; 68import com.android.mail.providers.Conversation; 69import com.android.mail.providers.Folder; 70import com.android.mail.providers.UIProvider; 71import com.android.mail.providers.UIProvider.ConversationColumns; 72import com.android.mail.providers.UIProvider.ConversationListIcon; 73import com.android.mail.providers.UIProvider.FolderType; 74import com.android.mail.ui.AnimatedAdapter; 75import com.android.mail.ui.ControllableActivity; 76import com.android.mail.ui.ConversationSelectionSet; 77import com.android.mail.ui.DividedImageCanvas; 78import com.android.mail.ui.DividedImageCanvas.InvalidateCallback; 79import com.android.mail.ui.FolderDisplayer; 80import com.android.mail.ui.SwipeableItemView; 81import com.android.mail.ui.SwipeableListView; 82import com.android.mail.ui.ViewMode; 83import com.android.mail.utils.HardwareLayerEnabler; 84import com.android.mail.utils.LogTag; 85import com.android.mail.utils.LogUtils; 86import com.android.mail.utils.Utils; 87import com.google.common.annotations.VisibleForTesting; 88 89import java.util.ArrayList; 90 91public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem, 92 InvalidateCallback { 93 // Timer. 94 private static int sLayoutCount = 0; 95 private static Timer sTimer; // Create the sTimer here if you need to do 96 // perf analysis. 97 private static final int PERF_LAYOUT_ITERATIONS = 50; 98 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 99 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 100 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 101 private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; 102 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 103 private static final String LOG_TAG = LogTag.getLogTag(); 104 105 // Static bitmaps. 106 private static Bitmap STAR_OFF; 107 private static Bitmap STAR_ON; 108 private static Bitmap ATTACHMENT; 109 private static Bitmap ONLY_TO_ME; 110 private static Bitmap TO_ME_AND_OTHERS; 111 private static Bitmap IMPORTANT_ONLY_TO_ME; 112 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 113 private static Bitmap IMPORTANT_TO_OTHERS; 114 private static Bitmap STATE_REPLIED; 115 private static Bitmap STATE_FORWARDED; 116 private static Bitmap STATE_REPLIED_AND_FORWARDED; 117 private static Bitmap STATE_CALENDAR_INVITE; 118 private static Bitmap VISIBLE_CONVERSATION_CARET; 119 private static Drawable RIGHT_EDGE_TABLET; 120 121 private static String sSendersSplitToken; 122 private static String sElidedPaddingToken; 123 124 // Static colors. 125 private static int sActivatedTextColor; 126 private static int sSendersTextColorRead; 127 private static int sSendersTextColorUnread; 128 private static int sDateTextColor; 129 private static int sStarTouchSlop; 130 private static int sSenderImageTouchSlop; 131 @Deprecated 132 private static int sStandardScaledDimen; 133 private static int sShrinkAnimationDuration; 134 private static int sSlideAnimationDuration; 135 private static int sAnimatingBackgroundColor; 136 137 // Static paints. 138 private static TextPaint sPaint = new TextPaint(); 139 private static TextPaint sFoldersPaint = new TextPaint(); 140 141 // Backgrounds for different states. 142 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 143 144 // Dimensions and coordinates. 145 private int mViewWidth = -1; 146 /** The view mode at which we calculated mViewWidth previously. */ 147 private int mPreviousMode; 148 private int mDateX; 149 private int mPaperclipX; 150 private int mSendersWidth; 151 152 /** Whether we are on a tablet device or not */ 153 private final boolean mTabletDevice; 154 /** Whether we are on an expansive tablet */ 155 private final boolean mIsExpansiveTablet; 156 /** When in conversation mode, true if the list is hidden */ 157 private final boolean mListCollapsible; 158 159 @VisibleForTesting 160 ConversationItemViewCoordinates mCoordinates; 161 162 private ConversationItemViewCoordinates.Config mConfig; 163 164 private final Context mContext; 165 166 public ConversationItemViewModel mHeader; 167 private boolean mDownEvent; 168 private boolean mSelected = false; 169 private ConversationSelectionSet mSelectedConversationSet; 170 private Folder mDisplayedFolder; 171 private boolean mStarEnabled; 172 private boolean mSwipeEnabled; 173 private int mLastTouchX; 174 private int mLastTouchY; 175 private AnimatedAdapter mAdapter; 176 private float mAnimatedHeightFraction = 1.0f; 177 private final String mAccount; 178 private ControllableActivity mActivity; 179 private final TextView mSubjectTextView; 180 private final TextView mSendersTextView; 181 private int mGadgetMode; 182 private final DividedImageCanvas mContactImagesHolder; 183 private int mAttachmentPreviewMode; 184 private final DividedImageCanvas mAttachmentPreviewsCanvas; 185 186 private static int sFoldersLeftPadding; 187 private static TextAppearanceSpan sSubjectTextUnreadSpan; 188 private static TextAppearanceSpan sSubjectTextReadSpan; 189 private static ForegroundColorSpan sSnippetTextUnreadSpan; 190 private static ForegroundColorSpan sSnippetTextReadSpan; 191 private static int sScrollSlop; 192 private static CharacterStyle sActivatedTextSpan; 193 private static ContactPhotoManager sContactPhotoManager; 194 private static ContactPhotoManager sAttachmentPreviewsManager; 195 196 static { 197 sPaint.setAntiAlias(true); 198 sFoldersPaint.setAntiAlias(true); 199 } 200 201 /** 202 * Handles displaying folders in a conversation header view. 203 */ 204 static class ConversationItemFolderDisplayer extends FolderDisplayer { 205 206 private int mFoldersCount; 207 208 public ConversationItemFolderDisplayer(Context context) { 209 super(context); 210 } 211 212 @Override 213 public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri, 214 final int ignoreFolderType) { 215 super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType); 216 mFoldersCount = mFoldersSortedSet.size(); 217 } 218 219 @Override 220 public void reset() { 221 super.reset(); 222 mFoldersCount = 0; 223 } 224 225 public boolean hasVisibleFolders() { 226 return mFoldersCount > 0; 227 } 228 229 private int measureFolders(int availableSpace, int cellSize) { 230 int totalWidth = 0; 231 boolean firstTime = true; 232 for (Folder f : mFoldersSortedSet) { 233 final String folderString = f.name; 234 int width = (int) sFoldersPaint.measureText(folderString) + cellSize; 235 if (firstTime) { 236 firstTime = false; 237 } else { 238 width += sFoldersLeftPadding; 239 } 240 totalWidth += width; 241 if (totalWidth > availableSpace) { 242 break; 243 } 244 } 245 246 return totalWidth; 247 } 248 249 public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) { 250 if (mFoldersCount == 0) { 251 return; 252 } 253 final int xMinStart = coordinates.foldersX; 254 final int xEnd = coordinates.foldersXEnd; 255 final int y = coordinates.foldersY; 256 final int height = coordinates.foldersHeight; 257 final int ascent = coordinates.foldersAscent; 258 int textBottomPadding = coordinates.foldersTextBottomPadding; 259 260 sFoldersPaint.setTextSize(coordinates.foldersFontSize); 261 sFoldersPaint.setTypeface(coordinates.foldersTypeface); 262 263 // Initialize space and cell size based on the current mode. 264 int availableSpace = xEnd - xMinStart; 265 int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth(); 266 int foldersCount = Math.min(mFoldersCount, maxFoldersCount); 267 int averageWidth = availableSpace / foldersCount; 268 int cellSize = coordinates.getFolderCellWidth(); 269 270 // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that. 271 // Extra credit: maybe cache results across items as long as font size doesn't change. 272 273 final int totalWidth = measureFolders(availableSpace, cellSize); 274 int xStart = xEnd - Math.min(availableSpace, totalWidth); 275 final boolean overflow = totalWidth > availableSpace; 276 277 // Second pass to draw folders. 278 int i = 0; 279 for (Folder f : mFoldersSortedSet) { 280 if (availableSpace <= 0) { 281 break; 282 } 283 final String folderString = f.name; 284 final int fgColor = f.getForegroundColor(mDefaultFgColor); 285 final int bgColor = f.getBackgroundColor(mDefaultBgColor); 286 boolean labelTooLong = false; 287 final int textW = (int) sFoldersPaint.measureText(folderString); 288 int width = textW + cellSize + sFoldersLeftPadding; 289 290 if (overflow && width > averageWidth) { 291 if (i < foldersCount - 1) { 292 width = averageWidth; 293 } else { 294 // allow the last label to take all remaining space 295 // (and don't let it make room for padding) 296 width = availableSpace + sFoldersLeftPadding; 297 } 298 labelTooLong = true; 299 } 300 301 // TODO (mindyp): how to we get this? 302 final boolean isMuted = false; 303 // labelValues.folderId == 304 // sGmail.getFolderMap(mAccount).getFolderIdIgnored(); 305 306 // Draw the box. 307 sFoldersPaint.setColor(bgColor); 308 sFoldersPaint.setStyle(Paint.Style.FILL); 309 canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding, 310 y + height, sFoldersPaint); 311 312 // Draw the text. 313 final int padding = cellSize / 2; 314 sFoldersPaint.setColor(fgColor); 315 sFoldersPaint.setStyle(Paint.Style.FILL); 316 if (labelTooLong) { 317 final int rightBorder = xStart + width - sFoldersLeftPadding - padding; 318 final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, 319 y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP); 320 sFoldersPaint.setShader(shader); 321 } 322 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, 323 sFoldersPaint); 324 if (labelTooLong) { 325 sFoldersPaint.setShader(null); 326 } 327 328 availableSpace -= width; 329 xStart += width; 330 i++; 331 } 332 } 333 } 334 335 /** 336 * Helpers function to align an element in the center of a space. 337 */ 338 private static int getPadding(int space, int length) { 339 return (space - length) / 2; 340 } 341 342 public ConversationItemView(Context context, String account) { 343 super(context); 344 setClickable(true); 345 setLongClickable(true); 346 mContext = context.getApplicationContext(); 347 final Resources res = mContext.getResources(); 348 mTabletDevice = Utils.useTabletUI(res); 349 mIsExpansiveTablet = 350 mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false; 351 mListCollapsible = res.getBoolean(R.bool.list_collapsed); 352 mAccount = account; 353 354 if (STAR_OFF == null) { 355 // Initialize static bitmaps. 356 STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_star_off); 357 STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_star_on); 358 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 359 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 360 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 361 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 362 R.drawable.ic_email_caret_double_important_unread); 363 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 364 R.drawable.ic_email_caret_single_important_unread); 365 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 366 R.drawable.ic_email_caret_none_important_unread); 367 STATE_REPLIED = 368 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light); 369 STATE_FORWARDED = 370 BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light); 371 STATE_REPLIED_AND_FORWARDED = 372 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light); 373 STATE_CALENDAR_INVITE = 374 BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light); 375 VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, 376 R.drawable.ic_carrot_holo); 377 378 RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet); 379 380 // Initialize colors. 381 sActivatedTextColor = res.getColor(R.color.senders_text_color_read); 382 sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(sActivatedTextColor)); 383 sSendersTextColorRead = res.getColor(R.color.senders_text_color_read); 384 sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread); 385 sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext, 386 R.style.SubjectAppearanceUnreadStyle); 387 sSubjectTextReadSpan = new TextAppearanceSpan(mContext, 388 R.style.SubjectAppearanceReadStyle); 389 sSnippetTextUnreadSpan = 390 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread)); 391 sSnippetTextReadSpan = 392 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read)); 393 sDateTextColor = res.getColor(R.color.date_text_color); 394 sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop); 395 sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop); 396 sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen); 397 sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); 398 sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration); 399 // Initialize static color. 400 sSendersSplitToken = res.getString(R.string.senders_split_token); 401 sElidedPaddingToken = res.getString(R.string.elided_padding_token); 402 sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color); 403 sScrollSlop = res.getInteger(R.integer.swipeScrollSlop); 404 sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding); 405 sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); 406 sAttachmentPreviewsManager = ContactPhotoManager.createContactPhotoManager(context); 407 } 408 409 mSendersTextView = new TextView(mContext); 410 mSendersTextView.setIncludeFontPadding(false); 411 412 mSubjectTextView = new TextView(mContext); 413 mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END); 414 mSubjectTextView.setIncludeFontPadding(false); 415 416 mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() { 417 @Override 418 public void invalidate() { 419 if (mCoordinates == null) { 420 return; 421 } 422 ConversationItemView.this.invalidate(mCoordinates.contactImagesX, 423 mCoordinates.contactImagesY, 424 mCoordinates.contactImagesX + mCoordinates.contactImagesWidth, 425 mCoordinates.contactImagesY + mCoordinates.contactImagesHeight); 426 } 427 }); 428 mAttachmentPreviewsCanvas = new DividedImageCanvas(context, this); 429 } 430 431 public void bind(Conversation conversation, ControllableActivity activity, 432 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 433 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 434 bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set, 435 folder, checkboxOrSenderImage, swipeEnabled, priorityArrowEnabled, adapter); 436 } 437 438 private void bind(ConversationItemViewModel header, ControllableActivity activity, 439 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 440 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 441 // If this was previously bound to a conversation, remove any contact 442 // photo manager requests. 443 // TODO:MARKWEI attachment previews 444 if (mHeader != null) { 445 final ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds(); 446 if (divisionIds != null) { 447 mContactImagesHolder.reset(); 448 for (int pos = 0; pos < divisionIds.size(); pos++) { 449 sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash( 450 mContactImagesHolder, pos, divisionIds.get(pos))); 451 } 452 } 453 } 454 mCoordinates = null; 455 mHeader = header; 456 mActivity = activity; 457 mSelectedConversationSet = set; 458 mDisplayedFolder = folder; 459 mStarEnabled = folder != null && !folder.isTrash(); 460 mSwipeEnabled = swipeEnabled; 461 mAdapter = adapter; 462 if (mHeader.conversation.getAttachmentsCount() == 0) { 463 mAttachmentPreviewMode = ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE; 464 } else { 465 mAttachmentPreviewMode = mHeader.conversation.read ? 466 ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_SHORT 467 : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_TALL; 468 } 469 470 if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) { 471 mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO; 472 } else { 473 mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE; 474 } 475 476 // Initialize folder displayer. 477 if (mHeader.folderDisplayer == null) { 478 mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext); 479 } else { 480 mHeader.folderDisplayer.reset(); 481 } 482 483 final int ignoreFolderType; 484 if (mDisplayedFolder.isInbox()) { 485 ignoreFolderType = FolderType.INBOX; 486 } else { 487 ignoreFolderType = -1; 488 } 489 490 mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, mDisplayedFolder.uri, 491 ignoreFolderType); 492 493 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 494 mHeader.conversation.dateMs); 495 496 mConfig = new ConversationItemViewCoordinates.Config() 497 .withGadget(mGadgetMode) 498 .withAttachmentPreviews(mAttachmentPreviewMode); 499 if (header.folderDisplayer.hasVisibleFolders()) { 500 mConfig.showFolders(); 501 } 502 if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) { 503 mConfig.showReplyState(); 504 } 505 if (mHeader.conversation.color != 0) { 506 mConfig.showColorBlock(); 507 } 508 // Personal level. 509 mHeader.personalLevelBitmap = null; 510 if (true) { // TODO: hook this up to a setting 511 final int personalLevel = mHeader.conversation.personalLevel; 512 final boolean isImportant = 513 mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT; 514 final boolean useImportantMarkers = isImportant && priorityArrowEnabled; 515 516 if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { 517 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME 518 : ONLY_TO_ME; 519 } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { 520 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS 521 : TO_ME_AND_OTHERS; 522 } else if (useImportantMarkers) { 523 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; 524 } 525 } 526 if (mHeader.personalLevelBitmap != null) { 527 mConfig.showPersonalIndicator(); 528 } 529 530 setContentDescription(); 531 requestLayout(); 532 } 533 534 /** 535 * Get the Conversation object associated with this view. 536 */ 537 public Conversation getConversation() { 538 return mHeader.conversation; 539 } 540 541 private static void startTimer(String tag) { 542 if (sTimer != null) { 543 sTimer.start(tag); 544 } 545 } 546 547 private static void pauseTimer(String tag) { 548 if (sTimer != null) { 549 sTimer.pause(tag); 550 } 551 } 552 553 @Override 554 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 555 final int wSize = MeasureSpec.getSize(widthMeasureSpec); 556 557 final int currentMode = mActivity.getViewMode().getMode(); 558 if (wSize != mViewWidth || mPreviousMode != currentMode) { 559 mViewWidth = wSize; 560 mPreviousMode = currentMode; 561 } 562 mHeader.viewWidth = mViewWidth; 563 564 mConfig.updateWidth(wSize).setViewMode(currentMode); 565 566 Resources res = getResources(); 567 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 568 if (mHeader.standardScaledDimen != sStandardScaledDimen) { 569 // Large Text has been toggle on/off. Update the static dimens. 570 sStandardScaledDimen = mHeader.standardScaledDimen; 571 ConversationItemViewCoordinates.refreshConversationDimens(mContext); 572 } 573 574 mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig, 575 mAdapter.getCoordinatesCache()); 576 577 final int h = (mAnimatedHeightFraction != 1.0f) ? 578 Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height; 579 setMeasuredDimension(mConfig.getWidth(), h); 580 } 581 582 @Override 583 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 584 startTimer(PERF_TAG_LAYOUT); 585 Utils.traceBeginSection("CIVC.layout"); 586 587 super.onLayout(changed, left, top, right, bottom); 588 589 calculateTextsAndBitmaps(); 590 calculateCoordinates(); 591 592 // Subject. 593 createSubject(mHeader.unread); 594 595 if (!mHeader.isLayoutValid(mContext)) { 596 setContentDescription(); 597 } 598 mHeader.validate(mContext); 599 600 pauseTimer(PERF_TAG_LAYOUT); 601 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 602 sTimer.dumpResults(); 603 sTimer = new Timer(); 604 sLayoutCount = 0; 605 } 606 Utils.traceEndSection(); 607 } 608 609 private void setContentDescription() { 610 if (mActivity.isAccessibilityEnabled()) { 611 mHeader.resetContentDescription(); 612 setContentDescription(mHeader.getContentDescription(mContext)); 613 } 614 } 615 616 @Override 617 public void setBackgroundResource(int resourceId) { 618 Drawable drawable = mBackgrounds.get(resourceId); 619 if (drawable == null) { 620 drawable = getResources().getDrawable(resourceId); 621 mBackgrounds.put(resourceId, drawable); 622 } 623 if (getBackground() != drawable) { 624 super.setBackgroundDrawable(drawable); 625 } 626 } 627 628 private void calculateTextsAndBitmaps() { 629 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 630 631 if (mSelectedConversationSet != null) { 632 mSelected = mSelectedConversationSet.contains(mHeader.conversation); 633 } 634 setSelected(mSelected); 635 mHeader.gadgetMode = mGadgetMode; 636 637 final boolean isUnread = mHeader.unread; 638 updateBackground(isUnread); 639 640 mHeader.sendersDisplayText = new SpannableStringBuilder(); 641 mHeader.styledSendersString = null; 642 643 // Parse senders fragments. 644 if (mHeader.conversation.conversationInfo != null) { 645 // This is Gmail 646 Context context = getContext(); 647 mHeader.messageInfoString = SendersView 648 .createMessageInfo(context, mHeader.conversation, true); 649 int maxChars = ConversationItemViewCoordinates.getSendersLength(context, 650 mCoordinates.getMode(), mHeader.conversation.hasAttachments); 651 mHeader.displayableSenderEmails = new ArrayList<String>(); 652 mHeader.displayableSenderNames = new ArrayList<String>(); 653 mHeader.styledSenders = new ArrayList<SpannableString>(); 654 SendersView.format(context, mHeader.conversation.conversationInfo, 655 mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders, 656 mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount, 657 true); 658 // If we have displayable senders, load their thumbnails 659 loadSenderImages(); 660 } else { 661 // This is Email 662 SendersView.formatSenders(mHeader, getContext(), true); 663 if (!TextUtils.isEmpty(mHeader.conversation.senders)) { 664 mHeader.displayableSenderEmails = new ArrayList<String>(); 665 mHeader.displayableSenderNames = new ArrayList<String>(); 666 667 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders); 668 for (int i = 0; i < tokens.length;i++) { 669 final Rfc822Token token = tokens[i]; 670 final String senderName = Address.decodeAddressName(token.getName()); 671 final String senderAddress = token.getAddress(); 672 mHeader.displayableSenderEmails.add(senderAddress); 673 mHeader.displayableSenderNames.add( 674 !TextUtils.isEmpty(senderName) ? senderName : senderAddress); 675 } 676 loadSenderImages(); 677 } 678 } 679 680 if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { 681 loadAttachmentPreviews(); 682 } 683 684 if (mHeader.isLayoutValid(mContext)) { 685 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 686 return; 687 } 688 startTimer(PERF_TAG_CALCULATE_FOLDERS); 689 690 691 pauseTimer(PERF_TAG_CALCULATE_FOLDERS); 692 693 // Paper clip icon. 694 mHeader.paperclip = null; 695 if (mHeader.conversation.hasAttachments) { 696 mHeader.paperclip = ATTACHMENT; 697 } 698 699 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 700 701 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 702 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 703 } 704 705 // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which 706 // is immutable. 707 private void loadSenderImages() { 708 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 709 && mHeader.displayableSenderEmails != null 710 && mHeader.displayableSenderEmails.size() > 0) { 711 if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) { 712 LogUtils.w(LOG_TAG, 713 "Contact image width(%d) or height(%d) is 0 for mode: (%d).", 714 mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, 715 mCoordinates.getMode()); 716 return; 717 } 718 mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth, 719 mCoordinates.contactImagesHeight); 720 mContactImagesHolder.setDivisionIds(mHeader.displayableSenderEmails); 721 int size = mHeader.displayableSenderEmails.size(); 722 String emailAddress; 723 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 724 emailAddress = mHeader.displayableSenderEmails.get(i); 725 PhotoIdentifier photoIdentifier = new ContactIdentifier( 726 mHeader.displayableSenderNames.get(i), emailAddress, i); 727 sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder); 728 } 729 } 730 } 731 732 private void loadAttachmentPreviews() { 733 if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { 734 final int attachmentPreviewsHeight = ConversationItemViewCoordinates 735 .getAttachmentPreviewsHeight(mContext, mAttachmentPreviewMode); 736 if (mCoordinates.attachmentPreviewsWidth <= 0 || attachmentPreviewsHeight <= 0) { 737 LogUtils.w(LOG_TAG, 738 "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).", 739 mCoordinates.attachmentPreviewsWidth, attachmentPreviewsHeight, 740 mCoordinates.getMode(), mAttachmentPreviewMode); 741 return; 742 } 743 mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth, 744 attachmentPreviewsHeight); 745 ArrayList<String> attachments = mHeader.conversation.getAttachments(); 746 mAttachmentPreviewsCanvas.setDivisionIds(attachments); 747 int size = attachments.size(); 748 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 749 String attachment = attachments.get(i); 750 PhotoIdentifier photoIdentifier = new ContactIdentifier( 751 attachment, attachment, i); 752 sAttachmentPreviewsManager.loadThumbnail( 753 photoIdentifier, mAttachmentPreviewsCanvas); 754 } 755 } 756 } 757 758 private static int makeExactSpecForSize(int size) { 759 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 760 } 761 762 private static void layoutViewExactly(View v, int w, int h) { 763 v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h)); 764 v.layout(0, 0, w, h); 765 } 766 767 private void layoutSenders() { 768 if (mHeader.styledSendersString != null) { 769 if (isActivated() && showActivatedText()) { 770 mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0, 771 mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 772 } else { 773 mHeader.styledSendersString.removeSpan(sActivatedTextSpan); 774 } 775 776 final int w = mSendersWidth; 777 final int h = mCoordinates.sendersHeight; 778 mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h)); 779 mSendersTextView.setMaxLines(mCoordinates.sendersLineCount); 780 mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize); 781 layoutViewExactly(mSendersTextView, w, h); 782 783 mSendersTextView.setText(mHeader.styledSendersString); 784 } 785 } 786 787 private void createSubject(final boolean isUnread) { 788 final String subject = filterTag(mHeader.conversation.subject); 789 final String snippet = mHeader.conversation.getSnippet(); 790 final Spannable displayedStringBuilder = new SpannableString( 791 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet)); 792 793 // since spans affect text metrics, add spans to the string before measure/layout or fancy 794 // ellipsizing 795 final int subjectTextLength = (subject != null) ? subject.length() : 0; 796 if (!TextUtils.isEmpty(subject)) { 797 displayedStringBuilder.setSpan(TextAppearanceSpan.wrap( 798 isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength, 799 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 800 } 801 if (!TextUtils.isEmpty(snippet)) { 802 final int startOffset = subjectTextLength; 803 // Start after the end of the subject text; since the subject may be 804 // "" or null, this could start at the 0th character in the subjectText string 805 displayedStringBuilder.setSpan(ForegroundColorSpan.wrap( 806 isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset, 807 displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 808 } 809 if (isActivated() && showActivatedText()) { 810 displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(), 811 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 812 } 813 814 final int subjectWidth = mCoordinates.subjectWidth; 815 final int subjectHeight = mCoordinates.subjectHeight; 816 mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight)); 817 mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount); 818 mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize); 819 layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight); 820 821 mSubjectTextView.setText(displayedStringBuilder); 822 } 823 824 private boolean showActivatedText() { 825 // For activated elements in tablet in conversation mode, we show an activated color, since 826 // the background is dark blue for activated versus gray for non-activated. 827 final boolean isListCollapsed = mContext.getResources().getBoolean(R.bool.list_collapsed); 828 return mTabletDevice && !isListCollapsed; 829 } 830 831 private boolean canFitFragment(int width, int line, int fixedWidth) { 832 if (line == mCoordinates.sendersLineCount) { 833 return width + fixedWidth <= mSendersWidth; 834 } else { 835 return width <= mSendersWidth; 836 } 837 } 838 839 private void calculateCoordinates() { 840 startTimer(PERF_TAG_CALCULATE_COORDINATES); 841 842 sPaint.setTextSize(mCoordinates.dateFontSize); 843 sPaint.setTypeface(Typeface.DEFAULT); 844 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText( 845 mHeader.dateText != null ? mHeader.dateText.toString() : ""); 846 847 mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft; 848 849 if (mCoordinates.isWide()) { 850 // In wide mode, the end of the senders should align with 851 // the start of the subject and is based on a max width. 852 mSendersWidth = mCoordinates.sendersWidth; 853 } else { 854 // In normal mode, the width is based on where the date/attachment icon start. 855 final int dateAttachmentStart; 856 // Have this end near the paperclip or date, not the folders. 857 if (mHeader.paperclip != null) { 858 dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft; 859 } else { 860 dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft; 861 } 862 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX; 863 } 864 865 // Second pass to layout each fragment. 866 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; 867 868 sPaint.setTextSize(mCoordinates.sendersFontSize); 869 sPaint.setTypeface(Typeface.DEFAULT); 870 871 if (mHeader.styledSenders != null) { 872 ellipsizeStyledSenders(); 873 layoutSenders(); 874 } else { 875 // First pass to calculate width of each fragment. 876 int totalWidth = 0; 877 int fixedWidth = 0; 878 for (SenderFragment senderFragment : mHeader.senderFragments) { 879 CharacterStyle style = senderFragment.style; 880 int start = senderFragment.start; 881 int end = senderFragment.end; 882 style.updateDrawState(sPaint); 883 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 884 boolean isFixed = senderFragment.isFixed; 885 if (isFixed) { 886 fixedWidth += senderFragment.width; 887 } 888 totalWidth += senderFragment.width; 889 } 890 891 if (!ConversationItemViewCoordinates.displaySendersInline(mCoordinates.getMode())) { 892 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; 893 } 894 if (mSendersWidth < 0) { 895 mSendersWidth = 0; 896 } 897 totalWidth = ellipsize(fixedWidth); 898 mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint, 899 mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 900 } 901 902 if (mSendersWidth < 0) { 903 mSendersWidth = 0; 904 } 905 906 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 907 } 908 909 // The rules for displaying ellipsized senders are as follows: 910 // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown 911 // 2) If senders do not fit, ellipsize the last one that does fit, and stop 912 // appending new senders 913 private int ellipsizeStyledSenders() { 914 SpannableStringBuilder builder = new SpannableStringBuilder(); 915 float totalWidth = 0; 916 boolean ellipsize = false; 917 float width; 918 SpannableStringBuilder messageInfoString = mHeader.messageInfoString; 919 if (messageInfoString.length() > 0) { 920 CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(), 921 CharacterStyle.class); 922 // There is only 1 character style span; make sure we apply all the 923 // styles to the paint object before measuring. 924 if (spans.length > 0) { 925 spans[0].updateDrawState(sPaint); 926 } 927 // Paint the message info string to see if we lose space. 928 float messageInfoWidth = sPaint.measureText(messageInfoString.toString()); 929 totalWidth += messageInfoWidth; 930 } 931 SpannableString prevSender = null; 932 SpannableString ellipsizedText; 933 for (SpannableString sender : mHeader.styledSenders) { 934 // There may be null sender strings if there were dupes we had to remove. 935 if (sender == null) { 936 continue; 937 } 938 // No more width available, we'll only show fixed fragments. 939 if (ellipsize) { 940 break; 941 } 942 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 943 // There is only 1 character style span. 944 if (spans.length > 0) { 945 spans[0].updateDrawState(sPaint); 946 } 947 // If there are already senders present in this string, we need to 948 // make sure we prepend the dividing token 949 if (SendersView.sElidedString.equals(sender.toString())) { 950 prevSender = sender; 951 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 952 } else if (builder.length() > 0 953 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 954 .toString()))) { 955 prevSender = sender; 956 sender = copyStyles(spans, sSendersSplitToken + sender); 957 } else { 958 prevSender = sender; 959 } 960 if (spans.length > 0) { 961 spans[0].updateDrawState(sPaint); 962 } 963 // Measure the width of the current sender and make sure we have space 964 width = (int) sPaint.measureText(sender.toString()); 965 if (width + totalWidth > mSendersWidth) { 966 // The text is too long, new line won't help. We have to 967 // ellipsize text. 968 ellipsize = true; 969 width = mSendersWidth - totalWidth; // ellipsis width? 970 ellipsizedText = copyStyles(spans, 971 TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END)); 972 width = (int) sPaint.measureText(ellipsizedText.toString()); 973 } else { 974 ellipsizedText = null; 975 } 976 totalWidth += width; 977 978 final CharSequence fragmentDisplayText; 979 if (ellipsizedText != null) { 980 fragmentDisplayText = ellipsizedText; 981 } else { 982 fragmentDisplayText = sender; 983 } 984 builder.append(fragmentDisplayText); 985 } 986 mHeader.styledMessageInfoStringOffset = builder.length(); 987 if (messageInfoString != null) { 988 builder.append(messageInfoString); 989 } 990 mHeader.styledSendersString = builder; 991 return (int)totalWidth; 992 } 993 994 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 995 SpannableString s = new SpannableString(newText); 996 if (spans != null && spans.length > 0) { 997 s.setSpan(spans[0], 0, s.length(), 0); 998 } 999 return s; 1000 } 1001 1002 private int ellipsize(int fixedWidth) { 1003 int totalWidth = 0; 1004 int currentLine = 1; 1005 boolean ellipsize = false; 1006 for (SenderFragment senderFragment : mHeader.senderFragments) { 1007 CharacterStyle style = senderFragment.style; 1008 int start = senderFragment.start; 1009 int end = senderFragment.end; 1010 int width = senderFragment.width; 1011 boolean isFixed = senderFragment.isFixed; 1012 style.updateDrawState(sPaint); 1013 1014 // No more width available, we'll only show fixed fragments. 1015 if (ellipsize && !isFixed) { 1016 senderFragment.shouldDisplay = false; 1017 continue; 1018 } 1019 1020 // New line and ellipsize text if needed. 1021 senderFragment.ellipsizedText = null; 1022 if (isFixed) { 1023 fixedWidth -= width; 1024 } 1025 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 1026 // The text is too long, new line won't help. We have to 1027 // ellipsize text. 1028 if (totalWidth == 0) { 1029 ellipsize = true; 1030 } else { 1031 // New line. 1032 if (currentLine < mCoordinates.sendersLineCount) { 1033 currentLine++; 1034 totalWidth = 0; 1035 // The text is still too long, we have to ellipsize 1036 // text. 1037 if (totalWidth + width > mSendersWidth) { 1038 ellipsize = true; 1039 } 1040 } else { 1041 ellipsize = true; 1042 } 1043 } 1044 1045 if (ellipsize) { 1046 width = mSendersWidth - totalWidth; 1047 // No more new line, we have to reserve width for fixed 1048 // fragments. 1049 if (currentLine == mCoordinates.sendersLineCount) { 1050 width -= fixedWidth; 1051 } 1052 senderFragment.ellipsizedText = TextUtils.ellipsize( 1053 mHeader.sendersText.substring(start, end), sPaint, width, 1054 TruncateAt.END).toString(); 1055 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 1056 } 1057 } 1058 senderFragment.shouldDisplay = true; 1059 totalWidth += width; 1060 1061 final CharSequence fragmentDisplayText; 1062 if (senderFragment.ellipsizedText != null) { 1063 fragmentDisplayText = senderFragment.ellipsizedText; 1064 } else { 1065 fragmentDisplayText = mHeader.sendersText.substring(start, end); 1066 } 1067 final int spanStart = mHeader.sendersDisplayText.length(); 1068 mHeader.sendersDisplayText.append(fragmentDisplayText); 1069 mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart, 1070 mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1071 } 1072 return totalWidth; 1073 } 1074 1075 /** 1076 * If the subject contains the tag of a mailing-list (text surrounded with 1077 * []), return the subject with that tag ellipsized, e.g. 1078 * "[android-gmail-team] Hello" -> "[andr...] Hello" 1079 */ 1080 private String filterTag(String subject) { 1081 String result = subject; 1082 String formatString = getContext().getResources().getString(R.string.filtered_tag); 1083 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 1084 int end = subject.indexOf(']'); 1085 if (end > 0) { 1086 String tag = subject.substring(1, end); 1087 result = String.format(formatString, Utils.ellipsize(tag, 7), 1088 subject.substring(end + 1)); 1089 } 1090 } 1091 return result; 1092 } 1093 1094 @Override 1095 protected void onDraw(Canvas canvas) { 1096 Utils.traceBeginSection("CIVC.draw"); 1097 // Contact photo 1098 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) { 1099 canvas.save(); 1100 drawContactImages(canvas); 1101 canvas.restore(); 1102 } 1103 // Senders. 1104 boolean isUnread = mHeader.unread; 1105 // Old style senders; apply text colors/ sizes/ styling. 1106 canvas.save(); 1107 if (mHeader.sendersDisplayLayout != null) { 1108 sPaint.setTextSize(mCoordinates.sendersFontSize); 1109 sPaint.setTypeface(SendersView.getTypeface(isUnread)); 1110 sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead); 1111 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY 1112 + mHeader.sendersDisplayLayout.getTopPadding()); 1113 mHeader.sendersDisplayLayout.draw(canvas); 1114 } else { 1115 drawSenders(canvas); 1116 } 1117 canvas.restore(); 1118 1119 1120 // Subject. 1121 sPaint.setTypeface(Typeface.DEFAULT); 1122 canvas.save(); 1123 drawSubject(canvas); 1124 canvas.restore(); 1125 1126 // Folders. 1127 if (mConfig.areFoldersVisible()) { 1128 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates); 1129 } 1130 1131 // If this folder has a color (combined view/Email), show it here 1132 if (mConfig.isColorBlockVisible()) { 1133 sFoldersPaint.setColor(mHeader.conversation.color); 1134 sFoldersPaint.setStyle(Paint.Style.FILL); 1135 canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY, 1136 mCoordinates.colorBlockX + mCoordinates.colorBlockWidth, 1137 mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint); 1138 } 1139 1140 // Draw the reply state. Draw nothing if neither replied nor forwarded. 1141 if (mConfig.isReplyStateVisible()) { 1142 if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) { 1143 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX, 1144 mCoordinates.replyStateY, null); 1145 } else if (mHeader.hasBeenRepliedTo) { 1146 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX, 1147 mCoordinates.replyStateY, null); 1148 } else if (mHeader.hasBeenForwarded) { 1149 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX, 1150 mCoordinates.replyStateY, null); 1151 } else if (mHeader.isInvite) { 1152 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX, 1153 mCoordinates.replyStateY, null); 1154 } 1155 } 1156 1157 if (mConfig.isPersonalIndicatorVisible()) { 1158 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX, 1159 mCoordinates.personalIndicatorY, null); 1160 } 1161 1162 // Date. 1163 sPaint.setTextSize(mCoordinates.dateFontSize); 1164 sPaint.setTypeface(Typeface.DEFAULT); 1165 sPaint.setColor(sDateTextColor); 1166 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, 1167 sPaint); 1168 1169 // Paper clip icon. 1170 if (mHeader.paperclip != null) { 1171 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 1172 } 1173 1174 if (mStarEnabled) { 1175 // Star. 1176 canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint); 1177 } 1178 1179 // Attachment previews 1180 if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { 1181 canvas.save(); 1182 drawAttachmentPreviews(canvas); 1183 canvas.restore(); 1184 } 1185 1186 // right-side edge effect when in tablet conversation mode and the list is not collapsed 1187 if (mTabletDevice && !mListCollapsible && 1188 ViewMode.isConversationMode(mConfig.getViewMode())) { 1189 RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0, 1190 getWidth(), getHeight()); 1191 RIGHT_EDGE_TABLET.draw(canvas); 1192 1193 if (isActivated()) { 1194 // draw caret on the right, centered vertically 1195 final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth(); 1196 final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2; 1197 canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null); 1198 } 1199 } 1200 Utils.traceEndSection(); 1201 } 1202 1203 private void drawContactImages(Canvas canvas) { 1204 canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); 1205 mContactImagesHolder.draw(canvas); 1206 } 1207 1208 private void drawAttachmentPreviews(Canvas canvas) { 1209 canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); 1210 mAttachmentPreviewsCanvas.draw(canvas); 1211 } 1212 1213 private void drawSubject(Canvas canvas) { 1214 canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY); 1215 mSubjectTextView.draw(canvas); 1216 } 1217 1218 private void drawSenders(Canvas canvas) { 1219 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY); 1220 mSendersTextView.draw(canvas); 1221 } 1222 1223 private Bitmap getStarBitmap() { 1224 return mHeader.conversation.starred ? STAR_ON : STAR_OFF; 1225 } 1226 1227 private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 1228 canvas.drawText(s, 0, s.length(), x, y, paint); 1229 } 1230 1231 /** 1232 * Set the background for this item based on: 1233 * 1. Read / Unread (unread messages have a lighter background) 1234 * 2. Tablet / Phone 1235 * 3. Checkbox checked / Unchecked (controls CAB color for item) 1236 * 4. Activated / Not activated (controls the blue highlight on tablet) 1237 * @param isUnread 1238 */ 1239 private void updateBackground(boolean isUnread) { 1240 final int background; 1241 if (isUnread) { 1242 background = R.drawable.conversation_unread_selector; 1243 } else { 1244 background = R.drawable.conversation_read_selector; 1245 } 1246 setBackgroundResource(background); 1247 } 1248 1249 /** 1250 * Toggle the check mark on this view and update the conversation or begin 1251 * drag, if drag is enabled. 1252 */ 1253 @Override 1254 public void toggleSelectedStateOrBeginDrag() { 1255 ViewMode mode = mActivity.getViewMode(); 1256 if (mIsExpansiveTablet && mode.isListMode()) { 1257 beginDragMode(); 1258 } else { 1259 toggleSelectedState(); 1260 } 1261 } 1262 1263 @Override 1264 public void toggleSelectedState() { 1265 if (mHeader != null && mHeader.conversation != null) { 1266 mSelected = !mSelected; 1267 setSelected(mSelected); 1268 Conversation conv = mHeader.conversation; 1269 // Set the list position of this item in the conversation 1270 SwipeableListView listView = getListView(); 1271 conv.position = mSelected && listView != null ? listView.getPositionForView(this) 1272 : Conversation.NO_POSITION; 1273 if (mSelectedConversationSet != null) { 1274 mSelectedConversationSet.toggle(conv); 1275 } 1276 if (mSelectedConversationSet.isEmpty()) { 1277 listView.commitDestructiveActions(true); 1278 } 1279 // We update the background after the checked state has changed 1280 // now that we have a selected background asset. Setting the background 1281 // usually waits for a layout pass, but we don't need a full layout, 1282 // just an update to the background. 1283 requestLayout(); 1284 } 1285 } 1286 1287 /** 1288 * Toggle the star on this view and update the conversation. 1289 */ 1290 public void toggleStar() { 1291 mHeader.conversation.starred = !mHeader.conversation.starred; 1292 Bitmap starBitmap = getStarBitmap(); 1293 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 1294 + starBitmap.getWidth(), 1295 mCoordinates.starY + starBitmap.getHeight()); 1296 ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor(); 1297 if (cursor != null) { 1298 cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED, 1299 mHeader.conversation.starred); 1300 } 1301 } 1302 1303 private boolean isTouchInContactPhoto(float x) { 1304 // Everything before the right edge of contact photo 1305 return (mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 1306 && x < (mCoordinates.contactImagesX + mCoordinates.contactImagesWidth 1307 + sSenderImageTouchSlop)); 1308 } 1309 1310 private boolean isTouchInStar(float x, float y) { 1311 // Everything after the star and include a touch slop. 1312 return mStarEnabled && x > mCoordinates.starX - sStarTouchSlop; 1313 } 1314 1315 @Override 1316 public boolean canChildBeDismissed() { 1317 return true; 1318 } 1319 1320 @Override 1321 public void dismiss() { 1322 SwipeableListView listView = getListView(); 1323 if (listView != null) { 1324 getListView().dismissChild(this); 1325 } 1326 } 1327 1328 private boolean onTouchEventNoSwipe(MotionEvent event) { 1329 boolean handled = false; 1330 1331 int x = (int) event.getX(); 1332 int y = (int) event.getY(); 1333 mLastTouchX = x; 1334 mLastTouchY = y; 1335 switch (event.getAction()) { 1336 case MotionEvent.ACTION_DOWN: 1337 if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) { 1338 mDownEvent = true; 1339 handled = true; 1340 } 1341 break; 1342 1343 case MotionEvent.ACTION_CANCEL: 1344 mDownEvent = false; 1345 break; 1346 1347 case MotionEvent.ACTION_UP: 1348 if (mDownEvent) { 1349 if (isTouchInContactPhoto(x)) { 1350 // Touch on the check mark 1351 toggleSelectedState(); 1352 } else if (isTouchInStar(x, y)) { 1353 // Touch on the star 1354 toggleStar(); 1355 } 1356 handled = true; 1357 } 1358 break; 1359 } 1360 1361 if (!handled) { 1362 handled = super.onTouchEvent(event); 1363 } 1364 1365 return handled; 1366 } 1367 1368 /** 1369 * ConversationItemView is given the first chance to handle touch events. 1370 */ 1371 @Override 1372 public boolean onTouchEvent(MotionEvent event) { 1373 int x = (int) event.getX(); 1374 int y = (int) event.getY(); 1375 mLastTouchX = x; 1376 mLastTouchY = y; 1377 if (!mSwipeEnabled) { 1378 return onTouchEventNoSwipe(event); 1379 } 1380 switch (event.getAction()) { 1381 case MotionEvent.ACTION_DOWN: 1382 if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) { 1383 mDownEvent = true; 1384 return true; 1385 } 1386 break; 1387 case MotionEvent.ACTION_UP: 1388 if (mDownEvent) { 1389 if (isTouchInContactPhoto(x)) { 1390 // Touch on the check mark 1391 mDownEvent = false; 1392 toggleSelectedState(); 1393 return true; 1394 } else if (isTouchInStar(x, y)) { 1395 // Touch on the star 1396 mDownEvent = false; 1397 toggleStar(); 1398 return true; 1399 } 1400 } 1401 break; 1402 } 1403 // Let View try to handle it as well. 1404 boolean handled = super.onTouchEvent(event); 1405 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1406 return true; 1407 } 1408 return handled; 1409 } 1410 1411 @Override 1412 public boolean performClick() { 1413 final boolean handled = super.performClick(); 1414 final SwipeableListView list = getListView(); 1415 if (list != null && list.getAdapter() != null) { 1416 final int pos = list.findConversation(this, mHeader.conversation); 1417 list.performItemClick(this, pos, mHeader.conversation.id); 1418 } 1419 return handled; 1420 } 1421 1422 private SwipeableListView getListView() { 1423 SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent()) 1424 .getListView(); 1425 if (v == null) { 1426 v = mAdapter.getListView(); 1427 } 1428 return v; 1429 } 1430 1431 /** 1432 * Reset any state associated with this conversation item view so that it 1433 * can be reused. 1434 */ 1435 public void reset() { 1436 setAlpha(1f); 1437 setTranslationX(0f); 1438 mAnimatedHeightFraction = 1.0f; 1439 } 1440 1441 @SuppressWarnings("deprecation") 1442 @Override 1443 public void setTranslationX(float translationX) { 1444 super.setTranslationX(translationX); 1445 1446 final ViewParent vp = getParent(); 1447 if (vp == null || !(vp instanceof SwipeableConversationItemView)) { 1448 LogUtils.w(LOG_TAG, 1449 "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s", 1450 vp, translationX); 1451 } 1452 1453 // When a list item is being swiped or animated, ensure that the hosting view has a 1454 // background color set. We only enable the background during the X-translation effect to 1455 // reduce overdraw during normal list scrolling. 1456 final SwipeableConversationItemView parent = (SwipeableConversationItemView) vp; 1457 if (translationX != 0f) { 1458 parent.setBackgroundResource(R.color.swiped_bg_color); 1459 } else { 1460 parent.setBackgroundDrawable(null); 1461 } 1462 } 1463 1464 /** 1465 * Grow the height of the item and fade it in when bringing a conversation 1466 * back from a destructive action. 1467 */ 1468 public Animator createSwipeUndoAnimation() { 1469 ObjectAnimator undoAnimator = createTranslateXAnimation(true); 1470 return undoAnimator; 1471 } 1472 1473 /** 1474 * Grow the height of the item and fade it in when bringing a conversation 1475 * back from a destructive action. 1476 */ 1477 public Animator createUndoAnimation() { 1478 ObjectAnimator height = createHeightAnimation(true); 1479 Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f); 1480 fade.setDuration(sShrinkAnimationDuration); 1481 fade.setInterpolator(new DecelerateInterpolator(2.0f)); 1482 AnimatorSet transitionSet = new AnimatorSet(); 1483 transitionSet.playTogether(height, fade); 1484 transitionSet.addListener(new HardwareLayerEnabler(this)); 1485 return transitionSet; 1486 } 1487 1488 /** 1489 * Grow the height of the item and fade it in when bringing a conversation 1490 * back from a destructive action. 1491 */ 1492 public Animator createDestroyWithSwipeAnimation() { 1493 ObjectAnimator slide = createTranslateXAnimation(false); 1494 ObjectAnimator height = createHeightAnimation(false); 1495 AnimatorSet transitionSet = new AnimatorSet(); 1496 transitionSet.playSequentially(slide, height); 1497 return transitionSet; 1498 } 1499 1500 private ObjectAnimator createTranslateXAnimation(boolean show) { 1501 SwipeableListView parent = getListView(); 1502 // If we can't get the parent...we have bigger problems. 1503 int width = parent != null ? parent.getMeasuredWidth() : 0; 1504 final float start = show ? width : 0f; 1505 final float end = show ? 0f : width; 1506 ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end); 1507 slide.setInterpolator(new DecelerateInterpolator(2.0f)); 1508 slide.setDuration(sSlideAnimationDuration); 1509 return slide; 1510 } 1511 1512 public Animator createDestroyAnimation() { 1513 return createHeightAnimation(false); 1514 } 1515 1516 private ObjectAnimator createHeightAnimation(boolean show) { 1517 final float start = show ? 0f : 1.0f; 1518 final float end = show ? 1.0f : 0f; 1519 ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end); 1520 height.setInterpolator(new DecelerateInterpolator(2.0f)); 1521 height.setDuration(sShrinkAnimationDuration); 1522 return height; 1523 } 1524 1525 // Used by animator 1526 public void setAnimatedHeightFraction(float height) { 1527 mAnimatedHeightFraction = height; 1528 requestLayout(); 1529 } 1530 1531 @Override 1532 public SwipeableView getSwipeableView() { 1533 return SwipeableView.from(this); 1534 } 1535 1536 /** 1537 * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag. 1538 */ 1539 private void beginDragMode() { 1540 if (mLastTouchX < 0 || mLastTouchY < 0) { 1541 return; 1542 } 1543 // If this is already checked, don't bother unchecking it! 1544 if (!mSelected) { 1545 toggleSelectedState(); 1546 } 1547 1548 // Clip data has form: [conversations_uri, conversationId1, 1549 // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] 1550 final int count = mSelectedConversationSet.size(); 1551 String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count); 1552 1553 final ClipData data = ClipData.newUri(mContext.getContentResolver(), description, 1554 Conversation.MOVE_CONVERSATIONS_URI); 1555 for (Conversation conversation : mSelectedConversationSet.values()) { 1556 data.addItem(new Item(String.valueOf(conversation.position))); 1557 } 1558 // Protect against non-existent views: only happens for monkeys 1559 final int width = this.getWidth(); 1560 final int height = this.getHeight(); 1561 final boolean isDimensionNegative = (width < 0) || (height < 0); 1562 if (isDimensionNegative) { 1563 LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: " 1564 + "width=%d, height=%d", width, height); 1565 return; 1566 } 1567 mActivity.startDragMode(); 1568 // Start drag mode 1569 startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0); 1570 } 1571 1572 /** 1573 * Handles the drag event. 1574 * 1575 * @param event the drag event to be handled 1576 */ 1577 @Override 1578 public boolean onDragEvent(DragEvent event) { 1579 switch (event.getAction()) { 1580 case DragEvent.ACTION_DRAG_ENDED: 1581 mActivity.stopDragMode(); 1582 return true; 1583 } 1584 return false; 1585 } 1586 1587 private class ShadowBuilder extends DragShadowBuilder { 1588 private final Drawable mBackground; 1589 1590 private final View mView; 1591 private final String mDragDesc; 1592 private final int mTouchX; 1593 private final int mTouchY; 1594 private int mDragDescX; 1595 private int mDragDescY; 1596 1597 public ShadowBuilder(View view, int count, int touchX, int touchY) { 1598 super(view); 1599 mView = view; 1600 mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo); 1601 mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count); 1602 mTouchX = touchX; 1603 mTouchY = touchY; 1604 } 1605 1606 @Override 1607 public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 1608 int width = mView.getWidth(); 1609 int height = mView.getHeight(); 1610 mDragDescX = mCoordinates.sendersX; 1611 mDragDescY = getPadding(height, (int) mCoordinates.subjectFontSize) 1612 - mCoordinates.subjectAscent; 1613 shadowSize.set(width, height); 1614 shadowTouchPoint.set(mTouchX, mTouchY); 1615 } 1616 1617 @Override 1618 public void onDrawShadow(Canvas canvas) { 1619 mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight()); 1620 mBackground.draw(canvas); 1621 sPaint.setTextSize(mCoordinates.subjectFontSize); 1622 canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint); 1623 } 1624 } 1625 1626 @Override 1627 public float getMinAllowScrollDistance() { 1628 return sScrollSlop; 1629 } 1630} 1631