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