/* * 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; } }