ConversationItemView.java revision 7d9143a225ef095db2ee6aa2e2d8aee237af4a8f
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 private static final String EMPTY_SNIPPET = ""; 197 198 static { 199 sPaint.setAntiAlias(true); 200 sFoldersPaint.setAntiAlias(true); 201 } 202 203 /** 204 * Handles displaying folders in a conversation header view. 205 */ 206 static class ConversationItemFolderDisplayer extends FolderDisplayer { 207 // Maximum number of folders to be displayed. 208 private static final int MAX_DISPLAYED_FOLDERS_COUNT = 4; 209 210 private int mFoldersCount; 211 private boolean mHasMoreFolders; 212 213 public ConversationItemFolderDisplayer(Context context) { 214 super(context); 215 } 216 217 @Override 218 public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri, 219 final int ignoreFolderType) { 220 super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType); 221 222 mFoldersCount = mFoldersSortedSet.size(); 223 mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT; 224 mFoldersCount = Math.min(mFoldersCount, MAX_DISPLAYED_FOLDERS_COUNT); 225 } 226 227 @Override 228 public void reset() { 229 super.reset(); 230 mFoldersCount = 0; 231 mHasMoreFolders = false; 232 } 233 234 public boolean hasVisibleFolders() { 235 return mFoldersCount > 0; 236 } 237 238 private int measureFolders(int mode) { 239 int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode); 240 int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode, 241 mFoldersCount); 242 243 int totalWidth = 0; 244 for (Folder f : mFoldersSortedSet) { 245 final String folderString = f.name; 246 int width = (int) sFoldersPaint.measureText(folderString) + cellSize; 247 if (width % cellSize != 0) { 248 width += cellSize - (width % cellSize); 249 } 250 totalWidth += width; 251 if (totalWidth > availableSpace) { 252 break; 253 } 254 } 255 256 return totalWidth; 257 } 258 259 public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates, 260 int foldersXEnd, int mode) { 261 if (mFoldersCount == 0) { 262 return; 263 } 264 int xEnd = foldersXEnd; 265 int y = coordinates.foldersY; 266 int height = coordinates.foldersHeight; 267 int topPadding = coordinates.foldersTopPadding; 268 int ascent = coordinates.foldersAscent; 269 int textBottomPadding = coordinates.foldersTextBottomPadding; 270 271 sFoldersPaint.setTextSize(coordinates.foldersFontSize); 272 273 // Initialize space and cell size based on the current mode. 274 int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode); 275 int averageWidth = availableSpace / mFoldersCount; 276 int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode, 277 mFoldersCount); 278 279 int totalWidth = measureFolders(mode); 280 int xStart = xEnd - Math.min(availableSpace, totalWidth); 281 282 // Second pass to draw folders. 283 for (Folder f : mFoldersSortedSet) { 284 final String folderString = f.name; 285 final int fgColor = f.getForegroundColor(mDefaultFgColor); 286 final int bgColor = f.getBackgroundColor(mDefaultBgColor); 287 int width = cellSize; 288 boolean labelTooLong = false; 289 width = (int) sFoldersPaint.measureText(folderString) + cellSize; 290 if (width % cellSize != 0) { 291 width += cellSize - (width % cellSize); 292 } 293 if (totalWidth > availableSpace && width > averageWidth) { 294 width = averageWidth; 295 labelTooLong = true; 296 } 297 298 // TODO (mindyp): how to we get this? 299 final boolean isMuted = false; 300 // labelValues.folderId == 301 // sGmail.getFolderMap(mAccount).getFolderIdIgnored(); 302 303 // Draw the box. 304 sFoldersPaint.setColor(bgColor); 305 sFoldersPaint.setStyle(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE); 306 canvas.drawRect(xStart, y, xStart + width, y + height - topPadding, 307 sFoldersPaint); 308 309 // Draw the text. 310 int padding = getPadding(width, (int) sFoldersPaint.measureText(folderString)); 311 if (labelTooLong) { 312 TextPaint shortPaint = new TextPaint(); 313 shortPaint.setColor(fgColor); 314 shortPaint.setTextSize(coordinates.foldersFontSize); 315 padding = cellSize / 2; 316 int rightBorder = xStart + width - padding; 317 Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y, 318 fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP); 319 shortPaint.setShader(shader); 320 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, 321 shortPaint); 322 } else { 323 sFoldersPaint.setColor(fgColor); 324 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, 325 sFoldersPaint); 326 } 327 328 availableSpace -= width; 329 xStart += width; 330 if (availableSpace <= 0 && mHasMoreFolders) { 331 canvas.drawBitmap(MORE_FOLDERS, xEnd, y + ascent, sFoldersPaint); 332 return; 333 } 334 } 335 } 336 } 337 338 /** 339 * Helpers function to align an element in the center of a space. 340 */ 341 private static int getPadding(int space, int length) { 342 return (space - length) / 2; 343 } 344 345 public ConversationItemView(Context context, String account) { 346 super(context); 347 setClickable(true); 348 setLongClickable(true); 349 mContext = context.getApplicationContext(); 350 final Resources res = mContext.getResources(); 351 mTabletDevice = Utils.useTabletUI(res); 352 mAccount = account; 353 354 if (CHECKMARK_OFF == null) { 355 // Initialize static bitmaps. 356 CHECKMARK_OFF = BitmapFactory.decodeResource(res, 357 R.drawable.btn_check_off_normal_holo_light); 358 CHECKMARK_ON = BitmapFactory.decodeResource(res, 359 R.drawable.btn_check_on_normal_holo_light); 360 STAR_OFF = BitmapFactory.decodeResource(res, 361 R.drawable.btn_star_off_normal_email_holo_light); 362 STAR_ON = BitmapFactory.decodeResource(res, 363 R.drawable.btn_star_on_normal_email_holo_light); 364 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 365 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 366 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 367 R.drawable.ic_email_caret_double_important_unread); 368 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 369 R.drawable.ic_email_caret_single_important_unread); 370 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 371 R.drawable.ic_email_caret_none_important_unread); 372 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 373 MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more); 374 STATE_REPLIED = 375 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light); 376 STATE_FORWARDED = 377 BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light); 378 STATE_REPLIED_AND_FORWARDED = 379 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light); 380 STATE_CALENDAR_INVITE = 381 BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light); 382 383 // Initialize colors. 384 sActivatedTextColor = res.getColor(android.R.color.white); 385 sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(sActivatedTextColor)); 386 sSendersTextColorRead = res.getColor(R.color.senders_text_color_read); 387 sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread); 388 sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext, 389 R.style.SubjectAppearanceUnreadStyle); 390 sSubjectTextReadSpan = new TextAppearanceSpan(mContext, 391 R.style.SubjectAppearanceReadStyle); 392 sSnippetTextUnreadSpan = 393 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread)); 394 sSnippetTextReadSpan = 395 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read)); 396 sDateTextColor = res.getColor(R.color.date_text_color); 397 sTouchSlop = res.getDimensionPixelSize(R.dimen.touch_slop); 398 sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen); 399 sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); 400 sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration); 401 // Initialize static color. 402 sSendersSplitToken = res.getString(R.string.senders_split_token); 403 sElidedPaddingToken = res.getString(R.string.elided_padding_token); 404 sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color); 405 sSendersTextViewTopPadding = res.getDimensionPixelSize 406 (R.dimen.senders_textview_top_padding); 407 sSendersTextViewHeight = res.getDimensionPixelSize 408 (R.dimen.senders_textview_height); 409 sScrollSlop = res.getInteger(R.integer.swipeScrollSlop); 410 sFoldersLeftPadding = res.getDimensionPixelSize(R.dimen.folders_left_padding); 411 sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); 412 } 413 414 mSubjectTextView = new EllipsizedMultilineTextView(mContext); 415 mSubjectTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 416 ViewGroup.LayoutParams.WRAP_CONTENT)); 417 mSubjectTextView.setMaxLines(2); 418 mSendersTextView = new TextView(mContext); 419 mSendersTextView.setMaxLines(1); 420 mSendersTextView.setEllipsize(TextUtils.TruncateAt.END); 421 mSendersTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 422 ViewGroup.LayoutParams.WRAP_CONTENT)); 423 424 mDateTextView = new TextView(mContext); 425 mDateTextView.setEllipsize(TextUtils.TruncateAt.END); 426 mDateTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 427 ViewGroup.LayoutParams.WRAP_CONTENT)); 428 429 mContactImagesHolder = new DividedImageCanvas(context, this); 430 } 431 432 public void bind(Cursor cursor, ControllableActivity activity, ConversationSelectionSet set, 433 Folder folder, boolean checkboxesDisabled, boolean swipeEnabled, 434 boolean priorityArrowEnabled, AnimatedAdapter adapter) { 435 bind(ConversationItemViewModel.forCursor(mAccount, cursor), activity, set, folder, 436 checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter); 437 } 438 439 public void bind(Conversation conversation, ControllableActivity activity, 440 ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled, 441 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 442 bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set, 443 folder, checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter); 444 } 445 446 private void bind(ConversationItemViewModel header, ControllableActivity activity, 447 ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled, 448 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 449 // If this was previously bound to a conversation, remove any contact 450 // photo manager requests. 451 if (mHeader != null) { 452 final ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds(); 453 if (divisionIds != null) { 454 for (int pos = 0; pos < divisionIds.size(); pos++) { 455 sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash( 456 mContactImagesHolder, pos, divisionIds.get(pos))); 457 } 458 } 459 } 460 mHeader = header; 461 mActivity = activity; 462 mSelectedConversationSet = set; 463 mDisplayedFolder = folder; 464 mCheckboxesEnabled = !checkboxesDisabled; 465 mConvListPhotosEnabled = MailPrefs.get(activity.getActivityContext()) 466 .areConvListPhotosEnabled(); 467 mStarEnabled = folder != null && !folder.isTrash(); 468 mSwipeEnabled = swipeEnabled; 469 mPriorityMarkersEnabled = priorityArrowEnabled; 470 mAdapter = adapter; 471 setContentDescription(); 472 requestLayout(); 473 } 474 475 /** 476 * Get the Conversation object associated with this view. 477 */ 478 public Conversation getConversation() { 479 return mHeader.conversation; 480 } 481 482 /** 483 * Sets the mode. Only used for testing. 484 */ 485 @VisibleForTesting 486 void setMode(int mode) { 487 mMode = mode; 488 mTesting = true; 489 } 490 491 private static void startTimer(String tag) { 492 if (sTimer != null) { 493 sTimer.start(tag); 494 } 495 } 496 497 private static void pauseTimer(String tag) { 498 if (sTimer != null) { 499 sTimer.pause(tag); 500 } 501 } 502 503 @Override 504 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 505 startTimer(PERF_TAG_LAYOUT); 506 507 super.onLayout(changed, left, top, right, bottom); 508 509 final int width = right - left; 510 final int currentMode = mActivity.getViewMode().getMode(); 511 if (width != mViewWidth || mPreviousMode != currentMode) { 512 mViewWidth = width; 513 mPreviousMode = currentMode; 514 if (!mTesting) { 515 mMode = ConversationItemViewCoordinates.getMode(mContext, mPreviousMode); 516 } 517 } 518 mHeader.viewWidth = mViewWidth; 519 Resources res = getResources(); 520 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 521 if (mHeader.standardScaledDimen != sStandardScaledDimen) { 522 // Large Text has been toggle on/off. Update the static dimens. 523 sStandardScaledDimen = mHeader.standardScaledDimen; 524 ConversationItemViewCoordinates.refreshConversationHeights(mContext); 525 } 526 mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode, 527 mHeader.standardScaledDimen, mConvListPhotosEnabled); 528 calculateTextsAndBitmaps(); 529 calculateCoordinates(); 530 531 // Subject. 532 createSubject(mHeader.unread); 533 534 if (!mHeader.isLayoutValid(mContext)) { 535 setContentDescription(); 536 } 537 mHeader.validate(mContext); 538 539 pauseTimer(PERF_TAG_LAYOUT); 540 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 541 sTimer.dumpResults(); 542 sTimer = new Timer(); 543 sLayoutCount = 0; 544 } 545 } 546 547 private void setContentDescription() { 548 if (mActivity.isAccessibilityEnabled()) { 549 mHeader.resetContentDescription(); 550 setContentDescription(mHeader.getContentDescription(mContext)); 551 } 552 } 553 554 @Override 555 public void setBackgroundResource(int resourceId) { 556 Drawable drawable = mBackgrounds.get(resourceId); 557 if (drawable == null) { 558 drawable = getResources().getDrawable(resourceId); 559 mBackgrounds.put(resourceId, drawable); 560 } 561 if (getBackground() != drawable) { 562 super.setBackgroundDrawable(drawable); 563 } 564 } 565 566 private void calculateTextsAndBitmaps() { 567 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 568 // Initialize folder displayer. 569 if (mCoordinates.showFolders) { 570 if (mHeader.folderDisplayer == null) { 571 mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext); 572 } else { 573 mHeader.folderDisplayer.reset(); 574 } 575 576 final int ignoreFolderType; 577 if (mDisplayedFolder.isInbox()) { 578 ignoreFolderType = FolderType.INBOX; 579 } else { 580 ignoreFolderType = -1; 581 } 582 583 mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, 584 mDisplayedFolder.uri, ignoreFolderType); 585 } 586 587 if (mSelectedConversationSet != null) { 588 mChecked = mSelectedConversationSet.contains(mHeader.conversation); 589 } 590 mHeader.checkboxVisible = mCheckboxesEnabled && !mConvListPhotosEnabled; 591 592 final boolean isUnread = mHeader.unread; 593 updateBackground(isUnread); 594 595 mHeader.sendersDisplayText = new SpannableStringBuilder(); 596 mHeader.styledSendersString = new SpannableStringBuilder(); 597 598 // Parse senders fragments. 599 if (mHeader.conversation.conversationInfo != null) { 600 Context context = getContext(); 601 mHeader.messageInfoString = SendersView 602 .createMessageInfo(context, mHeader.conversation, true); 603 int maxChars = ConversationItemViewCoordinates.getSendersLength(context, 604 ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()), 605 mHeader.conversation.hasAttachments); 606 mHeader.displayableSenderEmails = new ArrayList<String>(); 607 mHeader.displayableSenderNames = new ArrayList<String>(); 608 mHeader.styledSenders = new ArrayList<SpannableString>(); 609 SendersView.format(context, mHeader.conversation.conversationInfo, 610 mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders, 611 mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount, 612 true); 613 // If we have displayable sendres, load their thumbnails 614 loadSenderImages(); 615 } else { 616 SendersView.formatSenders(mHeader, getContext(), true); 617 } 618 619 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 620 mHeader.conversation.dateMs); 621 622 if (mHeader.isLayoutValid(mContext)) { 623 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 624 return; 625 } 626 startTimer(PERF_TAG_CALCULATE_FOLDERS); 627 628 629 pauseTimer(PERF_TAG_CALCULATE_FOLDERS); 630 631 // Paper clip icon. 632 mHeader.paperclip = null; 633 if (mHeader.conversation.hasAttachments) { 634 mHeader.paperclip = ATTACHMENT; 635 } 636 // Personal level. 637 mHeader.personalLevelBitmap = null; 638 if (mCoordinates.showPersonalLevel) { 639 final int personalLevel = mHeader.conversation.personalLevel; 640 final boolean isImportant = 641 mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT; 642 final boolean useImportantMarkers = isImportant && mPriorityMarkersEnabled; 643 644 if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { 645 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME 646 : ONLY_TO_ME; 647 } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { 648 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS 649 : TO_ME_AND_OTHERS; 650 } else if (useImportantMarkers) { 651 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; 652 } 653 } 654 655 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 656 657 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 658 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 659 } 660 661 private void loadSenderImages() { 662 if (mConvListPhotosEnabled && mHeader.displayableSenderEmails != null 663 && mHeader.displayableSenderEmails.size() > 0) { 664 mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth, 665 mCoordinates.contactImagesHeight); 666 mContactImagesHolder.setDivisionIds(mHeader.displayableSenderEmails); 667 int size = mHeader.displayableSenderEmails.size(); 668 String emailAddress; 669 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 670 emailAddress = mHeader.displayableSenderEmails.get(i); 671 sContactPhotoManager.loadThumbnail(DividedImageCanvas.generateHash( 672 mContactImagesHolder, i, emailAddress), 673 mContactImagesHolder, mHeader.displayableSenderNames.get(i), 674 emailAddress, DEFAULT_AVATAR_PROVIDER); 675 } 676 } 677 } 678 679 private void layoutSenders() { 680 TextView sendersTextView = mSendersTextView; 681 if (mHeader.styledSendersString != null) { 682 if (isActivated() && showActivatedText()) { 683 mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0, 684 mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 685 } else { 686 mHeader.styledSendersString.removeSpan(sActivatedTextSpan); 687 } 688 689 final int w = MeasureSpec.makeMeasureSpec(mSendersWidth, MeasureSpec.EXACTLY); 690 final int h = MeasureSpec.makeMeasureSpec(sSendersTextViewHeight, MeasureSpec.EXACTLY); 691 sendersTextView.measure(w, h); 692 sendersTextView.layout(0, 0, mSendersWidth, sSendersTextViewHeight); 693 sendersTextView.setText(mHeader.styledSendersString); 694 } 695 } 696 697 private void createSubject(final boolean isUnread) { 698 final String subject = filterTag(mHeader.conversation.subject); 699 final String snippet = mHeader.conversation.getSnippet(); 700 final SpannableStringBuilder displayedStringBuilder = new SpannableStringBuilder( 701 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet)); 702 703 // since spans affect text metrics, add spans to the string before measure/layout or fancy 704 // ellipsizing 705 final int subjectTextLength = (subject != null) ? subject.length() : 0; 706 if (!TextUtils.isEmpty(subject)) { 707 displayedStringBuilder.setSpan(TextAppearanceSpan.wrap( 708 isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength, 709 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 710 } 711 if (!TextUtils.isEmpty(snippet)) { 712 final int startOffset = subjectTextLength; 713 // Start after the end of the subject text; since the subject may be 714 // "" or null, this could start at the 0th character in the subjectText string 715 displayedStringBuilder.setSpan(ForegroundColorSpan.wrap( 716 isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset, 717 displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 718 } 719 if (isActivated() && showActivatedText()) { 720 displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(), 721 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 722 } 723 724 final int secondLineMaxWidth; 725 if (!ConversationItemViewCoordinates.isWideMode(mMode) && mCoordinates.showFolders 726 && mHeader.folderDisplayer != null && mHeader.folderDisplayer.hasVisibleFolders()) { 727 secondLineMaxWidth = mCoordinates.subjectWidth - Math.min( 728 ConversationItemViewCoordinates.getFoldersWidth(mContext, mMode), 729 mHeader.folderDisplayer.measureFolders(mMode)) - sFoldersLeftPadding; 730 } else { 731 secondLineMaxWidth = EllipsizedMultilineTextView.ALL_AVAILABLE; 732 } 733 final int subjectWidth = mCoordinates.subjectWidth; 734 final int subjectHeight = (int) (mSubjectTextView.getLineHeight() * 2 + 735 mSubjectTextView.getPaint().descent()); 736 mSubjectTextView.measure( 737 MeasureSpec.makeMeasureSpec(subjectWidth, MeasureSpec.EXACTLY), 738 MeasureSpec.makeMeasureSpec(subjectHeight, MeasureSpec.EXACTLY)); 739 mSubjectTextView.layout(0, 0, subjectWidth, subjectHeight); 740 741 if (displayedStringBuilder.length() == 0) { 742 // Work around for bug where empty displayedStringBuilder does not overwrite any 743 // existing text in mSubjectTextView 744 mSubjectTextView.setText(EMPTY_SNIPPET); 745 } else { 746 mSubjectTextView.setText(displayedStringBuilder, secondLineMaxWidth); 747 } 748 } 749 750 /** 751 * Returns the resource for the text color depending on whether the element is activated or not. 752 * @param defaultColor 753 * @return 754 */ 755 private int getFontColor(int defaultColor) { 756 final boolean isBackGroundBlue = isActivated() && showActivatedText(); 757 return isBackGroundBlue ? sActivatedTextColor : defaultColor; 758 } 759 760 private boolean showActivatedText() { 761 // For activated elements in tablet in conversation mode, we show an activated color, since 762 // the background is dark blue for activated versus gray for non-activated. 763 final boolean isListCollapsed = mContext.getResources().getBoolean(R.bool.list_collapsed); 764 return mTabletDevice && !isListCollapsed; 765 } 766 767 private boolean canFitFragment(int width, int line, int fixedWidth) { 768 if (line == mCoordinates.sendersLineCount) { 769 return width + fixedWidth <= mSendersWidth; 770 } else { 771 return width <= mSendersWidth; 772 } 773 } 774 775 private void calculateCoordinates() { 776 startTimer(PERF_TAG_CALCULATE_COORDINATES); 777 778 sPaint.setTextSize(mCoordinates.dateFontSize); 779 sPaint.setTypeface(Typeface.DEFAULT); 780 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText( 781 mHeader.dateText != null ? mHeader.dateText.toString() : ""); 782 783 mPaperclipX = mDateX - ATTACHMENT.getWidth(); 784 785 if (ConversationItemViewCoordinates.isWideMode(mMode)) { 786 // Folders are displayed above the date. 787 mFoldersXEnd = mCoordinates.foldersXEnd; 788 // In wide mode, the end of the senders should align with 789 // the start of the subject and is based on a max width. 790 mSendersWidth = mCoordinates.sendersWidth; 791 } else { 792 // In normal mode, the width is based on where the folders or date 793 // (or attachment icon) start. 794 mFoldersXEnd = mCoordinates.foldersXEnd; 795 int dateAttachmentStart = 0; 796 // Have this end near the paperclip or date, not the folders. 797 if (mHeader.paperclip != null) { 798 dateAttachmentStart = mPaperclipX; 799 } else { 800 dateAttachmentStart = mDateX; 801 } 802 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX; 803 } 804 805 // Second pass to layout each fragment. 806 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; 807 808 if (mHeader.styledSenders != null) { 809 ellipsizeStyledSenders(); 810 layoutSenders(); 811 } else { 812 // First pass to calculate width of each fragment. 813 int totalWidth = 0; 814 int fixedWidth = 0; 815 sPaint.setTextSize(mCoordinates.sendersFontSize); 816 sPaint.setTypeface(Typeface.DEFAULT); 817 for (SenderFragment senderFragment : mHeader.senderFragments) { 818 CharacterStyle style = senderFragment.style; 819 int start = senderFragment.start; 820 int end = senderFragment.end; 821 style.updateDrawState(sPaint); 822 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 823 boolean isFixed = senderFragment.isFixed; 824 if (isFixed) { 825 fixedWidth += senderFragment.width; 826 } 827 totalWidth += senderFragment.width; 828 } 829 830 if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) { 831 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; 832 } 833 if (mSendersWidth < 0) { 834 mSendersWidth = 0; 835 } 836 totalWidth = ellipsize(fixedWidth); 837 mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint, 838 mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 839 } 840 841 sPaint.setTextSize(mCoordinates.sendersFontSize); 842 sPaint.setTypeface(Typeface.DEFAULT); 843 if (mSendersWidth < 0) { 844 mSendersWidth = 0; 845 } 846 847 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 848 } 849 850 // The rules for displaying ellipsized senders are as follows: 851 // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown 852 // 2) If senders do not fit, ellipsize the last one that does fit, and stop 853 // appending new senders 854 private int ellipsizeStyledSenders() { 855 SpannableStringBuilder builder = new SpannableStringBuilder(); 856 float totalWidth = 0; 857 boolean ellipsize = false; 858 float width; 859 SpannableStringBuilder messageInfoString = mHeader.messageInfoString; 860 if (messageInfoString.length() > 0) { 861 CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(), 862 CharacterStyle.class); 863 // There is only 1 character style span; make sure we apply all the 864 // styles to the paint object before measuring. 865 if (spans.length > 0) { 866 spans[0].updateDrawState(sPaint); 867 } 868 // Paint the message info string to see if we lose space. 869 float messageInfoWidth = sPaint.measureText(messageInfoString.toString()); 870 totalWidth += messageInfoWidth; 871 } 872 SpannableString prevSender = null; 873 SpannableString ellipsizedText; 874 for (SpannableString sender : mHeader.styledSenders) { 875 // There may be null sender strings if there were dupes we had to remove. 876 if (sender == null) { 877 continue; 878 } 879 // No more width available, we'll only show fixed fragments. 880 if (ellipsize) { 881 break; 882 } 883 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 884 // There is only 1 character style span. 885 if (spans.length > 0) { 886 spans[0].updateDrawState(sPaint); 887 } 888 // If there are already senders present in this string, we need to 889 // make sure we prepend the dividing token 890 if (SendersView.sElidedString.equals(sender.toString())) { 891 prevSender = sender; 892 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 893 } else if (builder.length() > 0 894 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 895 .toString()))) { 896 prevSender = sender; 897 sender = copyStyles(spans, sSendersSplitToken + sender); 898 } else { 899 prevSender = sender; 900 } 901 if (spans.length > 0) { 902 spans[0].updateDrawState(sPaint); 903 } 904 // Measure the width of the current sender and make sure we have space 905 width = (int) sPaint.measureText(sender.toString()); 906 if (width + totalWidth > mSendersWidth) { 907 // The text is too long, new line won't help. We have to 908 // ellipsize text. 909 ellipsize = true; 910 width = mSendersWidth - totalWidth; // ellipsis width? 911 ellipsizedText = copyStyles(spans, 912 TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END)); 913 width = (int) sPaint.measureText(ellipsizedText.toString()); 914 } else { 915 ellipsizedText = null; 916 } 917 totalWidth += width; 918 919 final CharSequence fragmentDisplayText; 920 if (ellipsizedText != null) { 921 fragmentDisplayText = ellipsizedText; 922 } else { 923 fragmentDisplayText = sender; 924 } 925 builder.append(fragmentDisplayText); 926 } 927 mHeader.styledMessageInfoStringOffset = builder.length(); 928 if (messageInfoString != null) { 929 builder.append(messageInfoString); 930 } 931 mHeader.styledSendersString = builder; 932 return (int)totalWidth; 933 } 934 935 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 936 SpannableString s = new SpannableString(newText); 937 if (spans != null && spans.length > 0) { 938 s.setSpan(spans[0], 0, s.length(), 0); 939 } 940 return s; 941 } 942 943 private int ellipsize(int fixedWidth) { 944 int totalWidth = 0; 945 int currentLine = 1; 946 boolean ellipsize = false; 947 for (SenderFragment senderFragment : mHeader.senderFragments) { 948 CharacterStyle style = senderFragment.style; 949 int start = senderFragment.start; 950 int end = senderFragment.end; 951 int width = senderFragment.width; 952 boolean isFixed = senderFragment.isFixed; 953 style.updateDrawState(sPaint); 954 955 // No more width available, we'll only show fixed fragments. 956 if (ellipsize && !isFixed) { 957 senderFragment.shouldDisplay = false; 958 continue; 959 } 960 961 // New line and ellipsize text if needed. 962 senderFragment.ellipsizedText = null; 963 if (isFixed) { 964 fixedWidth -= width; 965 } 966 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 967 // The text is too long, new line won't help. We have to 968 // ellipsize text. 969 if (totalWidth == 0) { 970 ellipsize = true; 971 } else { 972 // New line. 973 if (currentLine < mCoordinates.sendersLineCount) { 974 currentLine++; 975 totalWidth = 0; 976 // The text is still too long, we have to ellipsize 977 // text. 978 if (totalWidth + width > mSendersWidth) { 979 ellipsize = true; 980 } 981 } else { 982 ellipsize = true; 983 } 984 } 985 986 if (ellipsize) { 987 width = mSendersWidth - totalWidth; 988 // No more new line, we have to reserve width for fixed 989 // fragments. 990 if (currentLine == mCoordinates.sendersLineCount) { 991 width -= fixedWidth; 992 } 993 senderFragment.ellipsizedText = TextUtils.ellipsize( 994 mHeader.sendersText.substring(start, end), sPaint, width, 995 TruncateAt.END).toString(); 996 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 997 } 998 } 999 senderFragment.shouldDisplay = true; 1000 totalWidth += width; 1001 1002 final CharSequence fragmentDisplayText; 1003 if (senderFragment.ellipsizedText != null) { 1004 fragmentDisplayText = senderFragment.ellipsizedText; 1005 } else { 1006 fragmentDisplayText = mHeader.sendersText.substring(start, end); 1007 } 1008 final int spanStart = mHeader.sendersDisplayText.length(); 1009 mHeader.sendersDisplayText.append(fragmentDisplayText); 1010 mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart, 1011 mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1012 } 1013 return totalWidth; 1014 } 1015 1016 /** 1017 * If the subject contains the tag of a mailing-list (text surrounded with 1018 * []), return the subject with that tag ellipsized, e.g. 1019 * "[android-gmail-team] Hello" -> "[andr...] Hello" 1020 */ 1021 private String filterTag(String subject) { 1022 String result = subject; 1023 String formatString = getContext().getResources().getString(R.string.filtered_tag); 1024 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 1025 int end = subject.indexOf(']'); 1026 if (end > 0) { 1027 String tag = subject.substring(1, end); 1028 result = String.format(formatString, Utils.ellipsize(tag, 7), 1029 subject.substring(end + 1)); 1030 } 1031 } 1032 return result; 1033 } 1034 1035 @Override 1036 protected void onDraw(Canvas canvas) { 1037 // Check mark. 1038 if (mConvListPhotosEnabled) { 1039 canvas.save(); 1040 drawContactImages(canvas); 1041 canvas.restore(); 1042 } else if (mHeader.checkboxVisible) { 1043 Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF; 1044 canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint); 1045 } 1046 1047 // Personal Level. 1048 if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) { 1049 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX, 1050 mCoordinates.personalLevelY, sPaint); 1051 } 1052 1053 // Senders. 1054 boolean isUnread = mHeader.unread; 1055 // Old style senders; apply text colors/ sizes/ styling. 1056 canvas.save(); 1057 if (mHeader.sendersDisplayLayout != null) { 1058 sPaint.setTextSize(mCoordinates.sendersFontSize); 1059 sPaint.setTypeface(SendersView.getTypeface(isUnread)); 1060 sPaint.setColor(getFontColor(isUnread ? 1061 sSendersTextColorUnread : sSendersTextColorRead)); 1062 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY 1063 + mHeader.sendersDisplayLayout.getTopPadding()); 1064 mHeader.sendersDisplayLayout.draw(canvas); 1065 } else { 1066 drawSenders(canvas); 1067 } 1068 canvas.restore(); 1069 1070 1071 // Subject. 1072 sPaint.setTypeface(Typeface.DEFAULT); 1073 canvas.save(); 1074 drawSubject(canvas); 1075 canvas.restore(); 1076 1077 // Folders. 1078 if (mCoordinates.showFolders && mHeader.folderDisplayer != null) { 1079 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode); 1080 } 1081 1082 // If this folder has a color (combined view/Email), show it here 1083 if (mHeader.conversation.color != 0) { 1084 sFoldersPaint.setColor(mHeader.conversation.color); 1085 sFoldersPaint.setStyle(Paint.Style.FILL); 1086 int width = ConversationItemViewCoordinates.getColorBlockWidth(mContext); 1087 int height = ConversationItemViewCoordinates.getColorBlockHeight(mContext); 1088 canvas.drawRect(mCoordinates.dateXEnd - width, 0, mCoordinates.dateXEnd, 1089 height, sFoldersPaint); 1090 } 1091 1092 // Draw the reply state. Draw nothing if neither replied nor forwarded. 1093 if (mCoordinates.showReplyState) { 1094 if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) { 1095 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX, 1096 mCoordinates.replyStateY, null); 1097 } else if (mHeader.hasBeenRepliedTo) { 1098 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX, 1099 mCoordinates.replyStateY, null); 1100 } else if (mHeader.hasBeenForwarded) { 1101 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX, 1102 mCoordinates.replyStateY, null); 1103 } else if (mHeader.isInvite) { 1104 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX, 1105 mCoordinates.replyStateY, null); 1106 } 1107 } 1108 1109 // Date. 1110 sPaint.setTextSize(mCoordinates.dateFontSize); 1111 sPaint.setTypeface(Typeface.DEFAULT); 1112 sPaint.setColor(sDateTextColor); 1113 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent, 1114 sPaint); 1115 1116 // Paper clip icon. 1117 if (mHeader.paperclip != null) { 1118 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 1119 } 1120 1121 if (mHeader.faded) { 1122 int fadedColor = -1; 1123 if (sFadedActivatedColor == -1) { 1124 sFadedActivatedColor = mContext.getResources().getColor( 1125 R.color.faded_activated_conversation_header); 1126 } 1127 fadedColor = sFadedActivatedColor; 1128 int restoreState = canvas.save(); 1129 Rect bounds = canvas.getClipBounds(); 1130 canvas.clipRect(bounds.left, bounds.top, bounds.right 1131 - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width), 1132 bounds.bottom); 1133 canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor), 1134 Color.green(fadedColor), Color.blue(fadedColor)); 1135 canvas.restoreToCount(restoreState); 1136 } 1137 1138 if (mStarEnabled) { 1139 // Star. 1140 canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint); 1141 } 1142 } 1143 1144 private void drawContactImages(Canvas canvas) { 1145 canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); 1146 mContactImagesHolder.draw(canvas); 1147 } 1148 1149 private void drawSubject(Canvas canvas) { 1150 canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY + sSendersTextViewTopPadding); 1151 mSubjectTextView.draw(canvas); 1152 } 1153 1154 private void drawSenders(Canvas canvas) { 1155 int left; 1156 if (mConvListPhotosEnabled && mCoordinates.inlinePersonalLevel) { 1157 if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) { 1158 left = mCoordinates.sendersX; 1159 } else { 1160 left = mCoordinates.personalLevelX; 1161 } 1162 } else { 1163 left = mCoordinates.sendersX; 1164 } 1165 canvas.translate(left, mCoordinates.sendersY + sSendersTextViewTopPadding); 1166 mSendersTextView.draw(canvas); 1167 } 1168 1169 private Bitmap getStarBitmap() { 1170 return mHeader.conversation.starred ? STAR_ON : STAR_OFF; 1171 } 1172 1173 private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 1174 canvas.drawText(s, 0, s.length(), x, y, paint); 1175 } 1176 1177 /** 1178 * Set the background for this item based on: 1179 * 1. Read / Unread (unread messages have a lighter background) 1180 * 2. Tablet / Phone 1181 * 3. Checkbox checked / Unchecked (controls CAB color for item) 1182 * 4. Activated / Not activated (controls the blue highlight on tablet) 1183 * @param isUnread 1184 */ 1185 private void updateBackground(boolean isUnread) { 1186 if (mBackgroundOverride != -1) { 1187 // If the item is animating, we use a color to avoid shrinking a 9-patch 1188 // and getting weird artifacts from the overlap. 1189 setBackgroundColor(mBackgroundOverride); 1190 return; 1191 } 1192 final boolean isListOnTablet = mTabletDevice && mActivity.getViewMode().isListMode(); 1193 final int background; 1194 if (isUnread) { 1195 if (isListOnTablet) { 1196 if (mChecked) { 1197 background = R.drawable.list_conversation_wide_unread_selected_holo; 1198 } else { 1199 background = R.drawable.conversation_wide_unread_selector; 1200 } 1201 } else { 1202 if (mChecked) { 1203 background = getCheckedActivatedBackground(); 1204 } else { 1205 background = R.drawable.conversation_unread_selector; 1206 } 1207 } 1208 } else { 1209 if (isListOnTablet) { 1210 if (mChecked) { 1211 background = R.drawable.list_conversation_wide_read_selected_holo; 1212 } else { 1213 background = R.drawable.conversation_wide_read_selector; 1214 } 1215 } else { 1216 if (mChecked) { 1217 background = getCheckedActivatedBackground(); 1218 } else { 1219 background = R.drawable.conversation_read_selector; 1220 } 1221 } 1222 } 1223 setBackgroundResource(background); 1224 } 1225 1226 private final int getCheckedActivatedBackground() { 1227 if (isActivated() && mTabletDevice) { 1228 return R.drawable.list_arrow_selected_holo; 1229 } else { 1230 return R.drawable.list_selected_holo; 1231 } 1232 } 1233 1234 /** 1235 * Toggle the check mark on this view and update the conversation or begin 1236 * drag, if drag is enabled. 1237 */ 1238 @Override 1239 public void toggleCheckMarkOrBeginDrag() { 1240 ViewMode mode = mActivity.getViewMode(); 1241 if (!mTabletDevice || !mode.isListMode()) { 1242 toggleCheckMark(); 1243 } else { 1244 beginDragMode(); 1245 } 1246 } 1247 1248 private void toggleCheckMark() { 1249 if (mHeader != null && mHeader.conversation != null) { 1250 mChecked = !mChecked; 1251 Conversation conv = mHeader.conversation; 1252 // Set the list position of this item in the conversation 1253 SwipeableListView listView = getListView(); 1254 conv.position = mChecked && listView != null ? listView.getPositionForView(this) 1255 : Conversation.NO_POSITION; 1256 if (mSelectedConversationSet != null) { 1257 mSelectedConversationSet.toggle(this, conv); 1258 } 1259 if (mSelectedConversationSet.isEmpty()) { 1260 listView.commitDestructiveActions(true); 1261 } 1262 // We update the background after the checked state has changed 1263 // now that we have a selected background asset. Setting the background 1264 // usually waits for a layout pass, but we don't need a full layout, 1265 // just an update to the background. 1266 requestLayout(); 1267 } 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 @Override 1556 public View getSwipeableView() { 1557 return this; 1558 } 1559 1560 /** 1561 * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag. 1562 */ 1563 private void beginDragMode() { 1564 if (mLastTouchX < 0 || mLastTouchY < 0) { 1565 return; 1566 } 1567 // If this is already checked, don't bother unchecking it! 1568 if (!mChecked) { 1569 toggleCheckMark(); 1570 } 1571 1572 // Clip data has form: [conversations_uri, conversationId1, 1573 // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] 1574 final int count = mSelectedConversationSet.size(); 1575 String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count); 1576 1577 final ClipData data = ClipData.newUri(mContext.getContentResolver(), description, 1578 Conversation.MOVE_CONVERSATIONS_URI); 1579 for (Conversation conversation : mSelectedConversationSet.values()) { 1580 data.addItem(new Item(String.valueOf(conversation.position))); 1581 } 1582 // Protect against non-existent views: only happens for monkeys 1583 final int width = this.getWidth(); 1584 final int height = this.getHeight(); 1585 final boolean isDimensionNegative = (width < 0) || (height < 0); 1586 if (isDimensionNegative) { 1587 LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: " 1588 + "width=%d, height=%d", width, height); 1589 return; 1590 } 1591 mActivity.startDragMode(); 1592 // Start drag mode 1593 startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0); 1594 } 1595 1596 /** 1597 * Handles the drag event. 1598 * 1599 * @param event the drag event to be handled 1600 */ 1601 @Override 1602 public boolean onDragEvent(DragEvent event) { 1603 switch (event.getAction()) { 1604 case DragEvent.ACTION_DRAG_ENDED: 1605 mActivity.stopDragMode(); 1606 return true; 1607 } 1608 return false; 1609 } 1610 1611 private class ShadowBuilder extends DragShadowBuilder { 1612 private final Drawable mBackground; 1613 1614 private final View mView; 1615 private final String mDragDesc; 1616 private final int mTouchX; 1617 private final int mTouchY; 1618 private int mDragDescX; 1619 private int mDragDescY; 1620 1621 public ShadowBuilder(View view, int count, int touchX, int touchY) { 1622 super(view); 1623 mView = view; 1624 mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo); 1625 mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count); 1626 mTouchX = touchX; 1627 mTouchY = touchY; 1628 } 1629 1630 @Override 1631 public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 1632 int width = mView.getWidth(); 1633 int height = mView.getHeight(); 1634 mDragDescX = mCoordinates.sendersX; 1635 mDragDescY = getPadding(height, mCoordinates.subjectFontSize) 1636 - mCoordinates.subjectAscent; 1637 shadowSize.set(width, height); 1638 shadowTouchPoint.set(mTouchX, mTouchY); 1639 } 1640 1641 @Override 1642 public void onDrawShadow(Canvas canvas) { 1643 mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight()); 1644 mBackground.draw(canvas); 1645 sPaint.setTextSize(mCoordinates.subjectFontSize); 1646 canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint); 1647 } 1648 } 1649 1650 @Override 1651 public float getMinAllowScrollDistance() { 1652 return sScrollSlop; 1653 } 1654} 1655