blob: 516561058a49cfab9eb9a332511dbf37b9005e98 [file] [log] [blame]
/*
* 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.Animator.AnimatorListener;
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.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
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.util.SparseArray;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
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.LetterTileProvider;
import com.android.mail.preferences.MailPrefs;
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.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.EllipsizedMultilineTextView;
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.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 CHECKMARK_OFF;
private static Bitmap CHECKMARK_ON;
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 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 sTouchSlop;
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<Drawable> mBackgrounds = new SparseArray<Drawable>();
// Dimensions and coordinates.
private int mViewWidth = -1;
/** The view mode at which we calculated mViewWidth previously. */
private int mPreviousMode;
private int mMode = -1;
private int mDateX;
private int mPaperclipX;
private int mFoldersXEnd;
private int mSendersWidth;
/** Whether we're running under test mode. */
private boolean mTesting = false;
/** Whether we are on a tablet device or not */
private final boolean mTabletDevice;
@VisibleForTesting
ConversationItemViewCoordinates mCoordinates;
private final Context mContext;
public ConversationItemViewModel mHeader;
private boolean mDownEvent;
private boolean mChecked = false;
private static int sFadedActivatedColor = -1;
private ConversationSelectionSet mSelectedConversationSet;
private Folder mDisplayedFolder;
private boolean mPriorityMarkersEnabled;
private boolean mCheckboxesEnabled;
private boolean mStarEnabled;
private boolean mSwipeEnabled;
private int mLastTouchX;
private int mLastTouchY;
private AnimatedAdapter mAdapter;
private int mAnimatedHeight = -1;
private String mAccount;
private ControllableActivity mActivity;
private int mBackgroundOverride = -1;
private EllipsizedMultilineTextView mSubjectTextView;
private TextView mSendersTextView;
private TextView mDateTextView;
private DividedImageCanvas mContactImagesHolder;
private boolean mConvListPhotosEnabled;
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 int sSendersTextViewTopPadding;
private static int sSendersTextViewHeight;
private static CharacterStyle sActivatedTextSpan;
private static Bitmap MORE_FOLDERS;
private static ContactPhotoManager sContactPhotoManager;
public static final LetterTileProvider DEFAULT_AVATAR_PROVIDER =
new LetterTileProvider();
static {
sPaint.setAntiAlias(true);
sFoldersPaint.setAntiAlias(true);
}
/**
* Handles displaying folders in a conversation header view.
*/
static class ConversationItemFolderDisplayer extends FolderDisplayer {
// Maximum number of folders to be displayed.
private static final int MAX_DISPLAYED_FOLDERS_COUNT = 4;
private int mFoldersCount;
private boolean mHasMoreFolders;
public ConversationItemFolderDisplayer(Context context) {
super(context);
}
@Override
public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri) {
super.loadConversationFolders(conv, ignoreFolderUri);
mFoldersCount = mFoldersSortedSet.size();
mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT;
mFoldersCount = Math.min(mFoldersCount, MAX_DISPLAYED_FOLDERS_COUNT);
}
@Override
public void reset() {
super.reset();
mFoldersCount = 0;
mHasMoreFolders = false;
}
public boolean hasVisibleFolders() {
return mFoldersCount > 0;
}
private int measureFolders(int mode) {
int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
mFoldersCount);
int totalWidth = 0;
for (Folder f : mFoldersSortedSet) {
final String folderString = f.name;
int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
if (width % cellSize != 0) {
width += cellSize - (width % cellSize);
}
totalWidth += width;
if (totalWidth > availableSpace) {
break;
}
}
return totalWidth;
}
public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates,
int foldersXEnd, int mode) {
if (mFoldersCount == 0) {
return;
}
int xEnd = foldersXEnd;
int y = coordinates.foldersY;
int height = coordinates.foldersHeight;
int topPadding = coordinates.foldersTopPadding;
int ascent = coordinates.foldersAscent;
int textBottomPadding = coordinates.foldersTextBottomPadding;
sFoldersPaint.setTextSize(coordinates.foldersFontSize);
// Initialize space and cell size based on the current mode.
int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
int averageWidth = availableSpace / mFoldersCount;
int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
mFoldersCount);
int totalWidth = measureFolders(mode);
int xStart = xEnd - Math.min(availableSpace, totalWidth);
// Second pass to draw folders.
for (Folder f : mFoldersSortedSet) {
final String folderString = f.name;
final int fgColor = f.getForegroundColor(mDefaultFgColor);
final int bgColor = f.getBackgroundColor(mDefaultBgColor);
int width = cellSize;
boolean labelTooLong = false;
width = (int) sFoldersPaint.measureText(folderString) + cellSize;
if (width % cellSize != 0) {
width += cellSize - (width % cellSize);
}
if (totalWidth > availableSpace && width > averageWidth) {
width = averageWidth;
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(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE);
canvas.drawRect(xStart, y, xStart + width, y + height - topPadding,
sFoldersPaint);
// Draw the text.
int padding = getPadding(width, (int) sFoldersPaint.measureText(folderString));
if (labelTooLong) {
TextPaint shortPaint = new TextPaint();
shortPaint.setColor(fgColor);
shortPaint.setTextSize(coordinates.foldersFontSize);
padding = cellSize / 2;
int rightBorder = xStart + width - padding;
Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y,
fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
shortPaint.setShader(shader);
canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding,
shortPaint);
} else {
sFoldersPaint.setColor(fgColor);
canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding,
sFoldersPaint);
}
availableSpace -= width;
xStart += width;
if (availableSpace <= 0 && mHasMoreFolders) {
canvas.drawBitmap(MORE_FOLDERS, xEnd, y + ascent, sFoldersPaint);
return;
}
}
}
}
/**
* 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);
mAccount = account;
if (CHECKMARK_OFF == null) {
// Initialize static bitmaps.
CHECKMARK_OFF = BitmapFactory.decodeResource(res,
R.drawable.btn_check_off_normal_holo_light);
CHECKMARK_ON = BitmapFactory.decodeResource(res,
R.drawable.btn_check_on_normal_holo_light);
STAR_OFF = BitmapFactory.decodeResource(res,
R.drawable.btn_star_off_normal_email_holo_light);
STAR_ON = BitmapFactory.decodeResource(res,
R.drawable.btn_star_on_normal_email_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);
ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more);
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);
// Initialize colors.
sActivatedTextColor = res.getColor(android.R.color.white);
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);
sTouchSlop = res.getDimensionPixelSize(R.dimen.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);
sSendersTextViewTopPadding = res.getDimensionPixelSize
(R.dimen.senders_textview_top_padding);
sSendersTextViewHeight = res.getDimensionPixelSize
(R.dimen.senders_textview_height);
sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
sFoldersLeftPadding = res.getDimensionPixelSize(R.dimen.folders_left_padding);
sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
}
mSubjectTextView = new EllipsizedMultilineTextView(mContext);
mSubjectTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mSubjectTextView.setMaxLines(2);
mSendersTextView = new TextView(mContext);
mSendersTextView.setMaxLines(1);
mSendersTextView.setEllipsize(TextUtils.TruncateAt.END);
mSendersTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mDateTextView = new TextView(mContext);
mDateTextView.setEllipsize(TextUtils.TruncateAt.END);
mDateTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mContactImagesHolder = new DividedImageCanvas(context, this);
}
public void bind(Cursor cursor, ControllableActivity activity, ConversationSelectionSet set,
Folder folder, boolean checkboxesDisabled, boolean swipeEnabled,
boolean priorityArrowEnabled, AnimatedAdapter adapter) {
bind(ConversationItemViewModel.forCursor(mAccount, cursor), activity, set, folder,
checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter);
}
public void bind(Conversation conversation, ControllableActivity activity,
ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled,
boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set,
folder, checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter);
}
private void bind(ConversationItemViewModel header, ControllableActivity activity,
ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled,
boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
// If this was previously bound to a conversation, remove any contact
// photo manager requests.
if (mHeader != null) {
final ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
if (divisionIds != null) {
for (int pos = 0; pos < divisionIds.size(); pos++) {
sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash(
mContactImagesHolder, pos, divisionIds.get(pos)));
}
}
}
mHeader = header;
mActivity = activity;
mSelectedConversationSet = set;
mDisplayedFolder = folder;
mCheckboxesEnabled = !checkboxesDisabled;
mConvListPhotosEnabled = MailPrefs.get(activity.getActivityContext())
.areConvListPhotosEnabled();
mStarEnabled = folder != null && !folder.isTrash();
mSwipeEnabled = swipeEnabled;
mPriorityMarkersEnabled = priorityArrowEnabled;
mAdapter = adapter;
setContentDescription();
requestLayout();
}
/**
* Get the Conversation object associated with this view.
*/
public Conversation getConversation() {
return mHeader.conversation;
}
/**
* Sets the mode. Only used for testing.
*/
@VisibleForTesting
void setMode(int mode) {
mMode = mode;
mTesting = true;
}
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 onLayout(boolean changed, int left, int top, int right, int bottom) {
startTimer(PERF_TAG_LAYOUT);
super.onLayout(changed, left, top, right, bottom);
final int width = right - left;
final int currentMode = mActivity.getViewMode().getMode();
if (width != mViewWidth || mPreviousMode != currentMode) {
mViewWidth = width;
mPreviousMode = currentMode;
if (!mTesting) {
mMode = ConversationItemViewCoordinates.getMode(mContext, mPreviousMode);
}
}
mHeader.viewWidth = mViewWidth;
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.refreshConversationHeights(mContext);
}
mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
mHeader.standardScaledDimen, mConvListPhotosEnabled);
calculateTextsAndBitmaps();
calculateCoordinates();
// Subject.
createSubject(mHeader.unread, showActivatedText());
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;
}
}
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);
// Initialize folder displayer.
if (mCoordinates.showFolders) {
if (mHeader.folderDisplayer == null) {
mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
} else {
mHeader.folderDisplayer.reset();
}
mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
mDisplayedFolder.uri);
}
if (mSelectedConversationSet != null) {
mChecked = mSelectedConversationSet.contains(mHeader.conversation);
}
mHeader.checkboxVisible = mCheckboxesEnabled && !mConvListPhotosEnabled;
final boolean isUnread = mHeader.unread;
updateBackground(isUnread);
mHeader.sendersDisplayText = new SpannableStringBuilder();
mHeader.styledSendersString = new SpannableStringBuilder();
// Parse senders fragments.
if (mHeader.conversation.conversationInfo != null) {
Context context = getContext();
mHeader.messageInfoString = SendersView
.createMessageInfo(context, mHeader.conversation, true);
int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()),
mHeader.conversation.hasAttachments);
mHeader.displayableSenderEmails = new ArrayList<String>();
mHeader.displayableSenderNames = new ArrayList<String>();
mHeader.styledSenders = new ArrayList<SpannableString>();
SendersView.format(context, mHeader.conversation.conversationInfo,
mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders,
mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount,
true);
// If we have displayable sendres, load their thumbnails
loadSenderImages();
} else {
SendersView.formatSenders(mHeader, getContext(), true);
}
mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
mHeader.conversation.dateMs);
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;
}
// Personal level.
mHeader.personalLevelBitmap = null;
if (mCoordinates.showPersonalLevel) {
final int personalLevel = mHeader.conversation.personalLevel;
final boolean isImportant =
mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
final boolean useImportantMarkers = isImportant && mPriorityMarkersEnabled;
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;
}
}
startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
}
private void loadSenderImages() {
if (mConvListPhotosEnabled && mHeader.displayableSenderEmails != null
&& mHeader.displayableSenderEmails.size() > 0) {
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);
sContactPhotoManager.loadThumbnail(DividedImageCanvas.generateHash(
mContactImagesHolder, i, emailAddress),
mContactImagesHolder, mHeader.displayableSenderNames.get(i),
emailAddress, DEFAULT_AVATAR_PROVIDER);
}
}
}
private void layoutSenders(SpannableStringBuilder sendersText) {
TextView sendersTextView = mSendersTextView;
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 = MeasureSpec.makeMeasureSpec(mSendersWidth, MeasureSpec.EXACTLY);
final int h = MeasureSpec.makeMeasureSpec(sSendersTextViewHeight, MeasureSpec.EXACTLY);
sendersTextView.measure(w, h);
sendersTextView.layout(0, 0, mSendersWidth, sSendersTextViewHeight);
sendersTextView.setText(mHeader.styledSendersString);
}
}
private void createSubject(final boolean isUnread, boolean activated) {
final String subject = filterTag(mHeader.conversation.subject);
final String snippet = mHeader.conversation.getSnippet();
final SpannableStringBuilder displayedStringBuilder = new SpannableStringBuilder(
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 subectText 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 secondLineMaxWidth;
if (!ConversationItemViewCoordinates.isWideMode(mMode) && mCoordinates.showFolders
&& mHeader.folderDisplayer != null && mHeader.folderDisplayer.hasVisibleFolders()) {
secondLineMaxWidth = mCoordinates.subjectWidth - Math.min(
ConversationItemViewCoordinates.getFoldersWidth(mContext, mMode),
mHeader.folderDisplayer.measureFolders(mMode)) - sFoldersLeftPadding;
} else {
secondLineMaxWidth = EllipsizedMultilineTextView.ALL_AVAILABLE;
}
final int subjectWidth = mCoordinates.subjectWidth;
final int subjectHeight = (int) (mSubjectTextView.getLineHeight() * 2 +
mSubjectTextView.getPaint().descent());
mSubjectTextView.measure(
MeasureSpec.makeMeasureSpec(subjectWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(subjectHeight, MeasureSpec.EXACTLY));
mSubjectTextView.layout(0, 0, subjectWidth, subjectHeight);
mSubjectTextView.setText(displayedStringBuilder, secondLineMaxWidth);
}
/**
* Returns the resource for the text color depending on whether the element is activated or not.
* @param defaultColor
* @return
*/
private int getFontColor(int defaultColor) {
final boolean isBackGroundBlue = isActivated() && showActivatedText();
return isBackGroundBlue ? sActivatedTextColor : defaultColor;
}
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();
if (ConversationItemViewCoordinates.isWideMode(mMode)) {
// Folders are displayed above the date.
mFoldersXEnd = mCoordinates.foldersXEnd;
// 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 folders or date
// (or attachment icon) start.
mFoldersXEnd = mCoordinates.foldersXEnd;
int dateAttachmentStart = 0;
// Have this end near the paperclip or date, not the folders.
if (mHeader.paperclip != null) {
dateAttachmentStart = mPaperclipX;
} else {
dateAttachmentStart = mDateX;
}
mSendersWidth = dateAttachmentStart - mCoordinates.sendersX;
}
// Second pass to layout each fragment.
int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
if (mHeader.styledSenders != null) {
ellipsizeStyledSenders();
layoutSenders(mHeader.styledSendersString);
} else {
// First pass to calculate width of each fragment.
int totalWidth = 0;
int fixedWidth = 0;
sPaint.setTextSize(mCoordinates.sendersFontSize);
sPaint.setTypeface(Typeface.DEFAULT);
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(mMode)) {
sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
}
if (mSendersWidth < 0) {
mSendersWidth = 0;
}
totalWidth = ellipsize(fixedWidth, sendersY);
mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
}
sPaint.setTextSize(mCoordinates.sendersFontSize);
sPaint.setTypeface(Typeface.DEFAULT);
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 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 sendersY) {
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++;
sendersY += mCoordinates.sendersLineHeight;
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) {
// Check mark.
if (mConvListPhotosEnabled) {
canvas.save();
drawContactImages(canvas);
canvas.restore();
} else if (mHeader.checkboxVisible) {
Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
}
// Personal Level.
if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) {
canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
mCoordinates.personalLevelY, sPaint);
}
// 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(getFontColor(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 (mCoordinates.showFolders && mHeader.folderDisplayer != null) {
mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode);
}
// If this folder has a color (combined view/Email), show it here
if (mHeader.conversation.color != 0) {
sFoldersPaint.setColor(mHeader.conversation.color);
sFoldersPaint.setStyle(Paint.Style.FILL);
int width = ConversationItemViewCoordinates.getColorBlockWidth(mContext);
int height = ConversationItemViewCoordinates.getColorBlockHeight(mContext);
canvas.drawRect(mCoordinates.dateXEnd - width, 0, mCoordinates.dateXEnd,
height, sFoldersPaint);
}
// Draw the reply state. Draw nothing if neither replied nor forwarded.
if (mCoordinates.showReplyState) {
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);
}
}
// Date.
sPaint.setTextSize(mCoordinates.dateFontSize);
sPaint.setTypeface(Typeface.DEFAULT);
sPaint.setColor(sDateTextColor);
drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
sPaint);
// Paper clip icon.
if (mHeader.paperclip != null) {
canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
}
if (mHeader.faded) {
int fadedColor = -1;
if (sFadedActivatedColor == -1) {
sFadedActivatedColor = mContext.getResources().getColor(
R.color.faded_activated_conversation_header);
}
fadedColor = sFadedActivatedColor;
int restoreState = canvas.save();
Rect bounds = canvas.getClipBounds();
canvas.clipRect(bounds.left, bounds.top, bounds.right
- mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
bounds.bottom);
canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
Color.green(fadedColor), Color.blue(fadedColor));
canvas.restoreToCount(restoreState);
}
if (mStarEnabled) {
// Star.
canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
}
}
private void drawContactImages(Canvas canvas) {
canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
mContactImagesHolder.draw(canvas);
}
private void drawSubject(Canvas canvas) {
canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY + sSendersTextViewTopPadding);
mSubjectTextView.draw(canvas);
}
private void drawSenders(Canvas canvas) {
int left;
if (mConvListPhotosEnabled && mCoordinates.inlinePersonalLevel) {
if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) {
left = mCoordinates.sendersX;
} else {
left = mCoordinates.personalLevelX;
}
} else {
left = mCoordinates.sendersX;
}
canvas.translate(left, mCoordinates.sendersY + sSendersTextViewTopPadding);
mSendersTextView.draw(canvas);
}
private Bitmap getStarBitmap() {
return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
}
private 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) {
if (mBackgroundOverride != -1) {
// If the item is animating, we use a color to avoid shrinking a 9-patch
// and getting weird artifacts from the overlap.
setBackgroundColor(mBackgroundOverride);
return;
}
final boolean isListOnTablet = mTabletDevice && mActivity.getViewMode().isListMode();
final int background;
if (isUnread) {
if (isListOnTablet) {
if (mChecked) {
background = R.drawable.list_conversation_wide_unread_selected_holo;
} else {
background = R.drawable.conversation_wide_unread_selector;
}
} else {
if (mChecked) {
background = getCheckedActivatedBackground();
} else {
background = R.drawable.conversation_unread_selector;
}
}
} else {
if (isListOnTablet) {
if (mChecked) {
background = R.drawable.list_conversation_wide_read_selected_holo;
} else {
background = R.drawable.conversation_wide_read_selector;
}
} else {
if (mChecked) {
background = getCheckedActivatedBackground();
} else {
background = R.drawable.conversation_read_selector;
}
}
}
setBackgroundResource(background);
}
private final int getCheckedActivatedBackground() {
if (isActivated() && mTabletDevice) {
return R.drawable.list_arrow_selected_holo;
} else {
return R.drawable.list_selected_holo;
}
}
/**
* Toggle the check mark on this view and update the conversation or begin
* drag, if drag is enabled.
*/
@Override
public void toggleCheckMarkOrBeginDrag() {
ViewMode mode = mActivity.getViewMode();
if (!mTabletDevice || !mode.isListMode()) {
toggleCheckMark();
} else {
beginDragMode();
}
}
private void toggleCheckMark() {
if (mHeader != null && mHeader.conversation != null) {
mChecked = !mChecked;
Conversation conv = mHeader.conversation;
// Set the list position of this item in the conversation
SwipeableListView listView = getListView();
conv.position = mChecked && listView != null ? listView.getPositionForView(this)
: Conversation.NO_POSITION;
if (mSelectedConversationSet != null) {
mSelectedConversationSet.toggle(this, 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();
}
}
/**
* Return if the checkbox for this item is checked.
*/
public boolean isChecked() {
return mChecked;
}
/**
* 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(mContext, mHeader.conversation, ConversationColumns.STARRED,
mHeader.conversation.starred);
}
}
private boolean isTouchInCheckmark(float x, float y) {
// Everything before senders and include a touch slop.
return mHeader.checkboxVisible && x < mCoordinates.sendersX + sTouchSlop;
}
private boolean isTouchInStar(float x, float y) {
// Everything after the star and include a touch slop.
return mStarEnabled && x > mCoordinates.starX - sTouchSlop;
}
@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 (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
mDownEvent = true;
handled = true;
}
break;
case MotionEvent.ACTION_CANCEL:
mDownEvent = false;
break;
case MotionEvent.ACTION_UP:
if (mDownEvent) {
if (isTouchInCheckmark(x, y)) {
// Touch on the check mark
toggleCheckMark();
} 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 (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
mDownEvent = true;
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mDownEvent) {
if (isTouchInCheckmark(x, y)) {
// Touch on the check mark
mDownEvent = false;
toggleCheckMark();
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() {
boolean handled = super.performClick();
SwipeableListView list = getListView();
if (list != null && list.getAdapter() != null) {
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() {
mBackgroundOverride = -1;
setAlpha(1);
setTranslationX(0);
setAnimatedHeight(-1);
setMinimumHeight(ConversationItemViewCoordinates.getMinHeight(mContext,
mActivity.getViewMode()));
}
/**
* Grow the height of the item and fade it in when bringing a conversation
* back from a destructive action.
* @param listener
*/
public void startSwipeUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
ObjectAnimator undoAnimator = createTranslateXAnimation(true);
undoAnimator.addListener(listener);
undoAnimator.start();
}
/**
* Grow the height of the item and fade it in when bringing a conversation
* back from a destructive action.
* @param listener
*/
public void startUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext, viewMode);
setMinimumHeight(minHeight);
mAnimatedHeight = 0;
ObjectAnimator height = createHeightAnimation(true);
Animator fade = ObjectAnimator.ofFloat(this, "itemAlpha", 0, 1.0f);
fade.setDuration(sShrinkAnimationDuration);
fade.setInterpolator(new DecelerateInterpolator(2.0f));
AnimatorSet transitionSet = new AnimatorSet();
transitionSet.playTogether(height, fade);
transitionSet.addListener(listener);
transitionSet.start();
}
/**
* Grow the height of the item and fade it in when bringing a conversation
* back from a destructive action.
* @param listener
*/
public void startDestroyWithSwipeAnimation(final AnimatorListener listener) {
ObjectAnimator slide = createTranslateXAnimation(false);
ObjectAnimator height = createHeightAnimation(false);
AnimatorSet transitionSet = new AnimatorSet();
transitionSet.playSequentially(slide, height);
transitionSet.addListener(listener);
transitionSet.start();
}
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 void startDestroyAnimation(final AnimatorListener listener) {
ObjectAnimator height = createHeightAnimation(false);
int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext,
mActivity.getViewMode());
setMinimumHeight(0);
mBackgroundOverride = sAnimatingBackgroundColor;
setBackgroundColor(mBackgroundOverride);
mAnimatedHeight = minHeight;
height.addListener(listener);
height.start();
}
private ObjectAnimator createHeightAnimation(boolean show) {
int minHeight = ConversationItemViewCoordinates.getMinHeight(getContext(),
mActivity.getViewMode());
final int start = show ? 0 : minHeight;
final int end = show ? minHeight : 0;
ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
height.setInterpolator(new DecelerateInterpolator(2.0f));
height.setDuration(sShrinkAnimationDuration);
return height;
}
// Used by animator
@SuppressWarnings("unused")
public void setItemAlpha(float alpha) {
setAlpha(alpha);
invalidate();
}
// Used by animator
@SuppressWarnings("unused")
public void setAnimatedHeight(int height) {
mAnimatedHeight = height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mAnimatedHeight == -1) {
int height = measureHeight(heightMeasureSpec,
ConversationItemViewCoordinates.getMode(mContext, mActivity.getViewMode()));
setMeasuredDimension(widthMeasureSpec, height);
} else {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
}
}
/**
* Determine the height of this view.
* @param measureSpec A measureSpec packed into an int
* @param mode The current mode of this view
* @return The height of the view, honoring constraints from measureSpec
*/
private int measureHeight(int measureSpec, int mode) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = ConversationItemViewCoordinates.getHeight(mContext, mode);
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by
// measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
/**
* Get the current position of this conversation item in the list.
*/
public int getPosition() {
return mHeader != null && mHeader.conversation != null ?
mHeader.conversation.position : -1;
}
@Override
public View getSwipeableView() {
return 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 (!mChecked) {
toggleCheckMark();
}
// 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, 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;
}
}