MessageListItem.java revision bd9b2ff2b31b8db24471f74d35b4f6c220a1d2e3
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    private static Drawable sReadSelector;
141    private static Drawable sUnreadSelector;
142    private static Drawable sWideReadSelector;
143    private static Drawable sWideUnreadSelector;
144
145    public int mSnippetLineCount = NEEDS_LAYOUT;
146    private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES];
147    private CharSequence mFormattedSender;
148    private CharSequence mFormattedDate;
149
150    private void init(Context context) {
151        if (!sInit) {
152            Resources r = context.getResources();
153            sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
154            sCheckboxHitWidth =
155                r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width);
156            sFavoriteHitWidth =
157                r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width);
158            sFavoritePaddingRight =
159                r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right);
160            sSenderPaddingTopNarrow =
161                r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow);
162            sDateIconWidthWide =
163                r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide);
164            sDateIconWidthNarrow =
165                r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow);
166            sSenderWidth =
167                r.getDimensionPixelSize(R.dimen.message_list_item_sender_width);
168            sPaddingLarge =
169                r.getDimensionPixelSize(R.dimen.message_list_item_padding_large);
170            sPaddingMedium =
171                r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium);
172            sPaddingSmall =
173                r.getDimensionPixelSize(R.dimen.message_list_item_padding_small);
174            sPaddingVerySmall =
175                r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small);
176            sTextSize =
177                r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
178            sItemHeightWide =
179                r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
180            sItemHeightNarrow =
181                r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
182            sMinimumWidthWideMode =
183                r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode);
184            sColorTipWidth =
185                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width);
186            sColorTipHeight =
187                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height);
188            sColorTipRightMarginOnNarrow =
189                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow);
190            sColorTipRightMarginOnWide =
191                r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide);
192
193            sDefaultPaint.setTypeface(Typeface.DEFAULT);
194            sDefaultPaint.setTextSize(sTextSize);
195            sDefaultPaint.setAntiAlias(true);
196            sDatePaint.setTypeface(Typeface.DEFAULT);
197            sDatePaint.setTextSize(sTextSize - 1);
198            sDatePaint.setAntiAlias(true);
199            sDatePaint.setTextAlign(Align.RIGHT);
200            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
201            sBoldPaint.setTextSize(sTextSize);
202            sBoldPaint.setAntiAlias(true);
203            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_mms_attachment_small);
204            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_calendar_event_small);
205            sFavoriteIconOff =
206                BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_off);
207            sFavoriteIconOn =
208                BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_on);
209            sSelectedIconOff =
210                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
211            sSelectedIconOn =
212                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
213            sReadSelector = r.getDrawable(R.drawable.message_list_read_selector);
214            sUnreadSelector = r.getDrawable(R.drawable.message_list_unread_selector);
215            sWideReadSelector = r.getDrawable(R.drawable.message_list_wide_read_selector);
216            sWideUnreadSelector = r.getDrawable(R.drawable.message_list_wide_unread_selector);
217
218            sFavoriteIconWidth = sFavoriteIconOff.getWidth();
219            sInit = true;
220        }
221    }
222
223    /**
224     * Determine the mode of this view (WIDE or NORMAL)
225     *
226     * @param width The width of the view
227     * @return The mode of the view
228     */
229    private int getViewMode(int width) {
230        int mode = MODE_NARROW;
231        if (width > sMinimumWidthWideMode) {
232            mode = MODE_WIDE;
233        }
234        return mode;
235    }
236
237    private void calculateDrawingData() {
238        if (mRead) {
239            if (mMode == MODE_WIDE) {
240                setBackgroundDrawable(sWideReadSelector);
241            } else {
242                setBackgroundDrawable(sReadSelector);
243            }
244        } else {
245            if (mMode == MODE_WIDE) {
246                setBackgroundDrawable(sWideUnreadSelector);
247            } else {
248                setBackgroundDrawable(sUnreadSelector);
249            }
250        }
251
252        SpannableStringBuilder ssb = new SpannableStringBuilder();
253        boolean hasSubject = false;
254        if (!TextUtils.isEmpty(mSubject)) {
255            SpannableString ss = new SpannableString(mSubject);
256            ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
257                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
258            ssb.append(ss);
259            hasSubject = true;
260        }
261        if (!TextUtils.isEmpty(mSnippet)) {
262            if (hasSubject) {
263                ssb.append(sSubjectSnippetDivider);
264            }
265            ssb.append(mSnippet);
266        }
267        mText = ssb;
268
269        if (mMode == MODE_WIDE) {
270            mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide;
271        } else {
272            mDateFaveWidth = sDateIconWidthNarrow;
273        }
274        mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth;
275
276        // In wide mode, we use 3/4 for snippet and 1/4 for sender
277        mSnippetWidth = mSenderSnippetWidth;
278        if (mMode == MODE_WIDE) {
279            mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge;
280        }
281
282        // Create a StaticLayout with our snippet to get the line breaks
283        StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint,
284                mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
285        // Get the number of lines needed to render the whole snippet
286        mSnippetLineCount = layout.getLineCount();
287        // Go through our maximum number of lines, and save away what we'll end up displaying
288        // for those lines
289        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
290            int start = layout.getLineStart(i);
291            if (i == MAX_SUBJECT_SNIPPET_LINES - 1) {
292                int end = mText.length();
293                if (start > end) continue;
294                // For the final line, ellipsize the text to our width
295                mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint,
296                        mSnippetWidth, TruncateAt.END);
297            } else {
298                // Just extract from start to end
299                mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i));
300            }
301        }
302
303        // Now, format the sender for its width
304        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
305        int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth;
306        // And get the ellipsized string for the calculated width
307        mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth, TruncateAt.END);
308        // Get a nicely formatted date string (relative to today)
309        String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
310        // And make it fit to our size
311        mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END);
312    }
313
314    @Override
315    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
316        if (widthMeasureSpec != 0 || mViewWidth == 0) {
317            mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
318            int mode = getViewMode(mViewWidth);
319            if (mode != mMode) {
320                // If the mode has changed, set the snippet line count to indicate layout required
321                mMode = mode;
322                mSnippetLineCount = NEEDS_LAYOUT;
323            }
324            mViewHeight = measureHeight(heightMeasureSpec, mMode);
325        }
326        setMeasuredDimension(mViewWidth, mViewHeight);
327    }
328
329    /**
330     * Determine the height of this view
331     *
332     * @param measureSpec A measureSpec packed into an int
333     * @param mode The current mode of this view
334     * @return The height of the view, honoring constraints from measureSpec
335     */
336    private int measureHeight(int measureSpec, int mode) {
337        int result = 0;
338        int specMode = MeasureSpec.getMode(measureSpec);
339        int specSize = MeasureSpec.getSize(measureSpec);
340
341        if (specMode == MeasureSpec.EXACTLY) {
342            // We were told how big to be
343            result = specSize;
344        } else {
345            // Measure the text
346            if (mMode == MODE_WIDE) {
347                result = sItemHeightWide;
348            } else {
349                result = sItemHeightNarrow;
350            }
351            if (specMode == MeasureSpec.AT_MOST) {
352                // Respect AT_MOST value if that was what is called for by
353                // measureSpec
354                result = Math.min(result, specSize);
355            }
356        }
357        return result;
358    }
359
360    @Override
361    protected void onDraw(Canvas canvas) {
362        if (mSnippetLineCount == NEEDS_LAYOUT) {
363            calculateDrawingData();
364        }
365        // Snippet starts at right of checkbox
366        int snippetX = sCheckboxHitWidth;
367        int snippetY;
368        int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall;
369        FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt();
370        int ascent = fontMetrics.ascent;
371        int descent = fontMetrics.descent;
372        int senderY;
373
374        if (mMode == MODE_WIDE) {
375            // Get the right starting point for the snippet
376            snippetX += sSenderWidth + sPaddingLarge;
377            // And center the sender and snippet
378            senderY = (mViewHeight - descent - ascent) / 2;
379            snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent;
380        } else {
381            senderY = -ascent + sSenderPaddingTopNarrow;
382            snippetY = senderY + lineHeight + sPaddingVerySmall;
383        }
384
385        // Draw the color chip
386        if (mColorChipPaint != null) {
387            final int rightMargin = (mMode == MODE_WIDE)
388                    ? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow;
389            final int x = mViewWidth - rightMargin - sColorTipWidth;
390            canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint);
391        }
392
393        // Draw the checkbox
394        int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2;
395        int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2;
396        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
397                checkboxLeft, checkboxTop, sDefaultPaint);
398
399        // Draw the sender name
400        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY,
401                mRead ? sDefaultPaint : sBoldPaint);
402
403        // Draw each of the snippet lines
404        int subjectEnd = (mSubject == null) ? 0 : mSubject.length();
405        int lineStart = 0;
406        TextPaint subjectPaint = mRead ? sDefaultPaint : sBoldPaint;
407        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) {
408            CharSequence line = mSnippetLines[i];
409            int drawX = snippetX;
410            if (line != null) {
411                int defaultPaintStart = 0;
412                if (lineStart <= subjectEnd) {
413                    int boldPaintEnd = subjectEnd - lineStart;
414                    if (boldPaintEnd > line.length()) {
415                        boldPaintEnd = line.length();
416                    }
417                    // From 0 to end, do in bold or default depending on the read flag
418                    canvas.drawText(line, 0, boldPaintEnd, drawX, snippetY, subjectPaint);
419                    defaultPaintStart = boldPaintEnd;
420                    drawX += subjectPaint.measureText(line, 0, boldPaintEnd);
421                }
422                canvas.drawText(line, defaultPaintStart, line.length(), drawX, snippetY,
423                        sDefaultPaint);
424                snippetY += lineHeight;
425                lineStart += line.length();
426            }
427        }
428
429        // Draw the attachment and invite icons, if necessary
430        int datePaddingRight;
431        if (mMode == MODE_WIDE) {
432            datePaddingRight = sFavoriteHitWidth;
433        } else {
434            datePaddingRight = sPaddingLarge;
435        }
436        int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate,
437                0, mFormattedDate.length()) - sPaddingMedium;
438
439        int iconTop;
440        if (mHasAttachment) {
441            left -= sAttachmentIcon.getWidth() + sPaddingSmall;
442            if (mMode == MODE_WIDE) {
443                iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2;
444            } else {
445                iconTop = senderY - sAttachmentIcon.getHeight();
446            }
447            canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint);
448        }
449        if (mHasInvite) {
450            left -= sInviteIcon.getWidth() + sPaddingSmall;
451            if (mMode == MODE_WIDE) {
452                iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2;
453            } else {
454                iconTop = senderY - sInviteIcon.getHeight();
455            }
456            canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint);
457        }
458
459        // Draw the date
460        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight,
461                senderY, sDatePaint);
462
463        // Draw the favorite icon
464        int faveLeft = mViewWidth - sFavoriteIconWidth;
465        if (mMode == MODE_WIDE) {
466            faveLeft -= sFavoritePaddingRight;
467        } else {
468            faveLeft -= sPaddingLarge;
469        }
470        int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2;
471        if (mMode == MODE_NARROW) {
472            faveTop += sSenderPaddingTopNarrow;
473        }
474        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop,
475                sDefaultPaint);
476    }
477
478    /**
479     * Called by the adapter at bindView() time
480     *
481     * @param adapter the adapter that creates this view
482     */
483    public void bindViewInit(MessagesAdapter adapter) {
484        mAdapter = adapter;
485    }
486
487    /**
488     * Overriding this method allows us to "catch" clicks in the checkbox or star
489     * and process them accordingly.
490     */
491    @Override
492    public boolean onTouchEvent(MotionEvent event) {
493        boolean handled = false;
494        int touchX = (int) event.getX();
495        int checkRight = sCheckboxHitWidth;
496        int starLeft = mViewWidth - sFavoriteHitWidth;
497
498        switch (event.getAction()) {
499            case MotionEvent.ACTION_DOWN:
500                if (touchX < checkRight || touchX > starLeft) {
501                    mDownEvent = true;
502                    if ((touchX < checkRight) || (touchX > starLeft)) {
503                        handled = true;
504                    }
505                }
506                break;
507
508            case MotionEvent.ACTION_CANCEL:
509                mDownEvent = false;
510                break;
511
512            case MotionEvent.ACTION_UP:
513                if (mDownEvent) {
514                    if (touchX < checkRight) {
515                        mAdapter.toggleSelected(this);
516                        handled = true;
517                    } else if (touchX > starLeft) {
518                        mIsFavorite = !mIsFavorite;
519                        mAdapter.updateFavorite(this, mIsFavorite);
520                        handled = true;
521                    }
522                }
523                break;
524        }
525
526        if (handled) {
527            invalidate();
528        } else {
529            handled = super.onTouchEvent(event);
530        }
531
532        return handled;
533    }
534}
535