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