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