MessageListItem.java revision 8b2109f047d8b50edbb677a822f6ee34df8d17b8
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;
20import com.android.emailcommon.utility.TextUtilities;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
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    private MessageListItemCoordinates mCoordinates;
57    private Context mContext;
58
59    private boolean mDownEvent;
60
61    public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
62        "com.android.email.MESSAGE_LIST_ITEMS";
63
64    public MessageListItem(Context context) {
65        super(context);
66        init(context);
67    }
68
69    public MessageListItem(Context context, AttributeSet attrs) {
70        super(context, attrs);
71        init(context);
72    }
73
74    public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
75        super(context, attrs, defStyle);
76        init(context);
77    }
78
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 = MessageListItemCoordinates.NARROW_MODE;
82    // Wide mode shows sender, snippet, time, and favorite spread out across the screen
83    private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE;
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 final TextPaint sHighlightPaint = 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 Bitmap sSelectedIconOn;
97    private static Bitmap sSelectedIconOff;
98    private static String sSubjectSnippetDivider;
99
100    public String mSender;
101    public CharSequence mText;
102    public CharSequence mSnippet;
103    public String mSubject;
104    private StaticLayout mSubjectLayout;
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
118    private static int sTextSize;
119    private static int sItemHeightWide;
120    private static int sItemHeightNarrow;
121
122    // Note: these cannot be shared Drawables because they are selectors which have state.
123    private Drawable mReadSelector;
124    private Drawable mUnreadSelector;
125    private Drawable mWideReadSelector;
126    private Drawable mWideUnreadSelector;
127
128    public int mSnippetLineCount = NEEDS_LAYOUT;
129    private CharSequence mFormattedSender;
130    private CharSequence mFormattedDate;
131
132    private void init(Context context) {
133        mContext = context;
134        if (!sInit) {
135            Resources r = context.getResources();
136            sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
137            sTextSize =
138                r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
139            sItemHeightWide =
140                r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
141            sItemHeightNarrow =
142                r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
143
144            sDefaultPaint.setTypeface(Typeface.DEFAULT);
145            sDefaultPaint.setTextSize(sTextSize);
146            sDefaultPaint.setAntiAlias(true);
147            sDatePaint.setTypeface(Typeface.DEFAULT);
148            sDatePaint.setAntiAlias(true);
149            sBoldPaint.setTextSize(sTextSize);
150            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
151            sBoldPaint.setAntiAlias(true);
152            sHighlightPaint.setColor(TextUtilities.HIGHLIGHT_COLOR_INT);
153            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
154            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite);
155            sFavoriteIconOff =
156                BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light);
157            sFavoriteIconOn =
158                BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light);
159            sSelectedIconOff =
160                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
161            sSelectedIconOn =
162                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
163
164            sInit = true;
165        }
166    }
167
168    /**
169     * Determine the mode of this view (WIDE or NORMAL)
170     *
171     * @param width The width of the view
172     * @return The mode of the view
173     */
174    private int getViewMode(int width) {
175        return MessageListItemCoordinates.getMode(mContext, width);
176    }
177
178    private Drawable mCurentBackground = null; // Only used by updateBackground()
179
180    private void updateBackground() {
181        final Drawable newBackground;
182        if (mRead) {
183            if (mMode == MODE_WIDE) {
184                if (mWideReadSelector == null) {
185                    mWideReadSelector = getContext().getResources()
186                            .getDrawable(R.drawable.message_list_wide_read_selector);
187                }
188                newBackground = mWideReadSelector;
189            } else {
190                if (mReadSelector == null) {
191                    mReadSelector = getContext().getResources()
192                            .getDrawable(R.drawable.message_list_read_selector);
193                }
194                newBackground = mReadSelector;
195            }
196        } else {
197            if (mMode == MODE_WIDE) {
198                if (mWideUnreadSelector == null) {
199                    mWideUnreadSelector = getContext().getResources()
200                            .getDrawable(R.drawable.message_list_wide_unread_selector);
201                }
202                newBackground = mWideUnreadSelector;
203            } else {
204                if (mUnreadSelector == null) {
205                    mUnreadSelector = getContext().getResources()
206                            .getDrawable(R.drawable.message_list_unread_selector);
207                }
208                newBackground = mUnreadSelector;
209            }
210        }
211        if (newBackground != mCurentBackground) {
212            // setBackgroundDrawable is a heavy operation.  Only call it when really needed.
213            setBackgroundDrawable(newBackground);
214            mCurentBackground = newBackground;
215        }
216    }
217
218    private void calculateDrawingData() {
219        SpannableStringBuilder ssb = new SpannableStringBuilder();
220        boolean hasSubject = false;
221        if (!TextUtils.isEmpty(mSubject)) {
222            SpannableString ss = new SpannableString(mSubject);
223            ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
224                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
225            ssb.append(ss);
226            hasSubject = true;
227        }
228        if (!TextUtils.isEmpty(mSnippet)) {
229            if (hasSubject) {
230                ssb.append(sSubjectSnippetDivider);
231            }
232            ssb.append(mSnippet);
233        }
234        mText = ssb;
235
236        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
237        mSubjectLayout = new StaticLayout(mText, sDefaultPaint,
238                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
239        if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) {
240            // TODO: ellipsize.
241            int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
242            mSubjectLayout = new StaticLayout(mText.subSequence(0, end),
243                    sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
244        }
245
246        // Now, format the sender for its width
247        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
248        int senderWidth = mCoordinates.sendersWidth;
249        // And get the ellipsized string for the calculated width
250        if (TextUtils.isEmpty(mSender)) {
251            mFormattedSender = "";
252        } else {
253            mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth,
254                    TruncateAt.END);
255        }
256        // Get a nicely formatted date string (relative to today)
257        mFormattedDate = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
258    }
259
260    @Override
261    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
262        if (widthMeasureSpec != 0 || mViewWidth == 0) {
263            mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
264            int mode = getViewMode(mViewWidth);
265            if (mode != mMode) {
266                // If the mode has changed, set the snippet line count to indicate layout required
267                mMode = mode;
268                mSnippetLineCount = NEEDS_LAYOUT;
269            }
270            mViewHeight = measureHeight(heightMeasureSpec, mMode);
271        }
272        setMeasuredDimension(mViewWidth, mViewHeight);
273    }
274
275    /**
276     * Determine the height of this view
277     *
278     * @param measureSpec A measureSpec packed into an int
279     * @param mode The current mode of this view
280     * @return The height of the view, honoring constraints from measureSpec
281     */
282    private int measureHeight(int measureSpec, int mode) {
283        int result = 0;
284        int specMode = MeasureSpec.getMode(measureSpec);
285        int specSize = MeasureSpec.getSize(measureSpec);
286
287        if (specMode == MeasureSpec.EXACTLY) {
288            // We were told how big to be
289            result = specSize;
290        } else {
291            // Measure the text
292            if (mMode == MODE_WIDE) {
293                result = sItemHeightWide;
294            } else {
295                result = sItemHeightNarrow;
296            }
297            if (specMode == MeasureSpec.AT_MOST) {
298                // Respect AT_MOST value if that was what is called for by
299                // measureSpec
300                result = Math.min(result, specSize);
301            }
302        }
303        return result;
304    }
305
306    @Override
307    public void draw(Canvas canvas) {
308        // Update the background, before View.draw() draws it.
309        setSelected(mAdapter.isSelected(this));
310        updateBackground();
311        super.draw(canvas);
312    }
313
314    @Override
315    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
316        super.onLayout(changed, left, top, right, bottom);
317
318        mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth);
319    }
320
321    @Override
322    protected void onDraw(Canvas canvas) {
323        if (mSnippetLineCount == NEEDS_LAYOUT) {
324            calculateDrawingData();
325        }
326
327        // Draw the color chip indicating the mailbox this belongs to
328        if (mColorChipPaint != null) {
329            canvas.drawRect(
330                    mCoordinates.chipX, mCoordinates.chipY,
331                    mCoordinates.chipX + mCoordinates.chipWidth,
332                    mCoordinates.chipY + mCoordinates.chipHeight,
333                    mColorChipPaint);
334        }
335
336        // Draw the checkbox
337        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
338                mCoordinates.checkmarkX, mCoordinates.checkmarkY, sDefaultPaint);
339
340        // Draw the sender name
341        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(),
342                mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent,
343                mRead ? sDefaultPaint : sBoldPaint);
344
345        // Subject and snippet.
346        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
347        sDefaultPaint.setTypeface(Typeface.DEFAULT);
348        canvas.save();
349        canvas.translate(
350                mCoordinates.subjectX,
351                mCoordinates.subjectY);
352        mSubjectLayout.draw(canvas);
353        canvas.restore();
354
355        // Draw the date
356        sDatePaint.setTextSize(mCoordinates.dateFontSize);
357        int dateX = mCoordinates.dateXEnd
358                - (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length());
359
360        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(),
361                dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);
362
363        // Draw the favorite icon
364        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff,
365                mCoordinates.starX, mCoordinates.starY, sDefaultPaint);
366
367        // TODO: deal with the icon layouts better from the coordinate class so that this logic
368        // doesn't have to exist.
369        // Draw the attachment and invite icons, if necessary.
370        int iconsLeft = dateX;
371        if (mHasAttachment) {
372            iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
373            canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
374        }
375        if (mHasInvite) {
376            iconsLeft -= sInviteIcon.getWidth();
377            canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
378        }
379
380    }
381
382    /**
383     * Called by the adapter at bindView() time
384     *
385     * @param adapter the adapter that creates this view
386     */
387    public void bindViewInit(MessagesAdapter adapter) {
388        mAdapter = adapter;
389    }
390
391    /**
392     * Overriding this method allows us to "catch" clicks in the checkbox or star
393     * and process them accordingly.
394     */
395    @Override
396    public boolean onTouchEvent(MotionEvent event) {
397        boolean handled = false;
398        int touchX = (int) event.getX();
399        int checkRight = mCoordinates.checkmarkWidthIncludingMargins;
400        int starLeft = mViewWidth - mCoordinates.starWidthIncludingMargins;
401
402        switch (event.getAction()) {
403            case MotionEvent.ACTION_DOWN:
404                if (touchX < checkRight || touchX > starLeft) {
405                    mDownEvent = true;
406                    if ((touchX < checkRight) || (touchX > starLeft)) {
407                        handled = true;
408                    }
409                }
410                break;
411
412            case MotionEvent.ACTION_CANCEL:
413                mDownEvent = false;
414                break;
415
416            case MotionEvent.ACTION_UP:
417                if (mDownEvent) {
418                    if (touchX < checkRight) {
419                        mAdapter.toggleSelected(this);
420                        handled = true;
421                    } else if (touchX > starLeft) {
422                        mIsFavorite = !mIsFavorite;
423                        mAdapter.updateFavorite(this, mIsFavorite);
424                        handled = true;
425                    }
426                }
427                break;
428        }
429
430        if (handled) {
431            invalidate();
432        } else {
433            handled = super.onTouchEvent(event);
434        }
435
436        return handled;
437    }
438}
439