MessageListItem.java revision 64ac7a6cc81ef6dc84354153b978bd5db944e8b0
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.R;
20
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Paint.Align;
28import android.graphics.Paint.FontMetricsInt;
29import android.graphics.Typeface;
30import android.graphics.drawable.Drawable;
31import android.text.Layout.Alignment;
32import android.text.Spannable;
33import android.text.SpannableString;
34import android.text.SpannableStringBuilder;
35import android.text.StaticLayout;
36import android.text.TextPaint;
37import android.text.TextUtils;
38import android.text.TextUtils.TruncateAt;
39import android.text.format.DateUtils;
40import android.text.style.StyleSpan;
41import android.util.AttributeSet;
42import android.view.MotionEvent;
43import android.view.View;
44
45/**
46 * This custom View is the list item for the MessageList activity, and serves two purposes:
47 * 1.  It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
48 * 2.  It handles internal clicks such as the checkbox or the favorite star
49 */
50public class MessageListItem extends View {
51    // Note: messagesAdapter directly fiddles with these fields.
52    /* package */ long mMessageId;
53    /* package */ long mMailboxId;
54    /* package */ long mAccountId;
55
56    private MessagesAdapter mAdapter;
57
58    private boolean mDownEvent;
59
60    public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
61        "com.android.email.MESSAGE_LIST_ITEMS";
62
63    public MessageListItem(Context context) {
64        super(context);
65        init(context);
66    }
67
68    public MessageListItem(Context context, AttributeSet attrs) {
69        super(context, attrs);
70        init(context);
71    }
72
73    public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
74        super(context, attrs, defStyle);
75        init(context);
76    }
77
78    // We always show two lines of subject/snippet
79    private static final int MAX_SUBJECT_SNIPPET_LINES = 2;
80    // Narrow mode shows sender/snippet and time/favorite stacked to save real estate; due to this,
81    // it is also somewhat taller
82    private static final int MODE_NARROW = 1;
83    // Wide mode shows sender, snippet, time, and favorite spread out across the screen
84    private static final int MODE_WIDE = 2;
85    // Sentinel indicating that the view needs layout
86    public static final int NEEDS_LAYOUT = -1;
87
88    private static boolean sInit = false;
89    private static final TextPaint sDefaultPaint = new TextPaint();
90    private static final TextPaint sBoldPaint = new TextPaint();
91    private static final TextPaint sDatePaint = new TextPaint();
92    private static Bitmap sAttachmentIcon;
93    private static Bitmap sInviteIcon;
94    private static Bitmap sFavoriteIconOff;
95    private static Bitmap sFavoriteIconOn;
96    private static int sFavoriteIconWidth;
97    private static Bitmap sSelectedIconOn;
98    private static Bitmap sSelectedIconOff;
99    private static String sSubjectSnippetDivider;
100
101    public String mSender;
102    public CharSequence mText;
103    public String mSnippet;
104    public String mSubject;
105    public boolean mRead;
106    public long mTimestamp;
107    public boolean mHasAttachment = false;
108    public boolean mHasInvite = true;
109    public boolean mIsFavorite = false;
110    /** {@link Paint} for account color chips.  null if no chips should be drawn.  */
111    public Paint mColorChipPaint;
112
113    private int mMode = -1;
114
115    private int mViewWidth = 0;
116    private int mViewHeight = 0;
117    private int mSenderSnippetWidth;
118    private int mSnippetWidth;
119    private int mDateFaveWidth;
120
121    private static int sCheckboxHitWidth;
122    private static int sDateIconWidthWide;
123    private static int sDateIconWidthNarrow;
124    private static int sFavoriteHitWidth;
125    private static int sFavoritePaddingRight;
126    private static int sSenderPaddingTopNarrow;
127    private static int sSenderWidth;
128    private static int sPaddingLarge;
129    private static int sPaddingVerySmall;
130    private static int sPaddingSmall;
131    private static int sPaddingMedium;
132    private static int sTextSize;
133    private static int sItemHeightWide;
134    private static int sItemHeightNarrow;
135    private static int sMinimumWidthWideMode;
136    private static int sColorTipWidth;
137    private static int sColorTipHeight;
138    private static int sColorTipRightMarginOnNarrow;
139    private static int sColorTipRightMarginOnWide;
140
141    // Note: these cannot be shared Drawables because they are selectors which have state.
142    private Drawable mReadSelector;
143    private Drawable mUnreadSelector;
144    private Drawable mWideReadSelector;
145    private Drawable mWideUnreadSelector;
146
147    public int mSnippetLineCount = NEEDS_LAYOUT;
148    private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES];
149    private CharSequence mFormattedSender;
150    private CharSequence mFormattedDate;
151
152    private void init(Context context) {
153        if (!sInit) {
154            Resources r = context.getResources();
155            sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
156            sCheckboxHitWidth =
157                r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width);
158            sFavoriteHitWidth =
159                r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width);
160            sFavoritePaddingRight =
161                r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right);
162            sSenderPaddingTopNarrow =
163                r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow);
164            sDateIconWidthWide =
165                r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide);
166            sDateIconWidthNarrow =
167                r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow);
168            sSenderWidth =
169                r.getDimensionPixelSize(R.dimen.message_list_item_sender_width);
170            sPaddingLarge =
171                r.getDimensionPixelSize(R.dimen.message_list_item_padding_large);
172            sPaddingMedium =
173                r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium);
174            sPaddingSmall =
175                r.getDimensionPixelSize(R.dimen.message_list_item_padding_small);
176            sPaddingVerySmall =
177                r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small);
178            sTextSize =
179                r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
180            sItemHeightWide =
181                r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
182            sItemHeightNarrow =
183                r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
184            sMinimumWidthWideMode =
185                r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode);
186            sColorTipWidth =
187                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width);
188            sColorTipHeight =
189                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height);
190            sColorTipRightMarginOnNarrow =
191                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow);
192            sColorTipRightMarginOnWide =
193                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide);
194
195            sDefaultPaint.setTypeface(Typeface.DEFAULT);
196            sDefaultPaint.setTextSize(sTextSize);
197            sDefaultPaint.setAntiAlias(true);
198            sDatePaint.setTypeface(Typeface.DEFAULT);
199            sDatePaint.setTextSize(sTextSize - 1);
200            sDatePaint.setAntiAlias(true);
201            sDatePaint.setTextAlign(Align.RIGHT);
202            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
203            sBoldPaint.setTextSize(sTextSize);
204            sBoldPaint.setAntiAlias(true);
205            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
206            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite);
207            sFavoriteIconOff =
208                BitmapFactory.decodeResource(r, R.drawable.ic_star_none_holo_light);
209            sFavoriteIconOn =
210                BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_on);
211            sSelectedIconOff =
212                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
213            sSelectedIconOn =
214                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
215
216            sFavoriteIconWidth = sFavoriteIconOff.getWidth();
217            sInit = true;
218        }
219    }
220
221    /**
222     * Determine the mode of this view (WIDE or NORMAL)
223     *
224     * @param width The width of the view
225     * @return The mode of the view
226     */
227    private int getViewMode(int width) {
228        int mode = MODE_NARROW;
229        if (width > sMinimumWidthWideMode) {
230            mode = MODE_WIDE;
231        }
232        return mode;
233    }
234
235    private Drawable mCurentBackground = null; // Only used by updateBackground()
236
237    /* package */ void updateBackground() {
238        final Drawable newBackground;
239        if (mRead) {
240            if (mMode == MODE_WIDE) {
241                if (mWideReadSelector == null) {
242                    mWideReadSelector = getContext().getResources()
243                            .getDrawable(R.drawable.message_list_wide_read_selector);
244                }
245                newBackground = mWideReadSelector;
246            } else {
247                if (mReadSelector == null) {
248                    mReadSelector = getContext().getResources()
249                            .getDrawable(R.drawable.message_list_read_selector);
250                }
251                newBackground = mReadSelector;
252            }
253        } else {
254            if (mMode == MODE_WIDE) {
255                if (mWideUnreadSelector == null) {
256                    mWideUnreadSelector = getContext().getResources()
257                            .getDrawable(R.drawable.message_list_wide_unread_selector);
258                }
259                newBackground = mWideUnreadSelector;
260            } else {
261                if (mUnreadSelector == null) {
262                    mUnreadSelector = getContext().getResources()
263                            .getDrawable(R.drawable.message_list_unread_selector);
264                }
265                newBackground = mUnreadSelector;
266            }
267        }
268        if (newBackground != mCurentBackground) {
269            // setBackgroundDrawable is a heavy operation.  Only call it when really needed.
270            setBackgroundDrawable(newBackground);
271            mCurentBackground = newBackground;
272        }
273    }
274
275    private void calculateDrawingData() {
276        SpannableStringBuilder ssb = new SpannableStringBuilder();
277        boolean hasSubject = false;
278        if (!TextUtils.isEmpty(mSubject)) {
279            SpannableString ss = new SpannableString(mSubject);
280            ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
281                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
282            ssb.append(ss);
283            hasSubject = true;
284        }
285        if (!TextUtils.isEmpty(mSnippet)) {
286            if (hasSubject) {
287                ssb.append(sSubjectSnippetDivider);
288            }
289            ssb.append(mSnippet);
290        }
291        mText = ssb;
292
293        if (mMode == MODE_WIDE) {
294            mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide;
295        } else {
296            mDateFaveWidth = sDateIconWidthNarrow;
297        }
298        mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth;
299
300        // In wide mode, we use 3/4 for snippet and 1/4 for sender
301        mSnippetWidth = mSenderSnippetWidth;
302        if (mMode == MODE_WIDE) {
303            mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge;
304        }
305
306        // Create a StaticLayout with our snippet to get the line breaks
307        StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint,
308                mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
309        // Get the number of lines needed to render the whole snippet
310        mSnippetLineCount = layout.getLineCount();
311        // Go through our maximum number of lines, and save away what we'll end up displaying
312        // for those lines
313        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
314            int start = layout.getLineStart(i);
315            if (i == MAX_SUBJECT_SNIPPET_LINES - 1) {
316                int end = mText.length();
317                if (start > end) continue;
318                // For the final line, ellipsize the text to our width
319                mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint,
320                        mSnippetWidth, TruncateAt.END);
321            } else {
322                // Just extract from start to end
323                mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i));
324            }
325        }
326
327        // Now, format the sender for its width
328        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
329        int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth;
330        // And get the ellipsized string for the calculated width
331        mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth, TruncateAt.END);
332        // Get a nicely formatted date string (relative to today)
333        String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
334        // And make it fit to our size
335        mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END);
336    }
337
338    @Override
339    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
340        if (widthMeasureSpec != 0 || mViewWidth == 0) {
341            mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
342            int mode = getViewMode(mViewWidth);
343            if (mode != mMode) {
344                // If the mode has changed, set the snippet line count to indicate layout required
345                mMode = mode;
346                mSnippetLineCount = NEEDS_LAYOUT;
347            }
348            mViewHeight = measureHeight(heightMeasureSpec, mMode);
349        }
350        setMeasuredDimension(mViewWidth, mViewHeight);
351    }
352
353    /**
354     * Determine the height of this view
355     *
356     * @param measureSpec A measureSpec packed into an int
357     * @param mode The current mode of this view
358     * @return The height of the view, honoring constraints from measureSpec
359     */
360    private int measureHeight(int measureSpec, int mode) {
361        int result = 0;
362        int specMode = MeasureSpec.getMode(measureSpec);
363        int specSize = MeasureSpec.getSize(measureSpec);
364
365        if (specMode == MeasureSpec.EXACTLY) {
366            // We were told how big to be
367            result = specSize;
368        } else {
369            // Measure the text
370            if (mMode == MODE_WIDE) {
371                result = sItemHeightWide;
372            } else {
373                result = sItemHeightNarrow;
374            }
375            if (specMode == MeasureSpec.AT_MOST) {
376                // Respect AT_MOST value if that was what is called for by
377                // measureSpec
378                result = Math.min(result, specSize);
379            }
380        }
381        return result;
382    }
383
384    @Override
385    public void draw(Canvas canvas) {
386        // Update the background, before View.draw() draws it.
387        updateBackground();
388        super.draw(canvas);
389    }
390
391    @Override
392    protected void onDraw(Canvas canvas) {
393        if (mSnippetLineCount == NEEDS_LAYOUT) {
394            calculateDrawingData();
395        }
396        // Snippet starts at right of checkbox
397        int snippetX = sCheckboxHitWidth;
398        int snippetY;
399        int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall;
400        FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt();
401        int ascent = fontMetrics.ascent;
402        int descent = fontMetrics.descent;
403        int senderY;
404
405        if (mMode == MODE_WIDE) {
406            // Get the right starting point for the snippet
407            snippetX += sSenderWidth + sPaddingLarge;
408            // And center the sender and snippet
409            senderY = (mViewHeight - descent - ascent) / 2;
410            snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent;
411        } else {
412            senderY = -ascent + sSenderPaddingTopNarrow;
413            snippetY = senderY + lineHeight + sPaddingVerySmall;
414        }
415
416        // Draw the color chip
417        if (mColorChipPaint != null) {
418            final int rightMargin = (mMode == MODE_WIDE)
419                    ? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow;
420            final int x = mViewWidth - rightMargin - sColorTipWidth;
421            canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint);
422        }
423
424        // Draw the checkbox
425        int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2;
426        int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2;
427        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
428                checkboxLeft, checkboxTop, sDefaultPaint);
429
430        // Draw the sender name
431        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY,
432                mRead ? sDefaultPaint : sBoldPaint);
433
434        // Draw each of the snippet lines
435        int subjectEnd = (mSubject == null) ? 0 : mSubject.length();
436        int lineStart = 0;
437        TextPaint subjectPaint = mRead ? sDefaultPaint : sBoldPaint;
438        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) {
439            CharSequence line = mSnippetLines[i];
440            int drawX = snippetX;
441            if (line != null) {
442                int defaultPaintStart = 0;
443                if (lineStart <= subjectEnd) {
444                    int boldPaintEnd = subjectEnd - lineStart;
445                    if (boldPaintEnd > line.length()) {
446                        boldPaintEnd = line.length();
447                    }
448                    // From 0 to end, do in bold or default depending on the read flag
449                    canvas.drawText(line, 0, boldPaintEnd, drawX, snippetY, subjectPaint);
450                    defaultPaintStart = boldPaintEnd;
451                    drawX += subjectPaint.measureText(line, 0, boldPaintEnd);
452                }
453                canvas.drawText(line, defaultPaintStart, line.length(), drawX, snippetY,
454                        sDefaultPaint);
455                snippetY += lineHeight;
456                lineStart += line.length();
457            }
458        }
459
460        // Draw the attachment and invite icons, if necessary
461        int datePaddingRight;
462        if (mMode == MODE_WIDE) {
463            datePaddingRight = sFavoriteHitWidth;
464        } else {
465            datePaddingRight = sPaddingLarge;
466        }
467        int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate,
468                0, mFormattedDate.length()) - sPaddingMedium;
469
470        int iconTop;
471        if (mHasAttachment) {
472            left -= sAttachmentIcon.getWidth() + sPaddingSmall;
473            if (mMode == MODE_WIDE) {
474                iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2;
475            } else {
476                iconTop = senderY - sAttachmentIcon.getHeight();
477            }
478            canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint);
479        }
480        if (mHasInvite) {
481            left -= sInviteIcon.getWidth() + sPaddingSmall;
482            if (mMode == MODE_WIDE) {
483                iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2;
484            } else {
485                iconTop = senderY - sInviteIcon.getHeight();
486            }
487            canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint);
488        }
489
490        // Draw the date
491        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight,
492                senderY, sDatePaint);
493
494        // Draw the favorite icon
495        int faveLeft = mViewWidth - sFavoriteIconWidth;
496        if (mMode == MODE_WIDE) {
497            faveLeft -= sFavoritePaddingRight;
498        } else {
499            faveLeft -= sPaddingLarge;
500        }
501        int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2;
502        if (mMode == MODE_NARROW) {
503            faveTop += sSenderPaddingTopNarrow;
504        }
505        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop,
506                sDefaultPaint);
507    }
508
509    /**
510     * Called by the adapter at bindView() time
511     *
512     * @param adapter the adapter that creates this view
513     */
514    public void bindViewInit(MessagesAdapter adapter) {
515        mAdapter = adapter;
516    }
517
518    /**
519     * Overriding this method allows us to "catch" clicks in the checkbox or star
520     * and process them accordingly.
521     */
522    @Override
523    public boolean onTouchEvent(MotionEvent event) {
524        boolean handled = false;
525        int touchX = (int) event.getX();
526        int checkRight = sCheckboxHitWidth;
527        int starLeft = mViewWidth - sFavoriteHitWidth;
528
529        switch (event.getAction()) {
530            case MotionEvent.ACTION_DOWN:
531                if (touchX < checkRight || touchX > starLeft) {
532                    mDownEvent = true;
533                    if ((touchX < checkRight) || (touchX > starLeft)) {
534                        handled = true;
535                    }
536                }
537                break;
538
539            case MotionEvent.ACTION_CANCEL:
540                mDownEvent = false;
541                break;
542
543            case MotionEvent.ACTION_UP:
544                if (mDownEvent) {
545                    if (touchX < checkRight) {
546                        mAdapter.toggleSelected(this);
547                        handled = true;
548                    } else if (touchX > starLeft) {
549                        mIsFavorite = !mIsFavorite;
550                        mAdapter.updateFavorite(this, mIsFavorite);
551                        handled = true;
552                    }
553                }
554                break;
555        }
556
557        if (handled) {
558            invalidate();
559        } else {
560            handled = super.onTouchEvent(event);
561        }
562
563        return handled;
564    }
565}
566