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