ConversationItemView.java revision 74db99555be46ccf44691805c359839268326d2e
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.AnimatorListenerAdapter; 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.graphics.Bitmap; 29import android.graphics.BitmapFactory; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.LinearGradient; 33import android.graphics.Matrix; 34import android.graphics.Paint; 35import android.graphics.Point; 36import android.graphics.Rect; 37import android.graphics.Shader; 38import android.graphics.Typeface; 39import android.graphics.drawable.Drawable; 40import android.text.Layout.Alignment; 41import android.text.Spannable; 42import android.text.SpannableString; 43import android.text.SpannableStringBuilder; 44import android.text.StaticLayout; 45import android.text.TextPaint; 46import android.text.TextUtils; 47import android.text.TextUtils.TruncateAt; 48import android.text.format.DateUtils; 49import android.text.style.CharacterStyle; 50import android.text.style.ForegroundColorSpan; 51import android.text.style.TextAppearanceSpan; 52import android.text.util.Rfc822Token; 53import android.text.util.Rfc822Tokenizer; 54import android.util.SparseArray; 55import android.util.TypedValue; 56import android.view.DragEvent; 57import android.view.MotionEvent; 58import android.view.View; 59import android.view.ViewGroup; 60import android.view.ViewParent; 61import android.view.animation.DecelerateInterpolator; 62import android.view.animation.LinearInterpolator; 63import android.widget.AbsListView; 64import android.widget.AbsListView.OnScrollListener; 65import android.widget.TextView; 66 67import com.android.mail.R; 68import com.android.mail.R.drawable; 69import com.android.mail.R.integer; 70import com.android.mail.R.string; 71import com.android.mail.bitmap.AttachmentDrawable; 72import com.android.mail.bitmap.AttachmentGridDrawable; 73import com.android.mail.bitmap.ImageAttachmentRequest; 74import com.android.mail.browse.ConversationItemViewModel.SenderFragment; 75import com.android.mail.perf.Timer; 76import com.android.mail.photomanager.ContactPhotoManager; 77import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier; 78import com.android.mail.photomanager.PhotoManager.PhotoIdentifier; 79import com.android.mail.providers.Address; 80import com.android.mail.providers.Attachment; 81import com.android.mail.providers.Conversation; 82import com.android.mail.providers.Folder; 83import com.android.mail.providers.UIProvider; 84import com.android.mail.providers.UIProvider.AttachmentRendition; 85import com.android.mail.providers.UIProvider.ConversationColumns; 86import com.android.mail.providers.UIProvider.ConversationListIcon; 87import com.android.mail.providers.UIProvider.FolderType; 88import com.android.mail.ui.AnimatedAdapter; 89import com.android.mail.ui.AnimatedAdapter.ConversationListListener; 90import com.android.mail.ui.ControllableActivity; 91import com.android.mail.ui.ConversationSelectionSet; 92import com.android.mail.ui.DividedImageCanvas; 93import com.android.mail.ui.DividedImageCanvas.InvalidateCallback; 94import com.android.mail.ui.FolderDisplayer; 95import com.android.mail.ui.SwipeableItemView; 96import com.android.mail.ui.SwipeableListView; 97import com.android.mail.ui.ViewMode; 98import com.android.mail.utils.FolderUri; 99import com.android.mail.utils.HardwareLayerEnabler; 100import com.android.mail.utils.LogTag; 101import com.android.mail.utils.LogUtils; 102import com.android.mail.utils.Utils; 103import com.google.common.annotations.VisibleForTesting; 104import com.google.common.collect.Lists; 105 106import java.util.ArrayList; 107import java.util.List; 108 109public class ConversationItemView extends View 110 implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener { 111 112 // Timer. 113 private static int sLayoutCount = 0; 114 private static Timer sTimer; // Create the sTimer here if you need to do 115 // perf analysis. 116 private static final int PERF_LAYOUT_ITERATIONS = 50; 117 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 118 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 119 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 120 private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; 121 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 122 private static final String LOG_TAG = LogTag.getLogTag(); 123 124 // Static bitmaps. 125 private static Bitmap STAR_OFF; 126 private static Bitmap STAR_ON; 127 private static Bitmap CHECK; 128 private static Bitmap ATTACHMENT; 129 private static Bitmap ONLY_TO_ME; 130 private static Bitmap TO_ME_AND_OTHERS; 131 private static Bitmap IMPORTANT_ONLY_TO_ME; 132 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 133 private static Bitmap IMPORTANT_TO_OTHERS; 134 private static Bitmap STATE_REPLIED; 135 private static Bitmap STATE_FORWARDED; 136 private static Bitmap STATE_REPLIED_AND_FORWARDED; 137 private static Bitmap STATE_CALENDAR_INVITE; 138 private static Bitmap VISIBLE_CONVERSATION_CARET; 139 private static Drawable RIGHT_EDGE_TABLET; 140 private static Drawable PLACEHOLDER; 141 private static Drawable PROGRESS_BAR; 142 143 private static String sSendersSplitToken; 144 private static String sElidedPaddingToken; 145 private static String sOverflowCountFormat; 146 147 // Static colors. 148 private static int sSendersTextColorRead; 149 private static int sSendersTextColorUnread; 150 private static int sDateTextColor; 151 private static int sStarTouchSlop; 152 private static int sSenderImageTouchSlop; 153 private static int sShrinkAnimationDuration; 154 private static int sSlideAnimationDuration; 155 private static int sOverflowCountMax; 156 private static int sCabAnimationDuration; 157 158 // Static paints. 159 private static final TextPaint sPaint = new TextPaint(); 160 private static final TextPaint sFoldersPaint = new TextPaint(); 161 private static final Paint sCheckBackgroundPaint = new Paint(); 162 163 // Backgrounds for different states. 164 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 165 166 // Dimensions and coordinates. 167 private int mViewWidth = -1; 168 /** The view mode at which we calculated mViewWidth previously. */ 169 private int mPreviousMode; 170 171 private int mDateX; 172 private int mPaperclipX; 173 private int mSendersWidth; 174 175 /** Whether we are on a tablet device or not */ 176 private final boolean mTabletDevice; 177 /** Whether we are on an expansive tablet */ 178 private final boolean mIsExpansiveTablet; 179 /** When in conversation mode, true if the list is hidden */ 180 private final boolean mListCollapsible; 181 182 @VisibleForTesting 183 ConversationItemViewCoordinates mCoordinates; 184 185 private ConversationItemViewCoordinates.Config mConfig; 186 187 private final Context mContext; 188 189 public ConversationItemViewModel mHeader; 190 private boolean mDownEvent; 191 private boolean mSelected = false; 192 private ConversationSelectionSet mSelectedConversationSet; 193 private Folder mDisplayedFolder; 194 private boolean mStarEnabled; 195 private boolean mSwipeEnabled; 196 private int mLastTouchX; 197 private int mLastTouchY; 198 private AnimatedAdapter mAdapter; 199 private float mAnimatedHeightFraction = 1.0f; 200 private final String mAccount; 201 private ControllableActivity mActivity; 202 private ConversationListListener mConversationListListener; 203 private final TextView mSubjectTextView; 204 private final TextView mSendersTextView; 205 private int mGadgetMode; 206 private final DividedImageCanvas mContactImagesHolder; 207 private static ContactPhotoManager sContactPhotoManager; 208 209 private static int sFoldersLeftPadding; 210 private static TextAppearanceSpan sSubjectTextUnreadSpan; 211 private static TextAppearanceSpan sSubjectTextReadSpan; 212 private static ForegroundColorSpan sSnippetTextUnreadSpan; 213 private static ForegroundColorSpan sSnippetTextReadSpan; 214 private static int sScrollSlop; 215 private static CharacterStyle sActivatedTextSpan; 216 217 private final AttachmentGridDrawable mAttachmentsView; 218 219 private static final boolean CONVLIST_ATTACHMENT_PREVIEWS_ENABLED = true; 220 221 private final Matrix mPhotoFlipMatrix = new Matrix(); 222 private final Matrix mCheckMatrix = new Matrix(); 223 224 private final CabAnimator mPhotoFlipAnimator; 225 226 /** 227 * The conversation id, if this conversation was selected the last time we were in a selection 228 * mode. This is reset after any animations complete upon exiting the selection mode. 229 */ 230 private long mLastSelectedId = -1; 231 232 static { 233 sPaint.setAntiAlias(true); 234 sFoldersPaint.setAntiAlias(true); 235 236 sCheckBackgroundPaint.setColor(Color.GRAY); 237 } 238 239 public static void setScrollStateChanged(final int scrollState) { 240 if (sContactPhotoManager == null) { 241 return; 242 } 243 final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE; 244 final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING; 245 246 // TODO: add support for yielding attachment bitmap allocation if/when needed 247 248 if (flinging) { 249 sContactPhotoManager.pause(); 250 } else { 251 sContactPhotoManager.resume(); 252 } 253 } 254 255 /** 256 * Handles displaying folders in a conversation header view. 257 */ 258 static class ConversationItemFolderDisplayer extends FolderDisplayer { 259 260 private int mFoldersCount; 261 262 public ConversationItemFolderDisplayer(Context context) { 263 super(context); 264 } 265 266 @Override 267 public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri, 268 final int ignoreFolderType) { 269 super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType); 270 mFoldersCount = mFoldersSortedSet.size(); 271 } 272 273 @Override 274 public void reset() { 275 super.reset(); 276 mFoldersCount = 0; 277 } 278 279 public boolean hasVisibleFolders() { 280 return mFoldersCount > 0; 281 } 282 283 private int measureFolders(int availableSpace, int cellSize) { 284 int totalWidth = 0; 285 boolean firstTime = true; 286 for (Folder f : mFoldersSortedSet) { 287 final String folderString = f.name; 288 int width = (int) sFoldersPaint.measureText(folderString) + cellSize; 289 if (firstTime) { 290 firstTime = false; 291 } else { 292 width += sFoldersLeftPadding; 293 } 294 totalWidth += width; 295 if (totalWidth > availableSpace) { 296 break; 297 } 298 } 299 300 return totalWidth; 301 } 302 303 public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) { 304 if (mFoldersCount == 0) { 305 return; 306 } 307 final int xMinStart = coordinates.foldersX; 308 final int xEnd = coordinates.foldersXEnd; 309 final int y = coordinates.foldersY; 310 final int height = coordinates.foldersHeight; 311 int textBottomPadding = coordinates.foldersTextBottomPadding; 312 313 sFoldersPaint.setTextSize(coordinates.foldersFontSize); 314 sFoldersPaint.setTypeface(coordinates.foldersTypeface); 315 316 // Initialize space and cell size based on the current mode. 317 int availableSpace = xEnd - xMinStart; 318 int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth(); 319 int foldersCount = Math.min(mFoldersCount, maxFoldersCount); 320 int averageWidth = availableSpace / foldersCount; 321 int cellSize = coordinates.getFolderCellWidth(); 322 323 // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that. 324 // Extra credit: maybe cache results across items as long as font size doesn't change. 325 326 final int totalWidth = measureFolders(availableSpace, cellSize); 327 int xStart = xEnd - Math.min(availableSpace, totalWidth); 328 final boolean overflow = totalWidth > availableSpace; 329 330 // Second pass to draw folders. 331 int i = 0; 332 for (Folder f : mFoldersSortedSet) { 333 if (availableSpace <= 0) { 334 break; 335 } 336 final String folderString = f.name; 337 final int fgColor = f.getForegroundColor(mDefaultFgColor); 338 final int bgColor = f.getBackgroundColor(mDefaultBgColor); 339 boolean labelTooLong = false; 340 final int textW = (int) sFoldersPaint.measureText(folderString); 341 int width = textW + cellSize + sFoldersLeftPadding; 342 343 if (overflow && width > averageWidth) { 344 if (i < foldersCount - 1) { 345 width = averageWidth; 346 } else { 347 // allow the last label to take all remaining space 348 // (and don't let it make room for padding) 349 width = availableSpace + sFoldersLeftPadding; 350 } 351 labelTooLong = true; 352 } 353 354 // TODO (mindyp): how to we get this? 355 final boolean isMuted = false; 356 // labelValues.folderId == 357 // sGmail.getFolderMap(mAccount).getFolderIdIgnored(); 358 359 // Draw the box. 360 sFoldersPaint.setColor(bgColor); 361 sFoldersPaint.setStyle(Paint.Style.FILL); 362 canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding, 363 y + height, sFoldersPaint); 364 365 // Draw the text. 366 final int padding = cellSize / 2; 367 sFoldersPaint.setColor(fgColor); 368 sFoldersPaint.setStyle(Paint.Style.FILL); 369 if (labelTooLong) { 370 final int rightBorder = xStart + width - sFoldersLeftPadding - padding; 371 final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, 372 y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP); 373 sFoldersPaint.setShader(shader); 374 } 375 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, 376 sFoldersPaint); 377 if (labelTooLong) { 378 sFoldersPaint.setShader(null); 379 } 380 381 availableSpace -= width; 382 xStart += width; 383 i++; 384 } 385 } 386 } 387 388 public ConversationItemView(Context context, String account) { 389 super(context); 390 Utils.traceBeginSection("CIVC constructor"); 391 setClickable(true); 392 setLongClickable(true); 393 mContext = context.getApplicationContext(); 394 final Resources res = mContext.getResources(); 395 mTabletDevice = Utils.useTabletUI(res); 396 mIsExpansiveTablet = 397 mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false; 398 mListCollapsible = res.getBoolean(R.bool.list_collapsible); 399 mAccount = account; 400 401 if (STAR_OFF == null) { 402 // Initialize static bitmaps. 403 STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off); 404 STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on); 405 CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check); 406 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 407 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 408 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 409 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 410 R.drawable.ic_email_caret_double_important_unread); 411 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 412 R.drawable.ic_email_caret_single_important_unread); 413 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 414 R.drawable.ic_email_caret_none_important_unread); 415 STATE_REPLIED = 416 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light); 417 STATE_FORWARDED = 418 BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light); 419 STATE_REPLIED_AND_FORWARDED = 420 BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light); 421 STATE_CALENDAR_INVITE = 422 BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light); 423 VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, 424 R.drawable.ic_carrot_holo); 425 RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet); 426 PLACEHOLDER = res.getDrawable(drawable.ic_attachment_load); 427 PROGRESS_BAR = res.getDrawable(drawable.progress_holo); 428 429 // Initialize colors. 430 sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan( 431 res.getColor(R.color.senders_text_color_read))); 432 sSendersTextColorRead = res.getColor(R.color.senders_text_color_read); 433 sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread); 434 sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext, 435 R.style.SubjectAppearanceUnreadStyle); 436 sSubjectTextReadSpan = new TextAppearanceSpan(mContext, 437 R.style.SubjectAppearanceReadStyle); 438 sSnippetTextUnreadSpan = 439 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread)); 440 sSnippetTextReadSpan = 441 new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read)); 442 sDateTextColor = res.getColor(R.color.date_text_color); 443 sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop); 444 sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop); 445 sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); 446 sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration); 447 // Initialize static color. 448 sSendersSplitToken = res.getString(R.string.senders_split_token); 449 sElidedPaddingToken = res.getString(R.string.elided_padding_token); 450 sOverflowCountFormat = res.getString(string.ap_overflow_format); 451 sScrollSlop = res.getInteger(R.integer.swipeScrollSlop); 452 sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding); 453 sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); 454 sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count); 455 sCabAnimationDuration = 456 res.getInteger(R.integer.conv_item_view_cab_anim_duration); 457 } 458 459 mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2, 460 sCabAnimationDuration) { 461 @Override 462 public void invalidateArea() { 463 final int left = mCoordinates.contactImagesX; 464 final int right = left + mContactImagesHolder.getWidth(); 465 final int top = mCoordinates.contactImagesY; 466 final int bottom = top + mContactImagesHolder.getHeight(); 467 invalidate(left, top, right, bottom); 468 } 469 }; 470 471 mSendersTextView = new TextView(mContext); 472 mSendersTextView.setIncludeFontPadding(false); 473 474 mSubjectTextView = new TextView(mContext); 475 mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END); 476 mSubjectTextView.setIncludeFontPadding(false); 477 478 mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() { 479 @Override 480 public void invalidate() { 481 if (mCoordinates == null) { 482 return; 483 } 484 ConversationItemView.this.invalidate(mCoordinates.contactImagesX, 485 mCoordinates.contactImagesY, 486 mCoordinates.contactImagesX + mCoordinates.contactImagesWidth, 487 mCoordinates.contactImagesY + mCoordinates.contactImagesHeight); 488 } 489 }); 490 491 mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR); 492 mAttachmentsView.setCallback(this); 493 494 Utils.traceEndSection(); 495 } 496 497 public void bind(Conversation conversation, ControllableActivity activity, 498 final ConversationListListener conversationListListener, 499 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 500 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 501 Utils.traceBeginSection("CIVC.bind"); 502 bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, 503 conversationListListener, set, folder, checkboxOrSenderImage, swipeEnabled, 504 priorityArrowEnabled, adapter); 505 Utils.traceEndSection(); 506 } 507 508 private void bind(ConversationItemViewModel header, ControllableActivity activity, 509 final ConversationListListener conversationListListener, 510 ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, 511 boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { 512 if (mHeader != null) { 513 // If this was previously bound to a different conversation, remove any contact photo 514 // manager requests. 515 if (header.conversation.id != mHeader.conversation.id || 516 (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames 517 .equals(header.displayableSenderNames))) { 518 ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds(); 519 if (divisionIds != null) { 520 mContactImagesHolder.reset(); 521 for (int pos = 0; pos < divisionIds.size(); pos++) { 522 sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash( 523 mContactImagesHolder, pos, divisionIds.get(pos))); 524 } 525 } 526 } 527 528 // If this was previously bound to a different conversation, 529 // remove any attachment preview manager requests. 530 if (header.conversation.id != mHeader.conversation.id 531 || header.conversation.attachmentPreviewsCount 532 != mHeader.conversation.attachmentPreviewsCount 533 || !header.conversation.getAttachmentPreviewUris() 534 .equals(mHeader.conversation.getAttachmentPreviewUris())) { 535 536 // unbind the attachments view (releasing bitmap references) 537 // (this also cancels all async tasks) 538 for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) { 539 mAttachmentsView.getOrCreateDrawable(i).setImage(null); 540 } 541 // reset the grid, as the newly bound item may have a different attachment count 542 mAttachmentsView.setCount(0); 543 } 544 } 545 mCoordinates = null; 546 mHeader = header; 547 mActivity = activity; 548 mConversationListListener = conversationListListener; 549 mSelectedConversationSet = set; 550 mDisplayedFolder = folder; 551 mStarEnabled = folder != null && !folder.isTrash(); 552 mSwipeEnabled = swipeEnabled; 553 mAdapter = adapter; 554 mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache()); 555 556 if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) { 557 mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO; 558 } else { 559 mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE; 560 } 561 562 // Initialize folder displayer. 563 if (mHeader.folderDisplayer == null) { 564 mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext); 565 } else { 566 mHeader.folderDisplayer.reset(); 567 } 568 569 final int ignoreFolderType; 570 if (mDisplayedFolder.isInbox()) { 571 ignoreFolderType = FolderType.INBOX; 572 } else { 573 ignoreFolderType = -1; 574 } 575 576 mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, 577 mDisplayedFolder.folderUri, ignoreFolderType); 578 579 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 580 mHeader.conversation.dateMs); 581 582 mConfig = new ConversationItemViewCoordinates.Config() 583 .withGadget(mGadgetMode) 584 .withAttachmentPreviews(getAttachmentPreviewsMode()); 585 if (header.folderDisplayer.hasVisibleFolders()) { 586 mConfig.showFolders(); 587 } 588 if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) { 589 mConfig.showReplyState(); 590 } 591 if (mHeader.conversation.color != 0) { 592 mConfig.showColorBlock(); 593 } 594 // Personal level. 595 mHeader.personalLevelBitmap = null; 596 if (true) { // TODO: hook this up to a setting 597 final int personalLevel = mHeader.conversation.personalLevel; 598 final boolean isImportant = 599 mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT; 600 final boolean useImportantMarkers = isImportant && priorityArrowEnabled; 601 602 if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { 603 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME 604 : ONLY_TO_ME; 605 } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { 606 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS 607 : TO_ME_AND_OTHERS; 608 } else if (useImportantMarkers) { 609 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; 610 } 611 } 612 if (mHeader.personalLevelBitmap != null) { 613 mConfig.showPersonalIndicator(); 614 } 615 616 final int overflowCount = Math.min(getOverflowCount(), sOverflowCountMax); 617 mHeader.overflowText = (overflowCount > 0) ? 618 String.format(sOverflowCountFormat, overflowCount) : null; 619 620 mAttachmentsView.setOverflowText(mHeader.overflowText); 621 622 setContentDescription(); 623 requestLayout(); 624 } 625 626 @Override 627 public void invalidateDrawable(Drawable who) { 628 boolean handled = false; 629 if (mCoordinates != null) { 630 if (mAttachmentsView.equals(who)) { 631 final Rect r = new Rect(who.getBounds()); 632 r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); 633 ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom); 634 handled = true; 635 } 636 } 637 if (!handled) { 638 super.invalidateDrawable(who); 639 } 640 } 641 642 /** 643 * Get the Conversation object associated with this view. 644 */ 645 public Conversation getConversation() { 646 return mHeader.conversation; 647 } 648 649 private static void startTimer(String tag) { 650 if (sTimer != null) { 651 sTimer.start(tag); 652 } 653 } 654 655 private static void pauseTimer(String tag) { 656 if (sTimer != null) { 657 sTimer.pause(tag); 658 } 659 } 660 661 @Override 662 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 663 Utils.traceBeginSection("CIVC.measure"); 664 final int wSize = MeasureSpec.getSize(widthMeasureSpec); 665 666 final int currentMode = mActivity.getViewMode().getMode(); 667 if (wSize != mViewWidth || mPreviousMode != currentMode) { 668 mViewWidth = wSize; 669 mPreviousMode = currentMode; 670 } 671 mHeader.viewWidth = mViewWidth; 672 673 mConfig.updateWidth(wSize).setViewMode(currentMode); 674 675 Resources res = getResources(); 676 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 677 678 mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig, 679 mAdapter.getCoordinatesCache()); 680 681 final int h = (mAnimatedHeightFraction != 1.0f) ? 682 Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height; 683 setMeasuredDimension(mConfig.getWidth(), h); 684 Utils.traceEndSection(); 685 } 686 687 @Override 688 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 689 startTimer(PERF_TAG_LAYOUT); 690 Utils.traceBeginSection("CIVC.layout"); 691 692 super.onLayout(changed, left, top, right, bottom); 693 694 Utils.traceBeginSection("text and bitmaps"); 695 calculateTextsAndBitmaps(); 696 Utils.traceEndSection(); 697 698 Utils.traceBeginSection("coordinates"); 699 calculateCoordinates(); 700 Utils.traceEndSection(); 701 702 // Subject. 703 createSubject(mHeader.unread); 704 705 if (!mHeader.isLayoutValid()) { 706 setContentDescription(); 707 } 708 mHeader.validate(); 709 710 pauseTimer(PERF_TAG_LAYOUT); 711 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 712 sTimer.dumpResults(); 713 sTimer = new Timer(); 714 sLayoutCount = 0; 715 } 716 Utils.traceEndSection(); 717 } 718 719 private void setContentDescription() { 720 if (mActivity.isAccessibilityEnabled()) { 721 mHeader.resetContentDescription(); 722 setContentDescription(mHeader.getContentDescription(mContext)); 723 } 724 } 725 726 @Override 727 public void setBackgroundResource(int resourceId) { 728 Utils.traceBeginSection("set background resource"); 729 Drawable drawable = mBackgrounds.get(resourceId); 730 if (drawable == null) { 731 drawable = getResources().getDrawable(resourceId); 732 mBackgrounds.put(resourceId, drawable); 733 } 734 if (getBackground() != drawable) { 735 super.setBackgroundDrawable(drawable); 736 } 737 Utils.traceEndSection(); 738 } 739 740 private void calculateTextsAndBitmaps() { 741 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 742 743 if (mSelectedConversationSet != null) { 744 mSelected = mSelectedConversationSet.contains(mHeader.conversation); 745 } 746 setSelected(mSelected); 747 mHeader.gadgetMode = mGadgetMode; 748 749 final boolean isUnread = mHeader.unread; 750 updateBackground(isUnread); 751 752 mHeader.sendersDisplayText = new SpannableStringBuilder(); 753 mHeader.styledSendersString = null; 754 755 // Parse senders fragments. 756 if (mHeader.conversation.conversationInfo != null) { 757 // This is Gmail 758 Context context = getContext(); 759 mHeader.messageInfoString = SendersView 760 .createMessageInfo(context, mHeader.conversation, true); 761 int maxChars = ConversationItemViewCoordinates.getSendersLength(context, 762 mCoordinates.getMode(), mHeader.conversation.hasAttachments); 763 mHeader.displayableSenderEmails = new ArrayList<String>(); 764 mHeader.displayableSenderNames = new ArrayList<String>(); 765 mHeader.styledSenders = new ArrayList<SpannableString>(); 766 SendersView.format(context, mHeader.conversation.conversationInfo, 767 mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders, 768 mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount, 769 true); 770 // If we have displayable senders, load their thumbnails 771 loadSenderImages(); 772 } else { 773 // This is Email 774 SendersView.formatSenders(mHeader, getContext(), true); 775 if (!TextUtils.isEmpty(mHeader.conversation.senders)) { 776 mHeader.displayableSenderEmails = new ArrayList<String>(); 777 mHeader.displayableSenderNames = new ArrayList<String>(); 778 779 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders); 780 for (int i = 0; i < tokens.length;i++) { 781 final Rfc822Token token = tokens[i]; 782 final String senderName = Address.decodeAddressName(token.getName()); 783 final String senderAddress = token.getAddress(); 784 mHeader.displayableSenderEmails.add(senderAddress); 785 mHeader.displayableSenderNames.add( 786 !TextUtils.isEmpty(senderName) ? senderName : senderAddress); 787 } 788 loadSenderImages(); 789 } 790 } 791 792 if (isAttachmentPreviewsEnabled()) { 793 loadAttachmentPreviews(); 794 } 795 796 if (mHeader.isLayoutValid()) { 797 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 798 return; 799 } 800 startTimer(PERF_TAG_CALCULATE_FOLDERS); 801 802 803 pauseTimer(PERF_TAG_CALCULATE_FOLDERS); 804 805 // Paper clip icon. 806 mHeader.paperclip = null; 807 if (mHeader.conversation.hasAttachments) { 808 mHeader.paperclip = ATTACHMENT; 809 } 810 811 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 812 813 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 814 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 815 } 816 817 private boolean isAttachmentPreviewsEnabled() { 818 return CONVLIST_ATTACHMENT_PREVIEWS_ENABLED 819 && !mHeader.conversation.getAttachmentPreviewUris().isEmpty(); 820 } 821 822 private int getOverflowCount() { 823 return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation 824 .getAttachmentPreviewUris().size(); 825 } 826 827 private int getAttachmentPreviewsMode() { 828 if (isAttachmentPreviewsEnabled()) { 829 return mHeader.conversation.read 830 ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ 831 : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD; 832 } else { 833 return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE; 834 } 835 } 836 837 // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which 838 // is immutable. 839 private void loadSenderImages() { 840 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 841 && mHeader.displayableSenderEmails != null 842 && mHeader.displayableSenderEmails.size() > 0) { 843 if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) { 844 LogUtils.w(LOG_TAG, 845 "Contact image width(%d) or height(%d) is 0 for mode: (%d).", 846 mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, 847 mCoordinates.getMode()); 848 return; 849 } 850 851 int size = mHeader.displayableSenderEmails.size(); 852 final List<Object> keys = Lists.newArrayListWithCapacity(size); 853 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 854 keys.add(mHeader.displayableSenderEmails.get(i)); 855 } 856 857 mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth, 858 mCoordinates.contactImagesHeight); 859 mContactImagesHolder.setDivisionIds(keys); 860 String emailAddress; 861 for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { 862 emailAddress = mHeader.displayableSenderEmails.get(i); 863 PhotoIdentifier photoIdentifier = new ContactIdentifier( 864 mHeader.displayableSenderNames.get(i), emailAddress, i); 865 sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder); 866 } 867 } 868 } 869 870 private void loadAttachmentPreviews() { 871 if (mCoordinates.attachmentPreviewsWidth <= 0 872 || mCoordinates.attachmentPreviewsHeight <= 0) { 873 LogUtils.w(LOG_TAG, 874 "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).", 875 mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight, 876 mCoordinates.getMode(), getAttachmentPreviewsMode()); 877 return; 878 } 879 Utils.traceBeginSection("attachment previews"); 880 881 Utils.traceBeginSection("Setup load attachment previews"); 882 883 LogUtils.d(LOG_TAG, 884 "loadAttachmentPreviews: Loading attachment previews for conversation %s", 885 mHeader.conversation); 886 887 // Get list of attachments and states from conversation 888 final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris(); 889 final int previewStates = mHeader.conversation.attachmentPreviewStates; 890 final int displayCount = Math.min( 891 attachmentUris.size(), AttachmentGridDrawable.MAX_VISIBLE_ATTACHMENT_COUNT); 892 Utils.traceEndSection(); 893 894 mAttachmentsView.setCoordinates(mCoordinates); 895 mAttachmentsView.setCount(displayCount); 896 mAttachmentsView.setDecodeWidth(mCoordinates.attachmentPreviewsWidth); 897 final int decodeHeight; 898 // if parallax is enabled, increase the desired vertical size of attachment bitmaps 899 // so we have extra pixels to scroll within 900 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 901 decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight 902 * SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER); 903 } else { 904 decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight; 905 } 906 mAttachmentsView.setDecodeHeight(decodeHeight); 907 908 for (int i = 0; i < displayCount; i++) { 909 Utils.traceBeginSection("setup single attachment preview"); 910 final String uri = attachmentUris.get(i); 911 912 // Find the rendition to load based on availability. 913 LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ", 914 Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST), 915 Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE), 916 uri); 917 int bestAvailableRendition = -1; 918 // BEST first, else use less preferred renditions 919 for (final int rendition : AttachmentRendition.PREFERRED_RENDITIONS) { 920 if (Attachment.getPreviewState(previewStates, i, rendition)) { 921 bestAvailableRendition = rendition; 922 break; 923 } 924 } 925 926 LogUtils.d(LOG_TAG, 927 "creating/setting drawable region in CIV=%s canvas=%s rend=%s uri=%s", 928 this, mAttachmentsView, bestAvailableRendition, uri); 929 final AttachmentDrawable ad = mAttachmentsView.getOrCreateDrawable(i); 930 if (bestAvailableRendition != -1) { 931 ad.setImage( 932 new ImageAttachmentRequest(getContext(), uri, bestAvailableRendition)); 933 } else { 934 ad.showStaticPlaceholder(); 935 } 936 937 Utils.traceEndSection(); 938 } 939 940 mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth, 941 mCoordinates.attachmentPreviewsHeight); 942 943 Utils.traceEndSection(); 944 } 945 946 private static int makeExactSpecForSize(int size) { 947 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 948 } 949 950 private static void layoutViewExactly(View v, int w, int h) { 951 v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h)); 952 v.layout(0, 0, w, h); 953 } 954 955 private void layoutSenders() { 956 if (mHeader.styledSendersString != null) { 957 if (isActivated() && showActivatedText()) { 958 mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0, 959 mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 960 } else { 961 mHeader.styledSendersString.removeSpan(sActivatedTextSpan); 962 } 963 964 final int w = mSendersWidth; 965 final int h = mCoordinates.sendersHeight; 966 mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h)); 967 mSendersTextView.setMaxLines(mCoordinates.sendersLineCount); 968 mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize); 969 layoutViewExactly(mSendersTextView, w, h); 970 971 mSendersTextView.setText(mHeader.styledSendersString); 972 } 973 } 974 975 private void createSubject(final boolean isUnread) { 976 final String subject = filterTag(mHeader.conversation.subject); 977 final String snippet = mHeader.conversation.getSnippet(); 978 final Spannable displayedStringBuilder = new SpannableString( 979 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet)); 980 981 // since spans affect text metrics, add spans to the string before measure/layout or fancy 982 // ellipsizing 983 final int subjectTextLength = (subject != null) ? subject.length() : 0; 984 if (!TextUtils.isEmpty(subject)) { 985 displayedStringBuilder.setSpan(TextAppearanceSpan.wrap( 986 isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength, 987 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 988 } 989 if (!TextUtils.isEmpty(snippet)) { 990 final int startOffset = subjectTextLength; 991 // Start after the end of the subject text; since the subject may be 992 // "" or null, this could start at the 0th character in the subjectText string 993 displayedStringBuilder.setSpan(ForegroundColorSpan.wrap( 994 isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset, 995 displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 996 } 997 if (isActivated() && showActivatedText()) { 998 displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(), 999 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 1000 } 1001 1002 final int subjectWidth = mCoordinates.subjectWidth; 1003 final int subjectHeight = mCoordinates.subjectHeight; 1004 mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight)); 1005 mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount); 1006 mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize); 1007 layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight); 1008 1009 mSubjectTextView.setText(displayedStringBuilder); 1010 } 1011 1012 private boolean showActivatedText() { 1013 // For activated elements in tablet in conversation mode, we show an activated color, since 1014 // the background is dark blue for activated versus gray for non-activated. 1015 return mTabletDevice && !mListCollapsible; 1016 } 1017 1018 private boolean canFitFragment(int width, int line, int fixedWidth) { 1019 if (line == mCoordinates.sendersLineCount) { 1020 return width + fixedWidth <= mSendersWidth; 1021 } else { 1022 return width <= mSendersWidth; 1023 } 1024 } 1025 1026 private void calculateCoordinates() { 1027 startTimer(PERF_TAG_CALCULATE_COORDINATES); 1028 1029 sPaint.setTextSize(mCoordinates.dateFontSize); 1030 sPaint.setTypeface(Typeface.DEFAULT); 1031 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText( 1032 mHeader.dateText != null ? mHeader.dateText.toString() : ""); 1033 1034 mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft; 1035 1036 if (mCoordinates.isWide()) { 1037 // In wide mode, the end of the senders should align with 1038 // the start of the subject and is based on a max width. 1039 mSendersWidth = mCoordinates.sendersWidth; 1040 } else { 1041 // In normal mode, the width is based on where the date/attachment icon start. 1042 final int dateAttachmentStart; 1043 // Have this end near the paperclip or date, not the folders. 1044 if (mHeader.paperclip != null) { 1045 dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft; 1046 } else { 1047 dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft; 1048 } 1049 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX; 1050 } 1051 1052 // Second pass to layout each fragment. 1053 sPaint.setTextSize(mCoordinates.sendersFontSize); 1054 sPaint.setTypeface(Typeface.DEFAULT); 1055 1056 if (mHeader.styledSenders != null) { 1057 ellipsizeStyledSenders(); 1058 layoutSenders(); 1059 } else { 1060 // First pass to calculate width of each fragment. 1061 int totalWidth = 0; 1062 int fixedWidth = 0; 1063 for (SenderFragment senderFragment : mHeader.senderFragments) { 1064 CharacterStyle style = senderFragment.style; 1065 int start = senderFragment.start; 1066 int end = senderFragment.end; 1067 style.updateDrawState(sPaint); 1068 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 1069 boolean isFixed = senderFragment.isFixed; 1070 if (isFixed) { 1071 fixedWidth += senderFragment.width; 1072 } 1073 totalWidth += senderFragment.width; 1074 } 1075 1076 if (mSendersWidth < 0) { 1077 mSendersWidth = 0; 1078 } 1079 totalWidth = ellipsize(fixedWidth); 1080 mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint, 1081 mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 1082 } 1083 1084 if (mSendersWidth < 0) { 1085 mSendersWidth = 0; 1086 } 1087 1088 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 1089 } 1090 1091 // The rules for displaying ellipsized senders are as follows: 1092 // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown 1093 // 2) If senders do not fit, ellipsize the last one that does fit, and stop 1094 // appending new senders 1095 private int ellipsizeStyledSenders() { 1096 SpannableStringBuilder builder = new SpannableStringBuilder(); 1097 float totalWidth = 0; 1098 boolean ellipsize = false; 1099 float width; 1100 SpannableStringBuilder messageInfoString = mHeader.messageInfoString; 1101 if (messageInfoString.length() > 0) { 1102 CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(), 1103 CharacterStyle.class); 1104 // There is only 1 character style span; make sure we apply all the 1105 // styles to the paint object before measuring. 1106 if (spans.length > 0) { 1107 spans[0].updateDrawState(sPaint); 1108 } 1109 // Paint the message info string to see if we lose space. 1110 float messageInfoWidth = sPaint.measureText(messageInfoString.toString()); 1111 totalWidth += messageInfoWidth; 1112 } 1113 SpannableString prevSender = null; 1114 SpannableString ellipsizedText; 1115 for (SpannableString sender : mHeader.styledSenders) { 1116 // There may be null sender strings if there were dupes we had to remove. 1117 if (sender == null) { 1118 continue; 1119 } 1120 // No more width available, we'll only show fixed fragments. 1121 if (ellipsize) { 1122 break; 1123 } 1124 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 1125 // There is only 1 character style span. 1126 if (spans.length > 0) { 1127 spans[0].updateDrawState(sPaint); 1128 } 1129 // If there are already senders present in this string, we need to 1130 // make sure we prepend the dividing token 1131 if (SendersView.sElidedString.equals(sender.toString())) { 1132 prevSender = sender; 1133 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 1134 } else if (builder.length() > 0 1135 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 1136 .toString()))) { 1137 prevSender = sender; 1138 sender = copyStyles(spans, sSendersSplitToken + sender); 1139 } else { 1140 prevSender = sender; 1141 } 1142 if (spans.length > 0) { 1143 spans[0].updateDrawState(sPaint); 1144 } 1145 // Measure the width of the current sender and make sure we have space 1146 width = (int) sPaint.measureText(sender.toString()); 1147 if (width + totalWidth > mSendersWidth) { 1148 // The text is too long, new line won't help. We have to 1149 // ellipsize text. 1150 ellipsize = true; 1151 width = mSendersWidth - totalWidth; // ellipsis width? 1152 ellipsizedText = copyStyles(spans, 1153 TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END)); 1154 width = (int) sPaint.measureText(ellipsizedText.toString()); 1155 } else { 1156 ellipsizedText = null; 1157 } 1158 totalWidth += width; 1159 1160 final CharSequence fragmentDisplayText; 1161 if (ellipsizedText != null) { 1162 fragmentDisplayText = ellipsizedText; 1163 } else { 1164 fragmentDisplayText = sender; 1165 } 1166 builder.append(fragmentDisplayText); 1167 } 1168 mHeader.styledMessageInfoStringOffset = builder.length(); 1169 builder.append(messageInfoString); 1170 mHeader.styledSendersString = builder; 1171 return (int)totalWidth; 1172 } 1173 1174 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 1175 SpannableString s = new SpannableString(newText); 1176 if (spans != null && spans.length > 0) { 1177 s.setSpan(spans[0], 0, s.length(), 0); 1178 } 1179 return s; 1180 } 1181 1182 private int ellipsize(int fixedWidth) { 1183 int totalWidth = 0; 1184 int currentLine = 1; 1185 boolean ellipsize = false; 1186 for (SenderFragment senderFragment : mHeader.senderFragments) { 1187 CharacterStyle style = senderFragment.style; 1188 int start = senderFragment.start; 1189 int end = senderFragment.end; 1190 int width = senderFragment.width; 1191 boolean isFixed = senderFragment.isFixed; 1192 style.updateDrawState(sPaint); 1193 1194 // No more width available, we'll only show fixed fragments. 1195 if (ellipsize && !isFixed) { 1196 senderFragment.shouldDisplay = false; 1197 continue; 1198 } 1199 1200 // New line and ellipsize text if needed. 1201 senderFragment.ellipsizedText = null; 1202 if (isFixed) { 1203 fixedWidth -= width; 1204 } 1205 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 1206 // The text is too long, new line won't help. We have to 1207 // ellipsize text. 1208 if (totalWidth == 0) { 1209 ellipsize = true; 1210 } else { 1211 // New line. 1212 if (currentLine < mCoordinates.sendersLineCount) { 1213 currentLine++; 1214 totalWidth = 0; 1215 // The text is still too long, we have to ellipsize 1216 // text. 1217 if (totalWidth + width > mSendersWidth) { 1218 ellipsize = true; 1219 } 1220 } else { 1221 ellipsize = true; 1222 } 1223 } 1224 1225 if (ellipsize) { 1226 width = mSendersWidth - totalWidth; 1227 // No more new line, we have to reserve width for fixed 1228 // fragments. 1229 if (currentLine == mCoordinates.sendersLineCount) { 1230 width -= fixedWidth; 1231 } 1232 senderFragment.ellipsizedText = TextUtils.ellipsize( 1233 mHeader.sendersText.substring(start, end), sPaint, width, 1234 TruncateAt.END).toString(); 1235 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 1236 } 1237 } 1238 senderFragment.shouldDisplay = true; 1239 totalWidth += width; 1240 1241 final CharSequence fragmentDisplayText; 1242 if (senderFragment.ellipsizedText != null) { 1243 fragmentDisplayText = senderFragment.ellipsizedText; 1244 } else { 1245 fragmentDisplayText = mHeader.sendersText.substring(start, end); 1246 } 1247 final int spanStart = mHeader.sendersDisplayText.length(); 1248 mHeader.sendersDisplayText.append(fragmentDisplayText); 1249 mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart, 1250 mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1251 } 1252 return totalWidth; 1253 } 1254 1255 /** 1256 * If the subject contains the tag of a mailing-list (text surrounded with 1257 * []), return the subject with that tag ellipsized, e.g. 1258 * "[android-gmail-team] Hello" -> "[andr...] Hello" 1259 */ 1260 private String filterTag(String subject) { 1261 String result = subject; 1262 String formatString = getContext().getResources().getString(R.string.filtered_tag); 1263 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 1264 int end = subject.indexOf(']'); 1265 if (end > 0) { 1266 String tag = subject.substring(1, end); 1267 result = String.format(formatString, Utils.ellipsize(tag, 7), 1268 subject.substring(end + 1)); 1269 } 1270 } 1271 return result; 1272 } 1273 1274 @Override 1275 public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1276 int totalItemCount) { 1277 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 1278 final View listItemView = unwrap(); 1279 if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) { 1280 return; 1281 } 1282 1283 invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY, 1284 mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth, 1285 mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight); 1286 } 1287 } 1288 1289 @Override 1290 public void onScrollStateChanged(AbsListView view, int scrollState) { 1291 } 1292 1293 @Override 1294 protected void onDraw(Canvas canvas) { 1295 Utils.traceBeginSection("CIVC.draw"); 1296 1297 // Contact photo 1298 if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) { 1299 canvas.save(); 1300 drawContactImageArea(canvas); 1301 canvas.restore(); 1302 } 1303 1304 // Senders. 1305 boolean isUnread = mHeader.unread; 1306 // Old style senders; apply text colors/ sizes/ styling. 1307 canvas.save(); 1308 if (mHeader.sendersDisplayLayout != null) { 1309 sPaint.setTextSize(mCoordinates.sendersFontSize); 1310 sPaint.setTypeface(SendersView.getTypeface(isUnread)); 1311 sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead); 1312 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY 1313 + mHeader.sendersDisplayLayout.getTopPadding()); 1314 mHeader.sendersDisplayLayout.draw(canvas); 1315 } else { 1316 drawSenders(canvas); 1317 } 1318 canvas.restore(); 1319 1320 1321 // Subject. 1322 sPaint.setTypeface(Typeface.DEFAULT); 1323 canvas.save(); 1324 drawSubject(canvas); 1325 canvas.restore(); 1326 1327 // Folders. 1328 if (mConfig.areFoldersVisible()) { 1329 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates); 1330 } 1331 1332 // If this folder has a color (combined view/Email), show it here 1333 if (mConfig.isColorBlockVisible()) { 1334 sFoldersPaint.setColor(mHeader.conversation.color); 1335 sFoldersPaint.setStyle(Paint.Style.FILL); 1336 canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY, 1337 mCoordinates.colorBlockX + mCoordinates.colorBlockWidth, 1338 mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint); 1339 } 1340 1341 // Draw the reply state. Draw nothing if neither replied nor forwarded. 1342 if (mConfig.isReplyStateVisible()) { 1343 if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) { 1344 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX, 1345 mCoordinates.replyStateY, null); 1346 } else if (mHeader.hasBeenRepliedTo) { 1347 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX, 1348 mCoordinates.replyStateY, null); 1349 } else if (mHeader.hasBeenForwarded) { 1350 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX, 1351 mCoordinates.replyStateY, null); 1352 } else if (mHeader.isInvite) { 1353 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX, 1354 mCoordinates.replyStateY, null); 1355 } 1356 } 1357 1358 if (mConfig.isPersonalIndicatorVisible()) { 1359 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX, 1360 mCoordinates.personalIndicatorY, null); 1361 } 1362 1363 // Date. 1364 sPaint.setTextSize(mCoordinates.dateFontSize); 1365 sPaint.setTypeface(Typeface.DEFAULT); 1366 sPaint.setColor(sDateTextColor); 1367 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, 1368 sPaint); 1369 1370 // Paper clip icon. 1371 if (mHeader.paperclip != null) { 1372 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 1373 } 1374 1375 if (mStarEnabled) { 1376 // Star. 1377 canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint); 1378 } 1379 1380 // Attachment previews 1381 if (isAttachmentPreviewsEnabled()) { 1382 canvas.save(); 1383 drawAttachmentPreviews(canvas); 1384 canvas.restore(); 1385 } 1386 1387 // right-side edge effect when in tablet conversation mode and the list is not collapsed 1388 if (mTabletDevice && !mListCollapsible && 1389 ViewMode.isConversationMode(mConfig.getViewMode())) { 1390 RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0, 1391 getWidth(), getHeight()); 1392 RIGHT_EDGE_TABLET.draw(canvas); 1393 1394 if (isActivated()) { 1395 // draw caret on the right, centered vertically 1396 final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth(); 1397 final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2; 1398 canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null); 1399 } 1400 } 1401 Utils.traceEndSection(); 1402 } 1403 1404 /** 1405 * Draws the contact images or check, in the correct animated state. 1406 */ 1407 private void drawContactImageArea(final Canvas canvas) { 1408 if (isSelected()) { 1409 mLastSelectedId = mHeader.conversation.id; 1410 1411 // Since this is selected, we draw the checkbox if the animation is not running, or if 1412 // it's running, and is past the half-way point 1413 if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) { 1414 // Flash in the check 1415 drawCheckbox(canvas); 1416 } else { 1417 // Flip out the contact photo 1418 drawContactImages(canvas); 1419 } 1420 } else { 1421 if ((mConversationListListener.isExitingSelectionMode() 1422 && mLastSelectedId == mHeader.conversation.id) 1423 || mPhotoFlipAnimator.isStarted()) { 1424 // Animate back to the photo 1425 if (!mPhotoFlipAnimator.isStarted()) { 1426 mPhotoFlipAnimator.startAnimation(true /* reverse */); 1427 } 1428 1429 if (mPhotoFlipAnimator.getValue() > 1) { 1430 // Flash out the check 1431 drawCheckbox(canvas); 1432 } else { 1433 // Flip in the contact photo 1434 drawContactImages(canvas); 1435 } 1436 } else { 1437 mLastSelectedId = -1; // We don't care anymore 1438 mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state 1439 1440 // Contact photos 1441 drawContactImages(canvas); 1442 } 1443 } 1444 } 1445 1446 private void drawContactImages(final Canvas canvas) { 1447 // mPhotoFlipFraction goes from 0 to 1 1448 final float value = mPhotoFlipAnimator.getValue(); 1449 1450 final float scale = 1f - value; 1451 final float xOffset = mContactImagesHolder.getWidth() * value / 2; 1452 1453 mPhotoFlipMatrix.reset(); 1454 mPhotoFlipMatrix.postScale(scale, 1); 1455 1456 canvas.translate(mCoordinates.contactImagesX + xOffset, mCoordinates.contactImagesY); 1457 1458 mContactImagesHolder.draw(canvas, mPhotoFlipMatrix); 1459 } 1460 1461 private void drawCheckbox(final Canvas canvas) { 1462 // mPhotoFlipFraction goes from 1 to 2 1463 1464 // Draw the background 1465 canvas.save(); 1466 canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); 1467 canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, 1468 sCheckBackgroundPaint); 1469 canvas.restore(); 1470 1471 final int x = mCoordinates.contactImagesX 1472 + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2; 1473 final int y = mCoordinates.contactImagesY 1474 + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2; 1475 1476 final float value = mPhotoFlipAnimator.getValue(); 1477 final float scale; 1478 1479 if (!mPhotoFlipAnimator.isStarted()) { 1480 // We're not animating 1481 scale = 1; 1482 } else if (value < 1.9) { 1483 // 1.0 to 1.9 will scale 0 to 1 1484 scale = (value - 1f) / 0.9f; 1485 } else if (value < 1.95) { 1486 // 1.9 to 1.95 will scale 1 to 19/18 1487 scale = (value - 1f) / 0.9f; 1488 } else { 1489 // 1.95 to 2.0 will scale 19/18 to 1 1490 scale = (0.95f - (value - 1.95f)) / 0.9f; 1491 } 1492 1493 final float xOffset = CHECK.getWidth() * (1f - scale) / 2f; 1494 final float yOffset = CHECK.getHeight() * (1f - scale) / 2f; 1495 1496 mCheckMatrix.reset(); 1497 mCheckMatrix.postScale(scale, scale); 1498 1499 canvas.translate(x + xOffset, y + yOffset); 1500 1501 canvas.drawBitmap(CHECK, mCheckMatrix, sPaint); 1502 } 1503 1504 private void drawAttachmentPreviews(Canvas canvas) { 1505 canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); 1506 final float fraction; 1507 if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) { 1508 final View listView = getListView(); 1509 final View listItemView = unwrap(); 1510 fraction = 1 - (float) listItemView.getBottom() 1511 / (listView.getHeight() + listItemView.getHeight()); 1512 } else { 1513 // center the preview crop around the 1/3 point from the top 1514 fraction = 0.33f; 1515 } 1516 mAttachmentsView.setParallaxFraction(fraction); 1517 mAttachmentsView.draw(canvas); 1518 } 1519 1520 private void drawSubject(Canvas canvas) { 1521 canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY); 1522 mSubjectTextView.draw(canvas); 1523 } 1524 1525 private void drawSenders(Canvas canvas) { 1526 canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY); 1527 mSendersTextView.draw(canvas); 1528 } 1529 1530 private Bitmap getStarBitmap() { 1531 return mHeader.conversation.starred ? STAR_ON : STAR_OFF; 1532 } 1533 1534 private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 1535 canvas.drawText(s, 0, s.length(), x, y, paint); 1536 } 1537 1538 /** 1539 * Set the background for this item based on: 1540 * 1. Read / Unread (unread messages have a lighter background) 1541 * 2. Tablet / Phone 1542 * 3. Checkbox checked / Unchecked (controls CAB color for item) 1543 * 4. Activated / Not activated (controls the blue highlight on tablet) 1544 * @param isUnread 1545 */ 1546 private void updateBackground(boolean isUnread) { 1547 final int background; 1548 if (isUnread) { 1549 background = R.drawable.conversation_unread_selector; 1550 } else { 1551 background = R.drawable.conversation_read_selector; 1552 } 1553 setBackgroundResource(background); 1554 } 1555 1556 /** 1557 * Toggle the check mark on this view and update the conversation or begin 1558 * drag, if drag is enabled. 1559 */ 1560 @Override 1561 public void toggleSelectedStateOrBeginDrag() { 1562 ViewMode mode = mActivity.getViewMode(); 1563 if (mIsExpansiveTablet && mode.isListMode()) { 1564 beginDragMode(); 1565 } else { 1566 toggleSelectedState(); 1567 } 1568 } 1569 1570 @Override 1571 public void toggleSelectedState() { 1572 if (mHeader != null && mHeader.conversation != null) { 1573 mSelected = !mSelected; 1574 setSelected(mSelected); 1575 Conversation conv = mHeader.conversation; 1576 // Set the list position of this item in the conversation 1577 SwipeableListView listView = getListView(); 1578 conv.position = mSelected && listView != null ? listView.getPositionForView(this) 1579 : Conversation.NO_POSITION; 1580 if (mSelectedConversationSet != null) { 1581 mSelectedConversationSet.toggle(conv); 1582 } 1583 if (mSelectedConversationSet.isEmpty()) { 1584 listView.commitDestructiveActions(true); 1585 } 1586 1587 final boolean reverse = !mSelected; 1588 mPhotoFlipAnimator.startAnimation(reverse); 1589 mPhotoFlipAnimator.invalidateArea(); 1590 1591 // We update the background after the checked state has changed 1592 // now that we have a selected background asset. Setting the background 1593 // usually waits for a layout pass, but we don't need a full layout, 1594 // just an update to the background. 1595 requestLayout(); 1596 } 1597 } 1598 1599 /** 1600 * Toggle the star on this view and update the conversation. 1601 */ 1602 public void toggleStar() { 1603 mHeader.conversation.starred = !mHeader.conversation.starred; 1604 Bitmap starBitmap = getStarBitmap(); 1605 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 1606 + starBitmap.getWidth(), 1607 mCoordinates.starY + starBitmap.getHeight()); 1608 ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor(); 1609 if (cursor != null) { 1610 cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED, 1611 mHeader.conversation.starred); 1612 } 1613 } 1614 1615 private boolean isTouchInContactPhoto(float x, float y) { 1616 // Everything before the right edge of contact photo 1617 return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 1618 && x < mCoordinates.contactImagesX + mCoordinates.contactImagesWidth 1619 + sSenderImageTouchSlop 1620 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY); 1621 } 1622 1623 private boolean isTouchInStar(float x, float y) { 1624 // Everything after the star and include a touch slop. 1625 return mStarEnabled 1626 && x > mCoordinates.starX - sStarTouchSlop 1627 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY); 1628 } 1629 1630 @Override 1631 public boolean canChildBeDismissed() { 1632 return true; 1633 } 1634 1635 @Override 1636 public void dismiss() { 1637 SwipeableListView listView = getListView(); 1638 if (listView != null) { 1639 getListView().dismissChild(this); 1640 } 1641 } 1642 1643 private boolean onTouchEventNoSwipe(MotionEvent event) { 1644 Utils.traceBeginSection("on touch event no swipe"); 1645 boolean handled = false; 1646 1647 int x = (int) event.getX(); 1648 int y = (int) event.getY(); 1649 mLastTouchX = x; 1650 mLastTouchY = y; 1651 switch (event.getAction()) { 1652 case MotionEvent.ACTION_DOWN: 1653 if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)) { 1654 mDownEvent = true; 1655 handled = true; 1656 } 1657 break; 1658 1659 case MotionEvent.ACTION_CANCEL: 1660 mDownEvent = false; 1661 break; 1662 1663 case MotionEvent.ACTION_UP: 1664 if (mDownEvent) { 1665 if (isTouchInContactPhoto(x, y)) { 1666 // Touch on the check mark 1667 toggleSelectedState(); 1668 } else if (isTouchInStar(x, y)) { 1669 // Touch on the star 1670 toggleStar(); 1671 } 1672 handled = true; 1673 } 1674 break; 1675 } 1676 1677 if (!handled) { 1678 handled = super.onTouchEvent(event); 1679 } 1680 1681 Utils.traceEndSection(); 1682 return handled; 1683 } 1684 1685 /** 1686 * ConversationItemView is given the first chance to handle touch events. 1687 */ 1688 @Override 1689 public boolean onTouchEvent(MotionEvent event) { 1690 Utils.traceBeginSection("on touch event"); 1691 int x = (int) event.getX(); 1692 int y = (int) event.getY(); 1693 mLastTouchX = x; 1694 mLastTouchY = y; 1695 if (!mSwipeEnabled) { 1696 Utils.traceEndSection(); 1697 return onTouchEventNoSwipe(event); 1698 } 1699 switch (event.getAction()) { 1700 case MotionEvent.ACTION_DOWN: 1701 if (isTouchInContactPhoto(x, y) || isTouchInStar(x, y)) { 1702 mDownEvent = true; 1703 Utils.traceEndSection(); 1704 return true; 1705 } 1706 break; 1707 case MotionEvent.ACTION_UP: 1708 if (mDownEvent) { 1709 if (isTouchInContactPhoto(x, y)) { 1710 // Touch on the check mark 1711 Utils.traceEndSection(); 1712 mDownEvent = false; 1713 toggleSelectedState(); 1714 Utils.traceEndSection(); 1715 return true; 1716 } else if (isTouchInStar(x, y)) { 1717 // Touch on the star 1718 toggleStar(); 1719 Utils.traceEndSection(); 1720 return true; 1721 } 1722 } 1723 break; 1724 } 1725 // Let View try to handle it as well. 1726 boolean handled = super.onTouchEvent(event); 1727 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1728 Utils.traceEndSection(); 1729 return true; 1730 } 1731 Utils.traceEndSection(); 1732 return handled; 1733 } 1734 1735 @Override 1736 public boolean performClick() { 1737 final boolean handled = super.performClick(); 1738 final SwipeableListView list = getListView(); 1739 if (list != null && list.getAdapter() != null) { 1740 final int pos = list.findConversation(this, mHeader.conversation); 1741 list.performItemClick(this, pos, mHeader.conversation.id); 1742 } 1743 return handled; 1744 } 1745 1746 private SwipeableConversationItemView unwrap() { 1747 final ViewParent vp = getParent(); 1748 if (vp == null || !(vp instanceof SwipeableConversationItemView)) { 1749 return null; 1750 } 1751 return (SwipeableConversationItemView) vp; 1752 } 1753 1754 private SwipeableListView getListView() { 1755 SwipeableListView v = null; 1756 final SwipeableConversationItemView wrapper = unwrap(); 1757 if (wrapper != null) { 1758 v = (SwipeableListView) wrapper.getListView(); 1759 } 1760 if (v == null) { 1761 v = mAdapter.getListView(); 1762 } 1763 return v; 1764 } 1765 1766 /** 1767 * Reset any state associated with this conversation item view so that it 1768 * can be reused. 1769 */ 1770 public void reset() { 1771 Utils.traceBeginSection("reset"); 1772 setAlpha(1f); 1773 setTranslationX(0f); 1774 mAnimatedHeightFraction = 1.0f; 1775 Utils.traceEndSection(); 1776 } 1777 1778 @SuppressWarnings("deprecation") 1779 @Override 1780 public void setTranslationX(float translationX) { 1781 super.setTranslationX(translationX); 1782 1783 // When a list item is being swiped or animated, ensure that the hosting view has a 1784 // background color set. We only enable the background during the X-translation effect to 1785 // reduce overdraw during normal list scrolling. 1786 final SwipeableConversationItemView parent = unwrap(); 1787 if (parent == null) { 1788 LogUtils.w(LOG_TAG, 1789 "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s", 1790 getParent(), translationX); 1791 } 1792 1793 if (translationX != 0f) { 1794 parent.setBackgroundResource(R.color.swiped_bg_color); 1795 } else { 1796 parent.setBackgroundDrawable(null); 1797 } 1798 } 1799 1800 /** 1801 * Grow the height of the item and fade it in when bringing a conversation 1802 * back from a destructive action. 1803 */ 1804 public Animator createSwipeUndoAnimation() { 1805 ObjectAnimator undoAnimator = createTranslateXAnimation(true); 1806 return undoAnimator; 1807 } 1808 1809 /** 1810 * Grow the height of the item and fade it in when bringing a conversation 1811 * back from a destructive action. 1812 */ 1813 public Animator createUndoAnimation() { 1814 ObjectAnimator height = createHeightAnimation(true); 1815 Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f); 1816 fade.setDuration(sShrinkAnimationDuration); 1817 fade.setInterpolator(new DecelerateInterpolator(2.0f)); 1818 AnimatorSet transitionSet = new AnimatorSet(); 1819 transitionSet.playTogether(height, fade); 1820 transitionSet.addListener(new HardwareLayerEnabler(this)); 1821 return transitionSet; 1822 } 1823 1824 /** 1825 * Grow the height of the item and fade it in when bringing a conversation 1826 * back from a destructive action. 1827 */ 1828 public Animator createDestroyWithSwipeAnimation() { 1829 ObjectAnimator slide = createTranslateXAnimation(false); 1830 ObjectAnimator height = createHeightAnimation(false); 1831 AnimatorSet transitionSet = new AnimatorSet(); 1832 transitionSet.playSequentially(slide, height); 1833 return transitionSet; 1834 } 1835 1836 private ObjectAnimator createTranslateXAnimation(boolean show) { 1837 SwipeableListView parent = getListView(); 1838 // If we can't get the parent...we have bigger problems. 1839 int width = parent != null ? parent.getMeasuredWidth() : 0; 1840 final float start = show ? width : 0f; 1841 final float end = show ? 0f : width; 1842 ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end); 1843 slide.setInterpolator(new DecelerateInterpolator(2.0f)); 1844 slide.setDuration(sSlideAnimationDuration); 1845 return slide; 1846 } 1847 1848 public Animator createDestroyAnimation() { 1849 return createHeightAnimation(false); 1850 } 1851 1852 private ObjectAnimator createHeightAnimation(boolean show) { 1853 final float start = show ? 0f : 1.0f; 1854 final float end = show ? 1.0f : 0f; 1855 ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end); 1856 height.setInterpolator(new DecelerateInterpolator(2.0f)); 1857 height.setDuration(sShrinkAnimationDuration); 1858 return height; 1859 } 1860 1861 // Used by animator 1862 public void setAnimatedHeightFraction(float height) { 1863 mAnimatedHeightFraction = height; 1864 requestLayout(); 1865 } 1866 1867 @Override 1868 public SwipeableView getSwipeableView() { 1869 return SwipeableView.from(this); 1870 } 1871 1872 /** 1873 * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag. 1874 */ 1875 private void beginDragMode() { 1876 if (mLastTouchX < 0 || mLastTouchY < 0) { 1877 return; 1878 } 1879 // If this is already checked, don't bother unchecking it! 1880 if (!mSelected) { 1881 toggleSelectedState(); 1882 } 1883 1884 // Clip data has form: [conversations_uri, conversationId1, 1885 // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] 1886 final int count = mSelectedConversationSet.size(); 1887 String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count); 1888 1889 final ClipData data = ClipData.newUri(mContext.getContentResolver(), description, 1890 Conversation.MOVE_CONVERSATIONS_URI); 1891 for (Conversation conversation : mSelectedConversationSet.values()) { 1892 data.addItem(new Item(String.valueOf(conversation.position))); 1893 } 1894 // Protect against non-existent views: only happens for monkeys 1895 final int width = this.getWidth(); 1896 final int height = this.getHeight(); 1897 final boolean isDimensionNegative = (width < 0) || (height < 0); 1898 if (isDimensionNegative) { 1899 LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: " 1900 + "width=%d, height=%d", width, height); 1901 return; 1902 } 1903 mActivity.startDragMode(); 1904 // Start drag mode 1905 startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0); 1906 } 1907 1908 /** 1909 * Handles the drag event. 1910 * 1911 * @param event the drag event to be handled 1912 */ 1913 @Override 1914 public boolean onDragEvent(DragEvent event) { 1915 switch (event.getAction()) { 1916 case DragEvent.ACTION_DRAG_ENDED: 1917 mActivity.stopDragMode(); 1918 return true; 1919 } 1920 return false; 1921 } 1922 1923 private class ShadowBuilder extends DragShadowBuilder { 1924 private final Drawable mBackground; 1925 1926 private final View mView; 1927 private final String mDragDesc; 1928 private final int mTouchX; 1929 private final int mTouchY; 1930 private int mDragDescX; 1931 private int mDragDescY; 1932 1933 public ShadowBuilder(View view, int count, int touchX, int touchY) { 1934 super(view); 1935 mView = view; 1936 mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo); 1937 mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count); 1938 mTouchX = touchX; 1939 mTouchY = touchY; 1940 } 1941 1942 @Override 1943 public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 1944 final int width = mView.getWidth(); 1945 final int height = mView.getHeight(); 1946 1947 sPaint.setTextSize(mCoordinates.subjectFontSize); 1948 mDragDescX = mCoordinates.sendersX; 1949 mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ; 1950 shadowSize.set(width, height); 1951 shadowTouchPoint.set(mTouchX, mTouchY); 1952 } 1953 1954 @Override 1955 public void onDrawShadow(Canvas canvas) { 1956 mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight()); 1957 mBackground.draw(canvas); 1958 sPaint.setTextSize(mCoordinates.subjectFontSize); 1959 canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint); 1960 } 1961 } 1962 1963 @Override 1964 public float getMinAllowScrollDistance() { 1965 return sScrollSlop; 1966 } 1967 1968 private abstract class CabAnimator { 1969 private ObjectAnimator mAnimator = null; 1970 1971 private final String mPropertyName; 1972 1973 private float mValue; 1974 1975 private final float mStartValue; 1976 private final float mEndValue; 1977 1978 private final long mDuration; 1979 1980 public CabAnimator(final String propertyName, final float startValue, final float endValue, 1981 final long duration) { 1982 mPropertyName = propertyName; 1983 1984 mStartValue = startValue; 1985 mEndValue = endValue; 1986 1987 mDuration = duration; 1988 } 1989 1990 private ObjectAnimator createAnimator() { 1991 final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this, 1992 mPropertyName, mStartValue, mEndValue); 1993 animator.setDuration(mDuration); 1994 animator.setInterpolator(new LinearInterpolator()); 1995 animator.addListener(new AnimatorListenerAdapter() { 1996 @Override 1997 public void onAnimationEnd(final Animator animation) { 1998 invalidateArea(); 1999 } 2000 }); 2001 return animator; 2002 } 2003 2004 public abstract void invalidateArea(); 2005 2006 public void setValue(final float fraction) { 2007 if (mValue == fraction) { 2008 return; 2009 } 2010 mValue = fraction; 2011 invalidateArea(); 2012 } 2013 2014 public float getValue() { 2015 return mValue; 2016 } 2017 2018 /** 2019 * @param reverse <code>true</code> to animate in reverse 2020 */ 2021 public void startAnimation(final boolean reverse) { 2022 if (mAnimator != null) { 2023 mAnimator.cancel(); 2024 } 2025 2026 mAnimator = createAnimator(); 2027 2028 if (reverse) { 2029 mAnimator.reverse(); 2030 } else { 2031 mAnimator.start(); 2032 } 2033 } 2034 2035 public void stopAnimation() { 2036 if (mAnimator != null) { 2037 mAnimator.cancel(); 2038 mAnimator = null; 2039 } 2040 2041 setValue(0); 2042 } 2043 2044 public boolean isStarted() { 2045 return mAnimator != null && mAnimator.isStarted(); 2046 } 2047 } 2048 2049 public void setPhotoFlipFraction(final float fraction) { 2050 mPhotoFlipAnimator.setValue(fraction); 2051 } 2052} 2053