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