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