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