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