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