MessageListItem.java revision 937ea4fc87eabd8fe785abeb2af1a38450e3fca9
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.Align;
27import android.graphics.Paint.FontMetricsInt;
28import android.graphics.Typeface;
29import android.text.Layout.Alignment;
30import android.text.StaticLayout;
31import android.text.TextPaint;
32import android.text.TextUtils;
33import android.text.TextUtils.TruncateAt;
34import android.text.format.DateUtils;
35import android.util.AttributeSet;
36import android.view.MotionEvent;
37import android.view.View;
38
39/**
40 * This custom View is the list item for the MessageList activity, and serves two purposes:
41 * 1.  It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
42 * 2.  It handles internal clicks such as the checkbox or the favorite star
43 */
44public class MessageListItem extends View {
45    // Note: messagesAdapter directly fiddles with these fields.
46    /* package */ long mMessageId;
47    /* package */ long mMailboxId;
48    /* package */ long mAccountId;
49
50    private MessagesAdapter mAdapter;
51
52    private boolean mDownEvent;
53
54    public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
55        "com.android.email.MESSAGE_LIST_ITEMS";
56
57    public MessageListItem(Context context) {
58        super(context);
59        init(context);
60    }
61
62    public MessageListItem(Context context, AttributeSet attrs) {
63        super(context, attrs);
64        init(context);
65    }
66
67    public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
68        super(context, attrs, defStyle);
69        init(context);
70    }
71
72    // We always show two lines of subject/snippet
73    private static final int MAX_SUBJECT_SNIPPET_LINES = 2;
74    // Narrow mode shows sender/snippet and time/favorite stacked to save real estate; due to this,
75    // it is also somewhat taller
76    private static final int MODE_NARROW = 1;
77    // Wide mode shows sender, snippet, time, and favorite spread out across the screen
78    private static final int MODE_WIDE = 2;
79    // Sentinel indicating that the view needs layout
80    public static final int NEEDS_LAYOUT = -1;
81
82    private static boolean sInit = false;
83    private static final TextPaint sDefaultPaint = new TextPaint();
84    private static final TextPaint sBoldPaint = new TextPaint();
85    private static final TextPaint sDatePaint = new TextPaint();
86    private static Bitmap sAttachmentIcon;
87    private static Bitmap sInviteIcon;
88    private static Bitmap sFavoriteIconOff;
89    private static Bitmap sFavoriteIconOn;
90    private static int sFavoriteIconLeft;
91    private static Bitmap sSelectedIconOn;
92    private static Bitmap sSelectedIconOff;
93
94    public String mSender;
95    public String mSnippet;
96    public boolean mRead;
97    public long mTimestamp;
98    public boolean mHasAttachment = false;
99    public boolean mHasInvite = true;
100    public boolean mIsFavorite = false;
101
102    private int mMode = -1;
103
104    private int mViewWidth = 0;
105    private int mViewHeight = 0;
106    private int mSenderSnippetWidth;
107    private int mSnippetWidth;
108    private int mDateFaveWidth;
109
110    private static int sCheckboxHitWidth;
111    private static int sMinimumDateWidth;
112    private static int sFavoriteHitWidth;
113    private static int sPaddingVerySmall;
114    private static int sPaddingSmall;
115    private static int sPaddingMedium;
116    private static int sTextSize;
117    private static int sItemHeightWide;
118    private static int sItemHeightNarrow;
119    private static int sMinimumWidthWideMode;
120
121    public int mSnippetLineCount = NEEDS_LAYOUT;
122    private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES];
123    private CharSequence mFormattedSender;
124    private CharSequence mFormattedDate;
125
126    private void init(Context context) {
127        if (!sInit) {
128            Resources r = context.getResources();
129
130            sCheckboxHitWidth =
131                r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width);
132            sFavoriteHitWidth =
133                r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width);
134            sMinimumDateWidth =
135                r.getDimensionPixelSize(R.dimen.message_list_item_minimum_date_width);
136            sPaddingMedium =
137                r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium);
138            sPaddingSmall =
139                r.getDimensionPixelSize(R.dimen.message_list_item_padding_small);
140            sPaddingVerySmall =
141                r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small);
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            sMinimumWidthWideMode =
149                r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode);
150
151            sDefaultPaint.setTypeface(Typeface.DEFAULT);
152            sDefaultPaint.setTextSize(sTextSize);
153            sDefaultPaint.setAntiAlias(true);
154            sDatePaint.setTypeface(Typeface.DEFAULT);
155            sDatePaint.setTextSize(sTextSize - 1);
156            sDatePaint.setAntiAlias(true);
157            sDatePaint.setTextAlign(Align.RIGHT);
158            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
159            sBoldPaint.setTextSize(sTextSize);
160            sBoldPaint.setAntiAlias(true);
161            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_mms_attachment_small);
162            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_calendar_event_small);
163            sFavoriteIconOff =
164                BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_off);
165            sFavoriteIconOn =
166                BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_on);
167            sSelectedIconOff =
168                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
169            sSelectedIconOn =
170                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
171
172            sFavoriteIconLeft =
173                sFavoriteHitWidth - ((sFavoriteHitWidth - sFavoriteIconOff.getWidth()) / 2);
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        int mode = MODE_NARROW;
186        if (width > sMinimumWidthWideMode) {
187            mode = MODE_WIDE;
188        }
189        return mode;
190    }
191
192    private void calculateDrawingData() {
193        if (mMode == MODE_WIDE) {
194            mDateFaveWidth = sFavoriteHitWidth + sMinimumDateWidth;
195        } else {
196            mDateFaveWidth = sMinimumDateWidth;
197        }
198        mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth;
199
200        // In wide mode, we use 3/4 for snippet and 1/4 for sender
201        mSnippetWidth = mSenderSnippetWidth;
202        if (mMode == MODE_WIDE) {
203            mSnippetWidth = mSenderSnippetWidth * 3 / 4;
204        }
205        if (mHasAttachment) {
206            mSnippetWidth -= (sAttachmentIcon.getWidth() + sPaddingSmall);
207        }
208        if (mHasInvite) {
209            mSnippetWidth -= (sInviteIcon.getWidth() + sPaddingSmall);
210        }
211
212        // First, we create a StaticLayout with our snippet to get the line breaks
213        StaticLayout layout = new StaticLayout(mSnippet, 0, mSnippet.length(), sDefaultPaint,
214                mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
215        // Get the number of lines needed to render the whole snippet
216        mSnippetLineCount = layout.getLineCount();
217        // Go through our maximum number of lines, and save away what we'll end up displaying
218        // for those lines
219        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
220            int start = layout.getLineStart(i);
221            if (i == MAX_SUBJECT_SNIPPET_LINES - 1) {
222                // For the final line, ellipsize the text to our width
223                mSnippetLines[i] = TextUtils.ellipsize(mSnippet.substring(start), sDefaultPaint,
224                        mSnippetWidth, TruncateAt.END);
225            } else {
226                // Just extract from start to end
227                mSnippetLines[i] = mSnippet.substring(start, layout.getLineEnd(i));
228            }
229        }
230
231        // Now, format the sender for its width
232        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
233        // In wide mode, we use 1/4 of the width, otherwise, the whole width
234        int senderWidth = (mMode == MODE_WIDE) ? mSenderSnippetWidth / 4 : mSenderSnippetWidth;
235        // And get the ellipsized string for the calculated width
236        mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth - sPaddingMedium,
237                TruncateAt.END);
238        // Get a nicely formatted date string (relative to today)
239        String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
240        // And make it fit to our size
241        mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sMinimumDateWidth, TruncateAt.END);
242    }
243
244    @Override
245    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
246        mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
247        int mode = getViewMode(mViewWidth);
248        if (mode != mMode) {
249            // If the mode has changed, set the snippet line count to indicate layout required
250            mMode = mode;
251            mSnippetLineCount = NEEDS_LAYOUT;
252        }
253        mViewHeight = measureHeight(heightMeasureSpec, mMode);
254        setMeasuredDimension(mViewWidth, mViewHeight);
255    }
256
257    /**
258     * Determine the height of this view
259     *
260     * @param measureSpec A measureSpec packed into an int
261     * @param mode The current mode of this view
262     * @return The height of the view, honoring constraints from measureSpec
263     */
264    private int measureHeight(int measureSpec, int mode) {
265        int result = 0;
266        int specMode = MeasureSpec.getMode(measureSpec);
267        int specSize = MeasureSpec.getSize(measureSpec);
268
269        if (specMode == MeasureSpec.EXACTLY) {
270            // We were told how big to be
271            result = specSize;
272        } else {
273            // Measure the text
274            if (mMode == MODE_WIDE) {
275                result = sItemHeightWide;
276            } else {
277                result = sItemHeightNarrow;
278            }
279            if (specMode == MeasureSpec.AT_MOST) {
280                // Respect AT_MOST value if that was what is called for by
281                // measureSpec
282                result = Math.min(result, specSize);
283            }
284        }
285        return result;
286    }
287
288    @Override
289    protected void onDraw(Canvas canvas) {
290        if (mSnippetLineCount == NEEDS_LAYOUT) {
291            calculateDrawingData();
292        }
293        // Snippet starts at right of checkbox
294        int snippetX = sCheckboxHitWidth;
295        int snippetY;
296        int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall;
297        FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt();
298        int ascent = fontMetrics.ascent;
299        int descent = fontMetrics.descent;
300        int senderY;
301
302        if (mMode == MODE_WIDE) {
303            // In wide mode, we'll use 1/4 for sender and 3/4 for snippet
304            snippetX += mSenderSnippetWidth / 4;
305            // And center the sender and snippet
306            senderY = (mViewHeight - descent - ascent) / 2;
307            snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent;
308        } else {
309            senderY = 20;  // TODO Remove magic number
310            snippetY = senderY + lineHeight + sPaddingVerySmall;
311        }
312
313        // Draw the checkbox
314        int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2;
315        int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2;
316        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
317                checkboxLeft, checkboxTop, sDefaultPaint);
318
319        // Draw the sender name
320        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY,
321                mRead ? sDefaultPaint : sBoldPaint);
322
323        // Draw each of the snippet lines
324        for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
325            CharSequence line = mSnippetLines[i];
326            if (line != null) {
327                canvas.drawText(line, 0, line.length(), snippetX, snippetY, sDefaultPaint);
328                snippetY += lineHeight;
329            }
330        }
331
332        // Draw the attachment and invite icons, if necessary
333        int left = mSenderSnippetWidth + sCheckboxHitWidth;
334        if (mHasAttachment) {
335            left -= sAttachmentIcon.getWidth() + sPaddingSmall;
336            int iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2;
337            canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint);
338        }
339        if (mHasInvite) {
340            left -= sInviteIcon.getWidth() + sPaddingSmall;
341            int iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2;
342            canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint);
343        }
344
345        // Draw the date
346        int dateRight = mViewWidth - sPaddingMedium;
347        if (mMode == MODE_WIDE) {
348            dateRight -= sFavoriteHitWidth;
349        }
350        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), dateRight, senderY, sDatePaint);
351
352        // Draw the favorite icon
353        int faveLeft = mViewWidth - sFavoriteIconLeft;
354        int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2;
355        if (mMode == MODE_NARROW) {
356            faveTop += sPaddingMedium;
357        }
358        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop,
359                sDefaultPaint);
360    }
361
362    /**
363     * Called by the adapter at bindView() time
364     *
365     * @param adapter the adapter that creates this view
366     */
367    public void bindViewInit(MessagesAdapter adapter) {
368        mAdapter = adapter;
369    }
370
371    /**
372     * Overriding this method allows us to "catch" clicks in the checkbox or star
373     * and process them accordingly.
374     */
375    @Override
376    public boolean onTouchEvent(MotionEvent event) {
377        boolean handled = false;
378        int touchX = (int) event.getX();
379        int checkRight = sCheckboxHitWidth;
380        int starLeft = mViewWidth - sFavoriteHitWidth;
381
382        switch (event.getAction()) {
383            case MotionEvent.ACTION_DOWN:
384                if (touchX < checkRight || touchX > starLeft) {
385                    mDownEvent = true;
386                    if ((touchX < checkRight) || (touchX > starLeft)) {
387                        handled = true;
388                    }
389                }
390                break;
391
392            case MotionEvent.ACTION_CANCEL:
393                mDownEvent = false;
394                break;
395
396            case MotionEvent.ACTION_UP:
397                if (mDownEvent) {
398                    if (touchX < checkRight) {
399                        mAdapter.toggleSelected(this);
400                        handled = true;
401                    } else if (touchX > starLeft) {
402                        mIsFavorite = !mIsFavorite;
403                        mAdapter.updateFavorite(this, mIsFavorite);
404                        handled = true;
405                    }
406                }
407                break;
408        }
409
410        if (handled) {
411            invalidate();
412        } else {
413            handled = super.onTouchEvent(event);
414        }
415
416        return handled;
417    }
418}
419