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