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