/* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.browse; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.ClipData; import android.content.ClipData.Item; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.text.format.DateUtils; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.TextAppearanceSpan; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; import android.util.SparseArray; import android.util.TypedValue; import android.view.DragEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.DecelerateInterpolator; import android.widget.TextView; import com.android.mail.R; import com.android.mail.browse.ConversationItemViewModel.SenderFragment; import com.android.mail.perf.Timer; import com.android.mail.photomanager.ContactPhotoManager; import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier; import com.android.mail.photomanager.PhotoManager.PhotoIdentifier; import com.android.mail.providers.Address; import com.android.mail.providers.Conversation; import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider; import com.android.mail.providers.UIProvider.ConversationColumns; import com.android.mail.providers.UIProvider.ConversationListIcon; import com.android.mail.providers.UIProvider.FolderType; import com.android.mail.ui.AnimatedAdapter; import com.android.mail.ui.ControllableActivity; import com.android.mail.ui.ConversationSelectionSet; import com.android.mail.ui.DividedImageCanvas; import com.android.mail.ui.DividedImageCanvas.InvalidateCallback; import com.android.mail.ui.FolderDisplayer; import com.android.mail.ui.SwipeableItemView; import com.android.mail.ui.SwipeableListView; import com.android.mail.ui.ViewMode; import com.android.mail.utils.HardwareLayerEnabler; import com.android.mail.utils.LogTag; import com.android.mail.utils.LogUtils; import com.android.mail.utils.Utils; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem, InvalidateCallback { // Timer. private static int sLayoutCount = 0; private static Timer sTimer; // Create the sTimer here if you need to do // perf analysis. private static final int PERF_LAYOUT_ITERATIONS = 50; private static final String PERF_TAG_LAYOUT = "CCHV.layout"; private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; private static final String LOG_TAG = LogTag.getLogTag(); // Static bitmaps. private static Bitmap STAR_OFF; private static Bitmap STAR_ON; private static Bitmap ATTACHMENT; private static Bitmap ONLY_TO_ME; private static Bitmap TO_ME_AND_OTHERS; private static Bitmap IMPORTANT_ONLY_TO_ME; private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; private static Bitmap IMPORTANT_TO_OTHERS; private static Bitmap STATE_REPLIED; private static Bitmap STATE_FORWARDED; private static Bitmap STATE_REPLIED_AND_FORWARDED; private static Bitmap STATE_CALENDAR_INVITE; private static Bitmap VISIBLE_CONVERSATION_CARET; private static Drawable RIGHT_EDGE_TABLET; private static String sSendersSplitToken; private static String sElidedPaddingToken; // Static colors. private static int sActivatedTextColor; private static int sSendersTextColorRead; private static int sSendersTextColorUnread; private static int sDateTextColor; private static int sStarTouchSlop; private static int sSenderImageTouchSlop; @Deprecated private static int sStandardScaledDimen; private static int sShrinkAnimationDuration; private static int sSlideAnimationDuration; private static int sAnimatingBackgroundColor; // Static paints. private static TextPaint sPaint = new TextPaint(); private static TextPaint sFoldersPaint = new TextPaint(); // Backgrounds for different states. private final SparseArray mBackgrounds = new SparseArray(); // Dimensions and coordinates. private int mViewWidth = -1; /** The view mode at which we calculated mViewWidth previously. */ private int mPreviousMode; private int mDateX; private int mPaperclipX; private int mSendersWidth; /** Whether we are on a tablet device or not */ private final boolean mTabletDevice; /** Whether we are on an expansive tablet */ private final boolean mIsExpansiveTablet; /** When in conversation mode, true if the list is hidden */ private final boolean mListCollapsible; @VisibleForTesting ConversationItemViewCoordinates mCoordinates; private ConversationItemViewCoordinates.Config mConfig; private final Context mContext; public ConversationItemViewModel mHeader; private boolean mDownEvent; private boolean mSelected = false; private ConversationSelectionSet mSelectedConversationSet; private Folder mDisplayedFolder; private boolean mStarEnabled; private boolean mSwipeEnabled; private int mLastTouchX; private int mLastTouchY; private AnimatedAdapter mAdapter; private float mAnimatedHeightFraction = 1.0f; private final String mAccount; private ControllableActivity mActivity; private final TextView mSubjectTextView; private final TextView mSendersTextView; private int mGadgetMode; private final DividedImageCanvas mContactImagesHolder; private int mAttachmentPreviewMode; private final DividedImageCanvas mAttachmentPreviewsCanvas; private static int sFoldersLeftPadding; private static TextAppearanceSpan sSubjectTextUnreadSpan; private static TextAppearanceSpan sSubjectTextReadSpan; private static ForegroundColorSpan sSnippetTextUnreadSpan; private static ForegroundColorSpan sSnippetTextReadSpan; private static int sScrollSlop; private static CharacterStyle sActivatedTextSpan; private static ContactPhotoManager sContactPhotoManager; private static ContactPhotoManager sAttachmentPreviewsManager; static { sPaint.setAntiAlias(true); sFoldersPaint.setAntiAlias(true); } /** * Handles displaying folders in a conversation header view. */ static class ConversationItemFolderDisplayer extends FolderDisplayer { private int mFoldersCount; public ConversationItemFolderDisplayer(Context context) { super(context); } @Override public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri, final int ignoreFolderType) { super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType); mFoldersCount = mFoldersSortedSet.size(); } @Override public void reset() { super.reset(); mFoldersCount = 0; } public boolean hasVisibleFolders() { return mFoldersCount > 0; } private int measureFolders(int availableSpace, int cellSize) { int totalWidth = 0; boolean firstTime = true; for (Folder f : mFoldersSortedSet) { final String folderString = f.name; int width = (int) sFoldersPaint.measureText(folderString) + cellSize; if (firstTime) { firstTime = false; } else { width += sFoldersLeftPadding; } totalWidth += width; if (totalWidth > availableSpace) { break; } } return totalWidth; } public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) { if (mFoldersCount == 0) { return; } final int xMinStart = coordinates.foldersX; final int xEnd = coordinates.foldersXEnd; final int y = coordinates.foldersY; final int height = coordinates.foldersHeight; final int ascent = coordinates.foldersAscent; int textBottomPadding = coordinates.foldersTextBottomPadding; sFoldersPaint.setTextSize(coordinates.foldersFontSize); sFoldersPaint.setTypeface(coordinates.foldersTypeface); // Initialize space and cell size based on the current mode. int availableSpace = xEnd - xMinStart; int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth(); int foldersCount = Math.min(mFoldersCount, maxFoldersCount); int averageWidth = availableSpace / foldersCount; int cellSize = coordinates.getFolderCellWidth(); // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that. // Extra credit: maybe cache results across items as long as font size doesn't change. final int totalWidth = measureFolders(availableSpace, cellSize); int xStart = xEnd - Math.min(availableSpace, totalWidth); final boolean overflow = totalWidth > availableSpace; // Second pass to draw folders. int i = 0; for (Folder f : mFoldersSortedSet) { if (availableSpace <= 0) { break; } final String folderString = f.name; final int fgColor = f.getForegroundColor(mDefaultFgColor); final int bgColor = f.getBackgroundColor(mDefaultBgColor); boolean labelTooLong = false; final int textW = (int) sFoldersPaint.measureText(folderString); int width = textW + cellSize + sFoldersLeftPadding; if (overflow && width > averageWidth) { if (i < foldersCount - 1) { width = averageWidth; } else { // allow the last label to take all remaining space // (and don't let it make room for padding) width = availableSpace + sFoldersLeftPadding; } labelTooLong = true; } // TODO (mindyp): how to we get this? final boolean isMuted = false; // labelValues.folderId == // sGmail.getFolderMap(mAccount).getFolderIdIgnored(); // Draw the box. sFoldersPaint.setColor(bgColor); sFoldersPaint.setStyle(Paint.Style.FILL); canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding, y + height, sFoldersPaint); // Draw the text. final int padding = cellSize / 2; sFoldersPaint.setColor(fgColor); sFoldersPaint.setStyle(Paint.Style.FILL); if (labelTooLong) { final int rightBorder = xStart + width - sFoldersLeftPadding - padding; final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP); sFoldersPaint.setShader(shader); } canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding, sFoldersPaint); if (labelTooLong) { sFoldersPaint.setShader(null); } availableSpace -= width; xStart += width; i++; } } } /** * Helpers function to align an element in the center of a space. */ private static int getPadding(int space, int length) { return (space - length) / 2; } public ConversationItemView(Context context, String account) { super(context); setClickable(true); setLongClickable(true); mContext = context.getApplicationContext(); final Resources res = mContext.getResources(); mTabletDevice = Utils.useTabletUI(res); mIsExpansiveTablet = mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false; mListCollapsible = res.getBoolean(R.bool.list_collapsed); mAccount = account; if (STAR_OFF == null) { // Initialize static bitmaps. STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_star_off); STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_star_on); ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double_important_unread); IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single_important_unread); IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_none_important_unread); STATE_REPLIED = BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light); STATE_FORWARDED = BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light); STATE_REPLIED_AND_FORWARDED = BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light); STATE_CALENDAR_INVITE = BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light); VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, R.drawable.ic_carrot_holo); RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet); // Initialize colors. sActivatedTextColor = res.getColor(R.color.senders_text_color_read); sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(sActivatedTextColor)); sSendersTextColorRead = res.getColor(R.color.senders_text_color_read); sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread); sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext, R.style.SubjectAppearanceUnreadStyle); sSubjectTextReadSpan = new TextAppearanceSpan(mContext, R.style.SubjectAppearanceReadStyle); sSnippetTextUnreadSpan = new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread)); sSnippetTextReadSpan = new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read)); sDateTextColor = res.getColor(R.color.date_text_color); sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop); sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop); sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen); sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration); // Initialize static color. sSendersSplitToken = res.getString(R.string.senders_split_token); sElidedPaddingToken = res.getString(R.string.elided_padding_token); sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color); sScrollSlop = res.getInteger(R.integer.swipeScrollSlop); sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding); sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); sAttachmentPreviewsManager = ContactPhotoManager.createContactPhotoManager(context); } mSendersTextView = new TextView(mContext); mSendersTextView.setIncludeFontPadding(false); mSubjectTextView = new TextView(mContext); mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END); mSubjectTextView.setIncludeFontPadding(false); mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() { @Override public void invalidate() { if (mCoordinates == null) { return; } ConversationItemView.this.invalidate(mCoordinates.contactImagesX, mCoordinates.contactImagesY, mCoordinates.contactImagesX + mCoordinates.contactImagesWidth, mCoordinates.contactImagesY + mCoordinates.contactImagesHeight); } }); mAttachmentPreviewsCanvas = new DividedImageCanvas(context, this); } public void bind(Conversation conversation, ControllableActivity activity, ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set, folder, checkboxOrSenderImage, swipeEnabled, priorityArrowEnabled, adapter); } private void bind(ConversationItemViewModel header, ControllableActivity activity, ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { // If this was previously bound to a conversation, remove any contact // photo manager requests. // TODO:MARKWEI attachment previews if (mHeader != null) { final ArrayList divisionIds = mContactImagesHolder.getDivisionIds(); if (divisionIds != null) { mContactImagesHolder.reset(); for (int pos = 0; pos < divisionIds.size(); pos++) { sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash( mContactImagesHolder, pos, divisionIds.get(pos))); } } } mCoordinates = null; mHeader = header; mActivity = activity; mSelectedConversationSet = set; mDisplayedFolder = folder; mStarEnabled = folder != null && !folder.isTrash(); mSwipeEnabled = swipeEnabled; mAdapter = adapter; if (mHeader.conversation.getAttachmentsCount() == 0) { mAttachmentPreviewMode = ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE; } else { mAttachmentPreviewMode = mHeader.conversation.read ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_SHORT : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_TALL; } if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) { mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO; } else { mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE; } // Initialize folder displayer. if (mHeader.folderDisplayer == null) { mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext); } else { mHeader.folderDisplayer.reset(); } final int ignoreFolderType; if (mDisplayedFolder.isInbox()) { ignoreFolderType = FolderType.INBOX; } else { ignoreFolderType = -1; } mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, mDisplayedFolder.uri, ignoreFolderType); mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, mHeader.conversation.dateMs); mConfig = new ConversationItemViewCoordinates.Config() .withGadget(mGadgetMode) .withAttachmentPreviews(mAttachmentPreviewMode); if (header.folderDisplayer.hasVisibleFolders()) { mConfig.showFolders(); } if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) { mConfig.showReplyState(); } if (mHeader.conversation.color != 0) { mConfig.showColorBlock(); } // Personal level. mHeader.personalLevelBitmap = null; if (true) { // TODO: hook this up to a setting final int personalLevel = mHeader.conversation.personalLevel; final boolean isImportant = mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT; final boolean useImportantMarkers = isImportant && priorityArrowEnabled; if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME : ONLY_TO_ME; } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS : TO_ME_AND_OTHERS; } else if (useImportantMarkers) { mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; } } if (mHeader.personalLevelBitmap != null) { mConfig.showPersonalIndicator(); } setContentDescription(); requestLayout(); } /** * Get the Conversation object associated with this view. */ public Conversation getConversation() { return mHeader.conversation; } private static void startTimer(String tag) { if (sTimer != null) { sTimer.start(tag); } } private static void pauseTimer(String tag) { if (sTimer != null) { sTimer.pause(tag); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int wSize = MeasureSpec.getSize(widthMeasureSpec); final int currentMode = mActivity.getViewMode().getMode(); if (wSize != mViewWidth || mPreviousMode != currentMode) { mViewWidth = wSize; mPreviousMode = currentMode; } mHeader.viewWidth = mViewWidth; mConfig.updateWidth(wSize).setViewMode(currentMode); Resources res = getResources(); mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); if (mHeader.standardScaledDimen != sStandardScaledDimen) { // Large Text has been toggle on/off. Update the static dimens. sStandardScaledDimen = mHeader.standardScaledDimen; ConversationItemViewCoordinates.refreshConversationDimens(mContext); } mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig, mAdapter.getCoordinatesCache()); final int h = (mAnimatedHeightFraction != 1.0f) ? Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height; setMeasuredDimension(mConfig.getWidth(), h); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { startTimer(PERF_TAG_LAYOUT); Utils.traceBeginSection("CIVC.layout"); super.onLayout(changed, left, top, right, bottom); calculateTextsAndBitmaps(); calculateCoordinates(); // Subject. createSubject(mHeader.unread); if (!mHeader.isLayoutValid(mContext)) { setContentDescription(); } mHeader.validate(mContext); pauseTimer(PERF_TAG_LAYOUT); if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { sTimer.dumpResults(); sTimer = new Timer(); sLayoutCount = 0; } Utils.traceEndSection(); } private void setContentDescription() { if (mActivity.isAccessibilityEnabled()) { mHeader.resetContentDescription(); setContentDescription(mHeader.getContentDescription(mContext)); } } @Override public void setBackgroundResource(int resourceId) { Drawable drawable = mBackgrounds.get(resourceId); if (drawable == null) { drawable = getResources().getDrawable(resourceId); mBackgrounds.put(resourceId, drawable); } if (getBackground() != drawable) { super.setBackgroundDrawable(drawable); } } private void calculateTextsAndBitmaps() { startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); if (mSelectedConversationSet != null) { mSelected = mSelectedConversationSet.contains(mHeader.conversation); } setSelected(mSelected); mHeader.gadgetMode = mGadgetMode; final boolean isUnread = mHeader.unread; updateBackground(isUnread); mHeader.sendersDisplayText = new SpannableStringBuilder(); mHeader.styledSendersString = null; // Parse senders fragments. if (mHeader.conversation.conversationInfo != null) { // This is Gmail Context context = getContext(); mHeader.messageInfoString = SendersView .createMessageInfo(context, mHeader.conversation, true); int maxChars = ConversationItemViewCoordinates.getSendersLength(context, mCoordinates.getMode(), mHeader.conversation.hasAttachments); mHeader.displayableSenderEmails = new ArrayList(); mHeader.displayableSenderNames = new ArrayList(); mHeader.styledSenders = new ArrayList(); SendersView.format(context, mHeader.conversation.conversationInfo, mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders, mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount, true); // If we have displayable senders, load their thumbnails loadSenderImages(); } else { // This is Email SendersView.formatSenders(mHeader, getContext(), true); if (!TextUtils.isEmpty(mHeader.conversation.senders)) { mHeader.displayableSenderEmails = new ArrayList(); mHeader.displayableSenderNames = new ArrayList(); final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders); for (int i = 0; i < tokens.length;i++) { final Rfc822Token token = tokens[i]; final String senderName = Address.decodeAddressName(token.getName()); final String senderAddress = token.getAddress(); mHeader.displayableSenderEmails.add(senderAddress); mHeader.displayableSenderNames.add( !TextUtils.isEmpty(senderName) ? senderName : senderAddress); } loadSenderImages(); } } if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { loadAttachmentPreviews(); } if (mHeader.isLayoutValid(mContext)) { pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); return; } startTimer(PERF_TAG_CALCULATE_FOLDERS); pauseTimer(PERF_TAG_CALCULATE_FOLDERS); // Paper clip icon. mHeader.paperclip = null; if (mHeader.conversation.hasAttachments) { mHeader.paperclip = ATTACHMENT; } startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); } // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which // is immutable. private void loadSenderImages() { if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO && mHeader.displayableSenderEmails != null && mHeader.displayableSenderEmails.size() > 0) { if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) { LogUtils.w(LOG_TAG, "Contact image width(%d) or height(%d) is 0 for mode: (%d).", mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, mCoordinates.getMode()); return; } mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight); mContactImagesHolder.setDivisionIds(mHeader.displayableSenderEmails); int size = mHeader.displayableSenderEmails.size(); String emailAddress; for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { emailAddress = mHeader.displayableSenderEmails.get(i); PhotoIdentifier photoIdentifier = new ContactIdentifier( mHeader.displayableSenderNames.get(i), emailAddress, i); sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder); } } } private void loadAttachmentPreviews() { if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { final int attachmentPreviewsHeight = ConversationItemViewCoordinates .getAttachmentPreviewsHeight(mContext, mAttachmentPreviewMode); if (mCoordinates.attachmentPreviewsWidth <= 0 || attachmentPreviewsHeight <= 0) { LogUtils.w(LOG_TAG, "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).", mCoordinates.attachmentPreviewsWidth, attachmentPreviewsHeight, mCoordinates.getMode(), mAttachmentPreviewMode); return; } mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth, attachmentPreviewsHeight); ArrayList attachments = mHeader.conversation.getAttachments(); mAttachmentPreviewsCanvas.setDivisionIds(attachments); int size = attachments.size(); for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) { String attachment = attachments.get(i); PhotoIdentifier photoIdentifier = new ContactIdentifier( attachment, attachment, i); sAttachmentPreviewsManager.loadThumbnail( photoIdentifier, mAttachmentPreviewsCanvas); } } } private static int makeExactSpecForSize(int size) { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } private static void layoutViewExactly(View v, int w, int h) { v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h)); v.layout(0, 0, w, h); } private void layoutSenders() { if (mHeader.styledSendersString != null) { if (isActivated() && showActivatedText()) { mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0, mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { mHeader.styledSendersString.removeSpan(sActivatedTextSpan); } final int w = mSendersWidth; final int h = mCoordinates.sendersHeight; mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h)); mSendersTextView.setMaxLines(mCoordinates.sendersLineCount); mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize); layoutViewExactly(mSendersTextView, w, h); mSendersTextView.setText(mHeader.styledSendersString); } } private void createSubject(final boolean isUnread) { final String subject = filterTag(mHeader.conversation.subject); final String snippet = mHeader.conversation.getSnippet(); final Spannable displayedStringBuilder = new SpannableString( Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet)); // since spans affect text metrics, add spans to the string before measure/layout or fancy // ellipsizing final int subjectTextLength = (subject != null) ? subject.length() : 0; if (!TextUtils.isEmpty(subject)) { displayedStringBuilder.setSpan(TextAppearanceSpan.wrap( isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } if (!TextUtils.isEmpty(snippet)) { final int startOffset = subjectTextLength; // Start after the end of the subject text; since the subject may be // "" or null, this could start at the 0th character in the subjectText string displayedStringBuilder.setSpan(ForegroundColorSpan.wrap( isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset, displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } if (isActivated() && showActivatedText()) { displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } final int subjectWidth = mCoordinates.subjectWidth; final int subjectHeight = mCoordinates.subjectHeight; mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight)); mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount); mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize); layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight); mSubjectTextView.setText(displayedStringBuilder); } private boolean showActivatedText() { // For activated elements in tablet in conversation mode, we show an activated color, since // the background is dark blue for activated versus gray for non-activated. final boolean isListCollapsed = mContext.getResources().getBoolean(R.bool.list_collapsed); return mTabletDevice && !isListCollapsed; } private boolean canFitFragment(int width, int line, int fixedWidth) { if (line == mCoordinates.sendersLineCount) { return width + fixedWidth <= mSendersWidth; } else { return width <= mSendersWidth; } } private void calculateCoordinates() { startTimer(PERF_TAG_CALCULATE_COORDINATES); sPaint.setTextSize(mCoordinates.dateFontSize); sPaint.setTypeface(Typeface.DEFAULT); mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText( mHeader.dateText != null ? mHeader.dateText.toString() : ""); mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft; if (mCoordinates.isWide()) { // In wide mode, the end of the senders should align with // the start of the subject and is based on a max width. mSendersWidth = mCoordinates.sendersWidth; } else { // In normal mode, the width is based on where the date/attachment icon start. final int dateAttachmentStart; // Have this end near the paperclip or date, not the folders. if (mHeader.paperclip != null) { dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft; } else { dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft; } mSendersWidth = dateAttachmentStart - mCoordinates.sendersX; } // Second pass to layout each fragment. int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; sPaint.setTextSize(mCoordinates.sendersFontSize); sPaint.setTypeface(Typeface.DEFAULT); if (mHeader.styledSenders != null) { ellipsizeStyledSenders(); layoutSenders(); } else { // First pass to calculate width of each fragment. int totalWidth = 0; int fixedWidth = 0; for (SenderFragment senderFragment : mHeader.senderFragments) { CharacterStyle style = senderFragment.style; int start = senderFragment.start; int end = senderFragment.end; style.updateDrawState(sPaint); senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); boolean isFixed = senderFragment.isFixed; if (isFixed) { fixedWidth += senderFragment.width; } totalWidth += senderFragment.width; } if (!ConversationItemViewCoordinates.displaySendersInline(mCoordinates.getMode())) { sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; } if (mSendersWidth < 0) { mSendersWidth = 0; } totalWidth = ellipsize(fixedWidth); mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint, mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true); } if (mSendersWidth < 0) { mSendersWidth = 0; } pauseTimer(PERF_TAG_CALCULATE_COORDINATES); } // The rules for displaying ellipsized senders are as follows: // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown // 2) If senders do not fit, ellipsize the last one that does fit, and stop // appending new senders private int ellipsizeStyledSenders() { SpannableStringBuilder builder = new SpannableStringBuilder(); float totalWidth = 0; boolean ellipsize = false; float width; SpannableStringBuilder messageInfoString = mHeader.messageInfoString; if (messageInfoString.length() > 0) { CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(), CharacterStyle.class); // There is only 1 character style span; make sure we apply all the // styles to the paint object before measuring. if (spans.length > 0) { spans[0].updateDrawState(sPaint); } // Paint the message info string to see if we lose space. float messageInfoWidth = sPaint.measureText(messageInfoString.toString()); totalWidth += messageInfoWidth; } SpannableString prevSender = null; SpannableString ellipsizedText; for (SpannableString sender : mHeader.styledSenders) { // There may be null sender strings if there were dupes we had to remove. if (sender == null) { continue; } // No more width available, we'll only show fixed fragments. if (ellipsize) { break; } CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); // There is only 1 character style span. if (spans.length > 0) { spans[0].updateDrawState(sPaint); } // If there are already senders present in this string, we need to // make sure we prepend the dividing token if (SendersView.sElidedString.equals(sender.toString())) { prevSender = sender; sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); } else if (builder.length() > 0 && (prevSender == null || !SendersView.sElidedString.equals(prevSender .toString()))) { prevSender = sender; sender = copyStyles(spans, sSendersSplitToken + sender); } else { prevSender = sender; } if (spans.length > 0) { spans[0].updateDrawState(sPaint); } // Measure the width of the current sender and make sure we have space width = (int) sPaint.measureText(sender.toString()); if (width + totalWidth > mSendersWidth) { // The text is too long, new line won't help. We have to // ellipsize text. ellipsize = true; width = mSendersWidth - totalWidth; // ellipsis width? ellipsizedText = copyStyles(spans, TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END)); width = (int) sPaint.measureText(ellipsizedText.toString()); } else { ellipsizedText = null; } totalWidth += width; final CharSequence fragmentDisplayText; if (ellipsizedText != null) { fragmentDisplayText = ellipsizedText; } else { fragmentDisplayText = sender; } builder.append(fragmentDisplayText); } mHeader.styledMessageInfoStringOffset = builder.length(); if (messageInfoString != null) { builder.append(messageInfoString); } mHeader.styledSendersString = builder; return (int)totalWidth; } private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { SpannableString s = new SpannableString(newText); if (spans != null && spans.length > 0) { s.setSpan(spans[0], 0, s.length(), 0); } return s; } private int ellipsize(int fixedWidth) { int totalWidth = 0; int currentLine = 1; boolean ellipsize = false; for (SenderFragment senderFragment : mHeader.senderFragments) { CharacterStyle style = senderFragment.style; int start = senderFragment.start; int end = senderFragment.end; int width = senderFragment.width; boolean isFixed = senderFragment.isFixed; style.updateDrawState(sPaint); // No more width available, we'll only show fixed fragments. if (ellipsize && !isFixed) { senderFragment.shouldDisplay = false; continue; } // New line and ellipsize text if needed. senderFragment.ellipsizedText = null; if (isFixed) { fixedWidth -= width; } if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { // The text is too long, new line won't help. We have to // ellipsize text. if (totalWidth == 0) { ellipsize = true; } else { // New line. if (currentLine < mCoordinates.sendersLineCount) { currentLine++; totalWidth = 0; // The text is still too long, we have to ellipsize // text. if (totalWidth + width > mSendersWidth) { ellipsize = true; } } else { ellipsize = true; } } if (ellipsize) { width = mSendersWidth - totalWidth; // No more new line, we have to reserve width for fixed // fragments. if (currentLine == mCoordinates.sendersLineCount) { width -= fixedWidth; } senderFragment.ellipsizedText = TextUtils.ellipsize( mHeader.sendersText.substring(start, end), sPaint, width, TruncateAt.END).toString(); width = (int) sPaint.measureText(senderFragment.ellipsizedText); } } senderFragment.shouldDisplay = true; totalWidth += width; final CharSequence fragmentDisplayText; if (senderFragment.ellipsizedText != null) { fragmentDisplayText = senderFragment.ellipsizedText; } else { fragmentDisplayText = mHeader.sendersText.substring(start, end); } final int spanStart = mHeader.sendersDisplayText.length(); mHeader.sendersDisplayText.append(fragmentDisplayText); mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart, mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } return totalWidth; } /** * If the subject contains the tag of a mailing-list (text surrounded with * []), return the subject with that tag ellipsized, e.g. * "[android-gmail-team] Hello" -> "[andr...] Hello" */ private String filterTag(String subject) { String result = subject; String formatString = getContext().getResources().getString(R.string.filtered_tag); if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { int end = subject.indexOf(']'); if (end > 0) { String tag = subject.substring(1, end); result = String.format(formatString, Utils.ellipsize(tag, 7), subject.substring(end + 1)); } } return result; } @Override protected void onDraw(Canvas canvas) { Utils.traceBeginSection("CIVC.draw"); // Contact photo if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) { canvas.save(); drawContactImages(canvas); canvas.restore(); } // Senders. boolean isUnread = mHeader.unread; // Old style senders; apply text colors/ sizes/ styling. canvas.save(); if (mHeader.sendersDisplayLayout != null) { sPaint.setTextSize(mCoordinates.sendersFontSize); sPaint.setTypeface(SendersView.getTypeface(isUnread)); sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead); canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY + mHeader.sendersDisplayLayout.getTopPadding()); mHeader.sendersDisplayLayout.draw(canvas); } else { drawSenders(canvas); } canvas.restore(); // Subject. sPaint.setTypeface(Typeface.DEFAULT); canvas.save(); drawSubject(canvas); canvas.restore(); // Folders. if (mConfig.areFoldersVisible()) { mHeader.folderDisplayer.drawFolders(canvas, mCoordinates); } // If this folder has a color (combined view/Email), show it here if (mConfig.isColorBlockVisible()) { sFoldersPaint.setColor(mHeader.conversation.color); sFoldersPaint.setStyle(Paint.Style.FILL); canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY, mCoordinates.colorBlockX + mCoordinates.colorBlockWidth, mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint); } // Draw the reply state. Draw nothing if neither replied nor forwarded. if (mConfig.isReplyStateVisible()) { if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) { canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX, mCoordinates.replyStateY, null); } else if (mHeader.hasBeenRepliedTo) { canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX, mCoordinates.replyStateY, null); } else if (mHeader.hasBeenForwarded) { canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX, mCoordinates.replyStateY, null); } else if (mHeader.isInvite) { canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX, mCoordinates.replyStateY, null); } } if (mConfig.isPersonalIndicatorVisible()) { canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX, mCoordinates.personalIndicatorY, null); } // Date. sPaint.setTextSize(mCoordinates.dateFontSize); sPaint.setTypeface(Typeface.DEFAULT); sPaint.setColor(sDateTextColor); drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, sPaint); // Paper clip icon. if (mHeader.paperclip != null) { canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); } if (mStarEnabled) { // Star. canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint); } // Attachment previews if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) { canvas.save(); drawAttachmentPreviews(canvas); canvas.restore(); } // right-side edge effect when in tablet conversation mode and the list is not collapsed if (mTabletDevice && !mListCollapsible && ViewMode.isConversationMode(mConfig.getViewMode())) { RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0, getWidth(), getHeight()); RIGHT_EDGE_TABLET.draw(canvas); if (isActivated()) { // draw caret on the right, centered vertically final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth(); final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2; canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null); } } Utils.traceEndSection(); } private void drawContactImages(Canvas canvas) { canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); mContactImagesHolder.draw(canvas); } private void drawAttachmentPreviews(Canvas canvas) { canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY); mAttachmentPreviewsCanvas.draw(canvas); } private void drawSubject(Canvas canvas) { canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY); mSubjectTextView.draw(canvas); } private void drawSenders(Canvas canvas) { canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY); mSendersTextView.draw(canvas); } private Bitmap getStarBitmap() { return mHeader.conversation.starred ? STAR_ON : STAR_OFF; } private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { canvas.drawText(s, 0, s.length(), x, y, paint); } /** * Set the background for this item based on: * 1. Read / Unread (unread messages have a lighter background) * 2. Tablet / Phone * 3. Checkbox checked / Unchecked (controls CAB color for item) * 4. Activated / Not activated (controls the blue highlight on tablet) * @param isUnread */ private void updateBackground(boolean isUnread) { final int background; if (isUnread) { background = R.drawable.conversation_unread_selector; } else { background = R.drawable.conversation_read_selector; } setBackgroundResource(background); } /** * Toggle the check mark on this view and update the conversation or begin * drag, if drag is enabled. */ @Override public void toggleSelectedStateOrBeginDrag() { ViewMode mode = mActivity.getViewMode(); if (mIsExpansiveTablet && mode.isListMode()) { beginDragMode(); } else { toggleSelectedState(); } } @Override public void toggleSelectedState() { if (mHeader != null && mHeader.conversation != null) { mSelected = !mSelected; setSelected(mSelected); Conversation conv = mHeader.conversation; // Set the list position of this item in the conversation SwipeableListView listView = getListView(); conv.position = mSelected && listView != null ? listView.getPositionForView(this) : Conversation.NO_POSITION; if (mSelectedConversationSet != null) { mSelectedConversationSet.toggle(conv); } if (mSelectedConversationSet.isEmpty()) { listView.commitDestructiveActions(true); } // We update the background after the checked state has changed // now that we have a selected background asset. Setting the background // usually waits for a layout pass, but we don't need a full layout, // just an update to the background. requestLayout(); } } /** * Toggle the star on this view and update the conversation. */ public void toggleStar() { mHeader.conversation.starred = !mHeader.conversation.starred; Bitmap starBitmap = getStarBitmap(); postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX + starBitmap.getWidth(), mCoordinates.starY + starBitmap.getHeight()); ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor(); if (cursor != null) { cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED, mHeader.conversation.starred); } } private boolean isTouchInContactPhoto(float x) { // Everything before the right edge of contact photo return (mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO && x < (mCoordinates.contactImagesX + mCoordinates.contactImagesWidth + sSenderImageTouchSlop)); } private boolean isTouchInStar(float x, float y) { // Everything after the star and include a touch slop. return mStarEnabled && x > mCoordinates.starX - sStarTouchSlop; } @Override public boolean canChildBeDismissed() { return true; } @Override public void dismiss() { SwipeableListView listView = getListView(); if (listView != null) { getListView().dismissChild(this); } } private boolean onTouchEventNoSwipe(MotionEvent event) { boolean handled = false; int x = (int) event.getX(); int y = (int) event.getY(); mLastTouchX = x; mLastTouchY = y; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) { mDownEvent = true; handled = true; } break; case MotionEvent.ACTION_CANCEL: mDownEvent = false; break; case MotionEvent.ACTION_UP: if (mDownEvent) { if (isTouchInContactPhoto(x)) { // Touch on the check mark toggleSelectedState(); } else if (isTouchInStar(x, y)) { // Touch on the star toggleStar(); } handled = true; } break; } if (!handled) { handled = super.onTouchEvent(event); } return handled; } /** * ConversationItemView is given the first chance to handle touch events. */ @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); mLastTouchX = x; mLastTouchY = y; if (!mSwipeEnabled) { return onTouchEventNoSwipe(event); } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isTouchInContactPhoto(x) || isTouchInStar(x, y)) { mDownEvent = true; return true; } break; case MotionEvent.ACTION_UP: if (mDownEvent) { if (isTouchInContactPhoto(x)) { // Touch on the check mark mDownEvent = false; toggleSelectedState(); return true; } else if (isTouchInStar(x, y)) { // Touch on the star mDownEvent = false; toggleStar(); return true; } } break; } // Let View try to handle it as well. boolean handled = super.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_DOWN) { return true; } return handled; } @Override public boolean performClick() { final boolean handled = super.performClick(); final SwipeableListView list = getListView(); if (list != null && list.getAdapter() != null) { final int pos = list.findConversation(this, mHeader.conversation); list.performItemClick(this, pos, mHeader.conversation.id); } return handled; } private SwipeableListView getListView() { SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent()) .getListView(); if (v == null) { v = mAdapter.getListView(); } return v; } /** * Reset any state associated with this conversation item view so that it * can be reused. */ public void reset() { setAlpha(1f); setTranslationX(0f); mAnimatedHeightFraction = 1.0f; } @SuppressWarnings("deprecation") @Override public void setTranslationX(float translationX) { super.setTranslationX(translationX); final ViewParent vp = getParent(); if (vp == null || !(vp instanceof SwipeableConversationItemView)) { LogUtils.w(LOG_TAG, "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s", vp, translationX); } // When a list item is being swiped or animated, ensure that the hosting view has a // background color set. We only enable the background during the X-translation effect to // reduce overdraw during normal list scrolling. final SwipeableConversationItemView parent = (SwipeableConversationItemView) vp; if (translationX != 0f) { parent.setBackgroundResource(R.color.swiped_bg_color); } else { parent.setBackgroundDrawable(null); } } /** * Grow the height of the item and fade it in when bringing a conversation * back from a destructive action. */ public Animator createSwipeUndoAnimation() { ObjectAnimator undoAnimator = createTranslateXAnimation(true); return undoAnimator; } /** * Grow the height of the item and fade it in when bringing a conversation * back from a destructive action. */ public Animator createUndoAnimation() { ObjectAnimator height = createHeightAnimation(true); Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f); fade.setDuration(sShrinkAnimationDuration); fade.setInterpolator(new DecelerateInterpolator(2.0f)); AnimatorSet transitionSet = new AnimatorSet(); transitionSet.playTogether(height, fade); transitionSet.addListener(new HardwareLayerEnabler(this)); return transitionSet; } /** * Grow the height of the item and fade it in when bringing a conversation * back from a destructive action. */ public Animator createDestroyWithSwipeAnimation() { ObjectAnimator slide = createTranslateXAnimation(false); ObjectAnimator height = createHeightAnimation(false); AnimatorSet transitionSet = new AnimatorSet(); transitionSet.playSequentially(slide, height); return transitionSet; } private ObjectAnimator createTranslateXAnimation(boolean show) { SwipeableListView parent = getListView(); // If we can't get the parent...we have bigger problems. int width = parent != null ? parent.getMeasuredWidth() : 0; final float start = show ? width : 0f; final float end = show ? 0f : width; ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end); slide.setInterpolator(new DecelerateInterpolator(2.0f)); slide.setDuration(sSlideAnimationDuration); return slide; } public Animator createDestroyAnimation() { return createHeightAnimation(false); } private ObjectAnimator createHeightAnimation(boolean show) { final float start = show ? 0f : 1.0f; final float end = show ? 1.0f : 0f; ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end); height.setInterpolator(new DecelerateInterpolator(2.0f)); height.setDuration(sShrinkAnimationDuration); return height; } // Used by animator public void setAnimatedHeightFraction(float height) { mAnimatedHeightFraction = height; requestLayout(); } @Override public SwipeableView getSwipeableView() { return SwipeableView.from(this); } /** * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag. */ private void beginDragMode() { if (mLastTouchX < 0 || mLastTouchY < 0) { return; } // If this is already checked, don't bother unchecking it! if (!mSelected) { toggleSelectedState(); } // Clip data has form: [conversations_uri, conversationId1, // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] final int count = mSelectedConversationSet.size(); String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count); final ClipData data = ClipData.newUri(mContext.getContentResolver(), description, Conversation.MOVE_CONVERSATIONS_URI); for (Conversation conversation : mSelectedConversationSet.values()) { data.addItem(new Item(String.valueOf(conversation.position))); } // Protect against non-existent views: only happens for monkeys final int width = this.getWidth(); final int height = this.getHeight(); final boolean isDimensionNegative = (width < 0) || (height < 0); if (isDimensionNegative) { LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: " + "width=%d, height=%d", width, height); return; } mActivity.startDragMode(); // Start drag mode startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0); } /** * Handles the drag event. * * @param event the drag event to be handled */ @Override public boolean onDragEvent(DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_ENDED: mActivity.stopDragMode(); return true; } return false; } private class ShadowBuilder extends DragShadowBuilder { private final Drawable mBackground; private final View mView; private final String mDragDesc; private final int mTouchX; private final int mTouchY; private int mDragDescX; private int mDragDescY; public ShadowBuilder(View view, int count, int touchX, int touchY) { super(view); mView = view; mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo); mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count); mTouchX = touchX; mTouchY = touchY; } @Override public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { int width = mView.getWidth(); int height = mView.getHeight(); mDragDescX = mCoordinates.sendersX; mDragDescY = getPadding(height, (int) mCoordinates.subjectFontSize) - mCoordinates.subjectAscent; shadowSize.set(width, height); shadowTouchPoint.set(mTouchX, mTouchY); } @Override public void onDrawShadow(Canvas canvas) { mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight()); mBackground.draw(canvas); sPaint.setTextSize(mCoordinates.subjectFontSize); canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint); } } @Override public float getMinAllowScrollDistance() { return sScrollSlop; } }