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