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