replicant-packages_apps_Email/src/com/android/email/activity/MessageListItem.java

593 lines
24 KiB
Java

/*
* Copyright (C) 2009 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.email.activity;
import com.android.email.R;
import com.android.emailcommon.utility.TextUtilities;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
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.BackgroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* This custom View is the list item for the MessageList activity, and serves two purposes:
* 1. It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
* 2. It handles internal clicks such as the checkbox or the favorite star
*/
public class MessageListItem extends View {
// Note: messagesAdapter directly fiddles with these fields.
/* package */ long mMessageId;
/* package */ long mMailboxId;
/* package */ long mAccountId;
private MessagesAdapter mAdapter;
private boolean mDownEvent;
public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
"com.android.email.MESSAGE_LIST_ITEMS";
public MessageListItem(Context context) {
super(context);
init(context);
}
public MessageListItem(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
// We always show two lines of subject/snippet
private static final int MAX_SUBJECT_SNIPPET_LINES = 2;
// Narrow mode shows sender/snippet and time/favorite stacked to save real estate; due to this,
// it is also somewhat taller
private static final int MODE_NARROW = 1;
// Wide mode shows sender, snippet, time, and favorite spread out across the screen
private static final int MODE_WIDE = 2;
// Sentinel indicating that the view needs layout
public static final int NEEDS_LAYOUT = -1;
private static boolean sInit = false;
private static final TextPaint sDefaultPaint = new TextPaint();
private static final TextPaint sBoldPaint = new TextPaint();
private static final TextPaint sDatePaint = new TextPaint();
private static final TextPaint sHighlightPaint = new TextPaint();
private static Bitmap sAttachmentIcon;
private static Bitmap sInviteIcon;
private static Bitmap sFavoriteIconOff;
private static Bitmap sFavoriteIconOn;
private static int sFavoriteIconWidth;
private static Bitmap sSelectedIconOn;
private static Bitmap sSelectedIconOff;
private static String sSubjectSnippetDivider;
public String mSender;
public CharSequence mText;
public CharSequence mSnippet;
public String mSubject;
public boolean mRead;
public long mTimestamp;
public boolean mHasAttachment = false;
public boolean mHasInvite = true;
public boolean mIsFavorite = false;
/** {@link Paint} for account color chips. null if no chips should be drawn. */
public Paint mColorChipPaint;
private int mMode = -1;
private int mViewWidth = 0;
private int mViewHeight = 0;
private int mSenderSnippetWidth;
private int mSnippetWidth;
private int mDateFaveWidth;
private static int sCheckboxHitWidth;
private static int sDateIconWidthWide;
private static int sDateIconWidthNarrow;
private static int sFavoriteHitWidth;
private static int sFavoritePaddingRight;
private static int sBadgePaddingTop;
private static int sBadgePaddingRight;
private static int sSenderPaddingTopNarrow;
private static int sSenderWidth;
private static int sPaddingLarge;
private static int sPaddingVerySmall;
private static int sPaddingSmall;
private static int sPaddingMedium;
private static int sTextSize;
private static int sItemHeightWide;
private static int sItemHeightNarrow;
private static int sMinimumWidthWideMode;
private static int sColorTipWidth;
private static int sColorTipHeight;
private static int sColorTipRightMarginOnNarrow;
private static int sColorTipRightMarginOnWide;
// Note: these cannot be shared Drawables because they are selectors which have state.
private Drawable mReadSelector;
private Drawable mUnreadSelector;
private Drawable mWideReadSelector;
private Drawable mWideUnreadSelector;
public int mSnippetLineCount = NEEDS_LAYOUT;
private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES];
private CharSequence mFormattedSender;
private CharSequence mFormattedDate;
private void init(Context context) {
if (!sInit) {
Resources r = context.getResources();
sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
sCheckboxHitWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width);
sFavoriteHitWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width);
sFavoritePaddingRight =
r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right);
sBadgePaddingTop =
r.getDimensionPixelSize(R.dimen.message_list_item_badge_padding_top);
sBadgePaddingRight =
r.getDimensionPixelSize(R.dimen.message_list_item_badge_padding_right);
sSenderPaddingTopNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow);
sDateIconWidthWide =
r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide);
sDateIconWidthNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow);
sSenderWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_sender_width);
sPaddingLarge =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_large);
sPaddingMedium =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium);
sPaddingSmall =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_small);
sPaddingVerySmall =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small);
sTextSize =
r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
sItemHeightWide =
r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
sItemHeightNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
sMinimumWidthWideMode =
r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode);
sColorTipWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width);
sColorTipHeight =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height);
sColorTipRightMarginOnNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow);
sColorTipRightMarginOnWide =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide);
sDefaultPaint.setTypeface(Typeface.DEFAULT);
sDefaultPaint.setTextSize(sTextSize);
sDefaultPaint.setAntiAlias(true);
sDatePaint.setTypeface(Typeface.DEFAULT);
sDatePaint.setTextSize(sTextSize - 1);
sDatePaint.setAntiAlias(true);
sDatePaint.setTextAlign(Align.RIGHT);
sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
sBoldPaint.setTextSize(sTextSize);
sBoldPaint.setAntiAlias(true);
sHighlightPaint.setColor(TextUtilities.HIGHLIGHT_COLOR_INT);
sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite);
sFavoriteIconOff =
BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light);
sFavoriteIconOn =
BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light);
sSelectedIconOff =
BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
sSelectedIconOn =
BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
sFavoriteIconWidth = sFavoriteIconOff.getWidth();
sInit = true;
}
}
/**
* Determine the mode of this view (WIDE or NORMAL)
*
* @param width The width of the view
* @return The mode of the view
*/
private int getViewMode(int width) {
int mode = MODE_NARROW;
if (width > sMinimumWidthWideMode) {
mode = MODE_WIDE;
}
return mode;
}
private Drawable mCurentBackground = null; // Only used by updateBackground()
/* package */ void updateBackground() {
final Drawable newBackground;
if (mRead) {
if (mMode == MODE_WIDE) {
if (mWideReadSelector == null) {
mWideReadSelector = getContext().getResources()
.getDrawable(R.drawable.message_list_wide_read_selector);
}
newBackground = mWideReadSelector;
} else {
if (mReadSelector == null) {
mReadSelector = getContext().getResources()
.getDrawable(R.drawable.message_list_read_selector);
}
newBackground = mReadSelector;
}
} else {
if (mMode == MODE_WIDE) {
if (mWideUnreadSelector == null) {
mWideUnreadSelector = getContext().getResources()
.getDrawable(R.drawable.message_list_wide_unread_selector);
}
newBackground = mWideUnreadSelector;
} else {
if (mUnreadSelector == null) {
mUnreadSelector = getContext().getResources()
.getDrawable(R.drawable.message_list_unread_selector);
}
newBackground = mUnreadSelector;
}
}
if (newBackground != mCurentBackground) {
// setBackgroundDrawable is a heavy operation. Only call it when really needed.
setBackgroundDrawable(newBackground);
mCurentBackground = newBackground;
}
}
private void calculateDrawingData() {
SpannableStringBuilder ssb = new SpannableStringBuilder();
boolean hasSubject = false;
if (!TextUtils.isEmpty(mSubject)) {
SpannableString ss = new SpannableString(mSubject);
ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(ss);
hasSubject = true;
}
if (!TextUtils.isEmpty(mSnippet)) {
if (hasSubject) {
ssb.append(sSubjectSnippetDivider);
}
ssb.append(mSnippet);
}
mText = ssb;
if (mMode == MODE_WIDE) {
mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide;
} else {
mDateFaveWidth = sDateIconWidthNarrow;
}
mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth;
// In wide mode, we use 3/4 for snippet and 1/4 for sender
mSnippetWidth = mSenderSnippetWidth;
if (mMode == MODE_WIDE) {
mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge;
}
// Create a StaticLayout with our snippet to get the line breaks
StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint,
mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
// Get the number of lines needed to render the whole snippet
mSnippetLineCount = layout.getLineCount();
// Go through our maximum number of lines, and save away what we'll end up displaying
// for those lines
for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
int start = layout.getLineStart(i);
if (i == MAX_SUBJECT_SNIPPET_LINES - 1) {
int end = mText.length();
if (start > end) continue;
// For the final line, ellipsize the text to our width
mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint,
mSnippetWidth, TruncateAt.END);
} else {
// Just extract from start to end
mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i));
}
}
// Now, format the sender for its width
TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth;
// And get the ellipsized string for the calculated width
if (TextUtils.isEmpty(mSender)) {
mFormattedSender = "";
} else {
mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth,
TruncateAt.END);
}
// Get a nicely formatted date string (relative to today)
String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
// And make it fit to our size
mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (widthMeasureSpec != 0 || mViewWidth == 0) {
mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
int mode = getViewMode(mViewWidth);
if (mode != mMode) {
// If the mode has changed, set the snippet line count to indicate layout required
mMode = mode;
mSnippetLineCount = NEEDS_LAYOUT;
}
mViewHeight = measureHeight(heightMeasureSpec, mMode);
}
setMeasuredDimension(mViewWidth, mViewHeight);
}
/**
* 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
if (mMode == MODE_WIDE) {
result = sItemHeightWide;
} else {
result = sItemHeightNarrow;
}
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;
}
@Override
public void draw(Canvas canvas) {
// Update the background, before View.draw() draws it.
setSelected(mAdapter.isSelected(this));
updateBackground();
super.draw(canvas);
}
@Override
protected void onDraw(Canvas canvas) {
if (mSnippetLineCount == NEEDS_LAYOUT) {
calculateDrawingData();
}
// Snippet starts at right of checkbox
int snippetX = sCheckboxHitWidth;
int snippetY;
int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall;
FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt();
int ascent = fontMetrics.ascent;
int descent = fontMetrics.descent;
int senderY;
if (mMode == MODE_WIDE) {
// Get the right starting point for the snippet
snippetX += sSenderWidth + sPaddingLarge;
// And center the sender and snippet
senderY = (mViewHeight - descent - ascent) / 2;
snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent;
} else {
senderY = -ascent + sSenderPaddingTopNarrow;
snippetY = senderY + lineHeight + sPaddingVerySmall;
}
// Draw the color chip
if (mColorChipPaint != null) {
final int rightMargin = (mMode == MODE_WIDE)
? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow;
final int x = mViewWidth - rightMargin - sColorTipWidth;
canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint);
}
// Draw the checkbox
int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2;
int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2;
canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
checkboxLeft, checkboxTop, sDefaultPaint);
// Draw the sender name
canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY,
mRead ? sDefaultPaint : sBoldPaint);
// Draw each of the snippet lines
for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) {
CharSequence line = mSnippetLines[i];
int drawX = snippetX;
if (line != null) {
SpannableStringBuilder ssb = (SpannableStringBuilder)line;
Object[] spans = ssb.getSpans(0, line.length(), Object.class);
int curr = 0;
for (Object span: spans) {
if (span != null) {
int spanStart = ssb.getSpanStart(span);
int spanEnd = ssb.getSpanEnd(span);
if (curr < spanStart) {
canvas.drawText(line, curr, spanStart, drawX, snippetY, sDefaultPaint);
drawX += sDefaultPaint.measureText(line, curr, spanStart);
}
TextPaint spanPaint =
(span instanceof StyleSpan) ? sBoldPaint : sDefaultPaint;
float textWidth = spanPaint.measureText(line, spanStart, spanEnd);
if (span instanceof BackgroundColorSpan) {
canvas.drawRect(drawX, snippetY + ascent, drawX + textWidth,
snippetY + descent, sHighlightPaint);
}
canvas.drawText(line, spanStart, spanEnd, drawX, snippetY, spanPaint);
drawX += textWidth;
curr = spanEnd;
}
}
canvas.drawText(line, curr, line.length(), drawX, snippetY, sDefaultPaint);
snippetY += lineHeight;
}
}
// Draw the attachment and invite icons, if necessary
int datePaddingRight;
if (mMode == MODE_WIDE) {
datePaddingRight = sFavoriteHitWidth;
} else {
datePaddingRight = sPaddingLarge;
}
int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate,
0, mFormattedDate.length()) - sPaddingMedium;
int iconTop;
if (mHasAttachment) {
left -= sAttachmentIcon.getWidth();
if (mMode == MODE_WIDE) {
iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2;
left -= sPaddingSmall;
} else {
iconTop = senderY - sAttachmentIcon.getHeight() + sBadgePaddingTop;
left -= sBadgePaddingRight;
}
canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint);
}
if (mHasInvite) {
left -= sInviteIcon.getWidth();
if (mMode == MODE_WIDE) {
iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2;
left -= sPaddingSmall;
} else {
iconTop = senderY - sInviteIcon.getHeight() + sBadgePaddingTop;
left -= sBadgePaddingRight;
}
canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint);
}
// Draw the date
canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight,
senderY, sDatePaint);
// Draw the favorite icon
int faveLeft = mViewWidth - sFavoriteIconWidth;
if (mMode == MODE_WIDE) {
faveLeft -= sFavoritePaddingRight;
} else {
faveLeft -= sPaddingLarge;
}
int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2;
if (mMode == MODE_NARROW) {
faveTop += sSenderPaddingTopNarrow;
}
canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop,
sDefaultPaint);
}
/**
* Called by the adapter at bindView() time
*
* @param adapter the adapter that creates this view
*/
public void bindViewInit(MessagesAdapter adapter) {
mAdapter = adapter;
}
/**
* Overriding this method allows us to "catch" clicks in the checkbox or star
* and process them accordingly.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
int touchX = (int) event.getX();
int checkRight = sCheckboxHitWidth;
int starLeft = mViewWidth - sFavoriteHitWidth;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (touchX < checkRight || touchX > starLeft) {
mDownEvent = true;
if ((touchX < checkRight) || (touchX > starLeft)) {
handled = true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
mDownEvent = false;
break;
case MotionEvent.ACTION_UP:
if (mDownEvent) {
if (touchX < checkRight) {
mAdapter.toggleSelected(this);
handled = true;
} else if (touchX > starLeft) {
mIsFavorite = !mIsFavorite;
mAdapter.updateFavorite(this, mIsFavorite);
handled = true;
}
}
break;
}
if (handled) {
invalidate();
} else {
handled = super.onTouchEvent(event);
}
return handled;
}
}