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