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