MessageListItem.java revision bd9b2ff2b31b8db24471f74d35b4f6c220a1d2e3
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; 27import android.graphics.Paint.Align; 28import android.graphics.Paint.FontMetricsInt; 29import android.graphics.Typeface; 30import android.graphics.drawable.Drawable; 31import android.text.Layout.Alignment; 32import android.text.Spannable; 33import android.text.SpannableString; 34import android.text.SpannableStringBuilder; 35import android.text.StaticLayout; 36import android.text.TextPaint; 37import android.text.TextUtils; 38import android.text.TextUtils.TruncateAt; 39import android.text.format.DateUtils; 40import android.text.style.StyleSpan; 41import android.util.AttributeSet; 42import android.view.MotionEvent; 43import android.view.View; 44 45/** 46 * This custom View is the list item for the MessageList activity, and serves two purposes: 47 * 1. It's a container to store message metadata (e.g. the ids of the message, mailbox, & account) 48 * 2. It handles internal clicks such as the checkbox or the favorite star 49 */ 50public class MessageListItem extends View { 51 // Note: messagesAdapter directly fiddles with these fields. 52 /* package */ long mMessageId; 53 /* package */ long mMailboxId; 54 /* package */ long mAccountId; 55 56 private MessagesAdapter mAdapter; 57 58 private boolean mDownEvent; 59 60 public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL = 61 "com.android.email.MESSAGE_LIST_ITEMS"; 62 63 public MessageListItem(Context context) { 64 super(context); 65 init(context); 66 } 67 68 public MessageListItem(Context context, AttributeSet attrs) { 69 super(context, attrs); 70 init(context); 71 } 72 73 public MessageListItem(Context context, AttributeSet attrs, int defStyle) { 74 super(context, attrs, defStyle); 75 init(context); 76 } 77 78 // We always show two lines of subject/snippet 79 private static final int MAX_SUBJECT_SNIPPET_LINES = 2; 80 // Narrow mode shows sender/snippet and time/favorite stacked to save real estate; due to this, 81 // it is also somewhat taller 82 private static final int MODE_NARROW = 1; 83 // Wide mode shows sender, snippet, time, and favorite spread out across the screen 84 private static final int MODE_WIDE = 2; 85 // Sentinel indicating that the view needs layout 86 public static final int NEEDS_LAYOUT = -1; 87 88 private static boolean sInit = false; 89 private static final TextPaint sDefaultPaint = new TextPaint(); 90 private static final TextPaint sBoldPaint = new TextPaint(); 91 private static final TextPaint sDatePaint = 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 int sFavoriteIconWidth; 97 private static Bitmap sSelectedIconOn; 98 private static Bitmap sSelectedIconOff; 99 private static String sSubjectSnippetDivider; 100 101 public String mSender; 102 public CharSequence mText; 103 public String mSnippet; 104 public String mSubject; 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 private int mSenderSnippetWidth; 118 private int mSnippetWidth; 119 private int mDateFaveWidth; 120 121 private static int sCheckboxHitWidth; 122 private static int sDateIconWidthWide; 123 private static int sDateIconWidthNarrow; 124 private static int sFavoriteHitWidth; 125 private static int sFavoritePaddingRight; 126 private static int sSenderPaddingTopNarrow; 127 private static int sSenderWidth; 128 private static int sPaddingLarge; 129 private static int sPaddingVerySmall; 130 private static int sPaddingSmall; 131 private static int sPaddingMedium; 132 private static int sTextSize; 133 private static int sItemHeightWide; 134 private static int sItemHeightNarrow; 135 private static int sMinimumWidthWideMode; 136 private static int sColorTipWidth; 137 private static int sColorTipHeight; 138 private static int sColorTipRightMarginOnNarrow; 139 private static int sColorTipRightMarginOnWide; 140 private static Drawable sReadSelector; 141 private static Drawable sUnreadSelector; 142 private static Drawable sWideReadSelector; 143 private static Drawable sWideUnreadSelector; 144 145 public int mSnippetLineCount = NEEDS_LAYOUT; 146 private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES]; 147 private CharSequence mFormattedSender; 148 private CharSequence mFormattedDate; 149 150 private void init(Context context) { 151 if (!sInit) { 152 Resources r = context.getResources(); 153 sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider); 154 sCheckboxHitWidth = 155 r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width); 156 sFavoriteHitWidth = 157 r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width); 158 sFavoritePaddingRight = 159 r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right); 160 sSenderPaddingTopNarrow = 161 r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow); 162 sDateIconWidthWide = 163 r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide); 164 sDateIconWidthNarrow = 165 r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow); 166 sSenderWidth = 167 r.getDimensionPixelSize(R.dimen.message_list_item_sender_width); 168 sPaddingLarge = 169 r.getDimensionPixelSize(R.dimen.message_list_item_padding_large); 170 sPaddingMedium = 171 r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium); 172 sPaddingSmall = 173 r.getDimensionPixelSize(R.dimen.message_list_item_padding_small); 174 sPaddingVerySmall = 175 r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small); 176 sTextSize = 177 r.getDimensionPixelSize(R.dimen.message_list_item_text_size); 178 sItemHeightWide = 179 r.getDimensionPixelSize(R.dimen.message_list_item_height_wide); 180 sItemHeightNarrow = 181 r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow); 182 sMinimumWidthWideMode = 183 r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode); 184 sColorTipWidth = 185 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width); 186 sColorTipHeight = 187 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height); 188 sColorTipRightMarginOnNarrow = 189 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow); 190 sColorTipRightMarginOnWide = 191 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide); 192 193 sDefaultPaint.setTypeface(Typeface.DEFAULT); 194 sDefaultPaint.setTextSize(sTextSize); 195 sDefaultPaint.setAntiAlias(true); 196 sDatePaint.setTypeface(Typeface.DEFAULT); 197 sDatePaint.setTextSize(sTextSize - 1); 198 sDatePaint.setAntiAlias(true); 199 sDatePaint.setTextAlign(Align.RIGHT); 200 sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD); 201 sBoldPaint.setTextSize(sTextSize); 202 sBoldPaint.setAntiAlias(true); 203 sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_mms_attachment_small); 204 sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_calendar_event_small); 205 sFavoriteIconOff = 206 BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_off); 207 sFavoriteIconOn = 208 BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_on); 209 sSelectedIconOff = 210 BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light); 211 sSelectedIconOn = 212 BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light); 213 sReadSelector = r.getDrawable(R.drawable.message_list_read_selector); 214 sUnreadSelector = r.getDrawable(R.drawable.message_list_unread_selector); 215 sWideReadSelector = r.getDrawable(R.drawable.message_list_wide_read_selector); 216 sWideUnreadSelector = r.getDrawable(R.drawable.message_list_wide_unread_selector); 217 218 sFavoriteIconWidth = sFavoriteIconOff.getWidth(); 219 sInit = true; 220 } 221 } 222 223 /** 224 * Determine the mode of this view (WIDE or NORMAL) 225 * 226 * @param width The width of the view 227 * @return The mode of the view 228 */ 229 private int getViewMode(int width) { 230 int mode = MODE_NARROW; 231 if (width > sMinimumWidthWideMode) { 232 mode = MODE_WIDE; 233 } 234 return mode; 235 } 236 237 private void calculateDrawingData() { 238 if (mRead) { 239 if (mMode == MODE_WIDE) { 240 setBackgroundDrawable(sWideReadSelector); 241 } else { 242 setBackgroundDrawable(sReadSelector); 243 } 244 } else { 245 if (mMode == MODE_WIDE) { 246 setBackgroundDrawable(sWideUnreadSelector); 247 } else { 248 setBackgroundDrawable(sUnreadSelector); 249 } 250 } 251 252 SpannableStringBuilder ssb = new SpannableStringBuilder(); 253 boolean hasSubject = false; 254 if (!TextUtils.isEmpty(mSubject)) { 255 SpannableString ss = new SpannableString(mSubject); 256 ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(), 257 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 258 ssb.append(ss); 259 hasSubject = true; 260 } 261 if (!TextUtils.isEmpty(mSnippet)) { 262 if (hasSubject) { 263 ssb.append(sSubjectSnippetDivider); 264 } 265 ssb.append(mSnippet); 266 } 267 mText = ssb; 268 269 if (mMode == MODE_WIDE) { 270 mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide; 271 } else { 272 mDateFaveWidth = sDateIconWidthNarrow; 273 } 274 mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth; 275 276 // In wide mode, we use 3/4 for snippet and 1/4 for sender 277 mSnippetWidth = mSenderSnippetWidth; 278 if (mMode == MODE_WIDE) { 279 mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge; 280 } 281 282 // Create a StaticLayout with our snippet to get the line breaks 283 StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint, 284 mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 285 // Get the number of lines needed to render the whole snippet 286 mSnippetLineCount = layout.getLineCount(); 287 // Go through our maximum number of lines, and save away what we'll end up displaying 288 // for those lines 289 for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) { 290 int start = layout.getLineStart(i); 291 if (i == MAX_SUBJECT_SNIPPET_LINES - 1) { 292 int end = mText.length(); 293 if (start > end) continue; 294 // For the final line, ellipsize the text to our width 295 mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint, 296 mSnippetWidth, TruncateAt.END); 297 } else { 298 // Just extract from start to end 299 mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i)); 300 } 301 } 302 303 // Now, format the sender for its width 304 TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint; 305 int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth; 306 // And get the ellipsized string for the calculated width 307 mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth, TruncateAt.END); 308 // Get a nicely formatted date string (relative to today) 309 String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString(); 310 // And make it fit to our size 311 mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END); 312 } 313 314 @Override 315 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 316 if (widthMeasureSpec != 0 || mViewWidth == 0) { 317 mViewWidth = MeasureSpec.getSize(widthMeasureSpec); 318 int mode = getViewMode(mViewWidth); 319 if (mode != mMode) { 320 // If the mode has changed, set the snippet line count to indicate layout required 321 mMode = mode; 322 mSnippetLineCount = NEEDS_LAYOUT; 323 } 324 mViewHeight = measureHeight(heightMeasureSpec, mMode); 325 } 326 setMeasuredDimension(mViewWidth, mViewHeight); 327 } 328 329 /** 330 * Determine the height of this view 331 * 332 * @param measureSpec A measureSpec packed into an int 333 * @param mode The current mode of this view 334 * @return The height of the view, honoring constraints from measureSpec 335 */ 336 private int measureHeight(int measureSpec, int mode) { 337 int result = 0; 338 int specMode = MeasureSpec.getMode(measureSpec); 339 int specSize = MeasureSpec.getSize(measureSpec); 340 341 if (specMode == MeasureSpec.EXACTLY) { 342 // We were told how big to be 343 result = specSize; 344 } else { 345 // Measure the text 346 if (mMode == MODE_WIDE) { 347 result = sItemHeightWide; 348 } else { 349 result = sItemHeightNarrow; 350 } 351 if (specMode == MeasureSpec.AT_MOST) { 352 // Respect AT_MOST value if that was what is called for by 353 // measureSpec 354 result = Math.min(result, specSize); 355 } 356 } 357 return result; 358 } 359 360 @Override 361 protected void onDraw(Canvas canvas) { 362 if (mSnippetLineCount == NEEDS_LAYOUT) { 363 calculateDrawingData(); 364 } 365 // Snippet starts at right of checkbox 366 int snippetX = sCheckboxHitWidth; 367 int snippetY; 368 int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall; 369 FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt(); 370 int ascent = fontMetrics.ascent; 371 int descent = fontMetrics.descent; 372 int senderY; 373 374 if (mMode == MODE_WIDE) { 375 // Get the right starting point for the snippet 376 snippetX += sSenderWidth + sPaddingLarge; 377 // And center the sender and snippet 378 senderY = (mViewHeight - descent - ascent) / 2; 379 snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent; 380 } else { 381 senderY = -ascent + sSenderPaddingTopNarrow; 382 snippetY = senderY + lineHeight + sPaddingVerySmall; 383 } 384 385 // Draw the color chip 386 if (mColorChipPaint != null) { 387 final int rightMargin = (mMode == MODE_WIDE) 388 ? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow; 389 final int x = mViewWidth - rightMargin - sColorTipWidth; 390 canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint); 391 } 392 393 // Draw the checkbox 394 int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2; 395 int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2; 396 canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff, 397 checkboxLeft, checkboxTop, sDefaultPaint); 398 399 // Draw the sender name 400 canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY, 401 mRead ? sDefaultPaint : sBoldPaint); 402 403 // Draw each of the snippet lines 404 int subjectEnd = (mSubject == null) ? 0 : mSubject.length(); 405 int lineStart = 0; 406 TextPaint subjectPaint = mRead ? sDefaultPaint : sBoldPaint; 407 for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) { 408 CharSequence line = mSnippetLines[i]; 409 int drawX = snippetX; 410 if (line != null) { 411 int defaultPaintStart = 0; 412 if (lineStart <= subjectEnd) { 413 int boldPaintEnd = subjectEnd - lineStart; 414 if (boldPaintEnd > line.length()) { 415 boldPaintEnd = line.length(); 416 } 417 // From 0 to end, do in bold or default depending on the read flag 418 canvas.drawText(line, 0, boldPaintEnd, drawX, snippetY, subjectPaint); 419 defaultPaintStart = boldPaintEnd; 420 drawX += subjectPaint.measureText(line, 0, boldPaintEnd); 421 } 422 canvas.drawText(line, defaultPaintStart, line.length(), drawX, snippetY, 423 sDefaultPaint); 424 snippetY += lineHeight; 425 lineStart += line.length(); 426 } 427 } 428 429 // Draw the attachment and invite icons, if necessary 430 int datePaddingRight; 431 if (mMode == MODE_WIDE) { 432 datePaddingRight = sFavoriteHitWidth; 433 } else { 434 datePaddingRight = sPaddingLarge; 435 } 436 int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate, 437 0, mFormattedDate.length()) - sPaddingMedium; 438 439 int iconTop; 440 if (mHasAttachment) { 441 left -= sAttachmentIcon.getWidth() + sPaddingSmall; 442 if (mMode == MODE_WIDE) { 443 iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2; 444 } else { 445 iconTop = senderY - sAttachmentIcon.getHeight(); 446 } 447 canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint); 448 } 449 if (mHasInvite) { 450 left -= sInviteIcon.getWidth() + sPaddingSmall; 451 if (mMode == MODE_WIDE) { 452 iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2; 453 } else { 454 iconTop = senderY - sInviteIcon.getHeight(); 455 } 456 canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint); 457 } 458 459 // Draw the date 460 canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight, 461 senderY, sDatePaint); 462 463 // Draw the favorite icon 464 int faveLeft = mViewWidth - sFavoriteIconWidth; 465 if (mMode == MODE_WIDE) { 466 faveLeft -= sFavoritePaddingRight; 467 } else { 468 faveLeft -= sPaddingLarge; 469 } 470 int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2; 471 if (mMode == MODE_NARROW) { 472 faveTop += sSenderPaddingTopNarrow; 473 } 474 canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop, 475 sDefaultPaint); 476 } 477 478 /** 479 * Called by the adapter at bindView() time 480 * 481 * @param adapter the adapter that creates this view 482 */ 483 public void bindViewInit(MessagesAdapter adapter) { 484 mAdapter = adapter; 485 } 486 487 /** 488 * Overriding this method allows us to "catch" clicks in the checkbox or star 489 * and process them accordingly. 490 */ 491 @Override 492 public boolean onTouchEvent(MotionEvent event) { 493 boolean handled = false; 494 int touchX = (int) event.getX(); 495 int checkRight = sCheckboxHitWidth; 496 int starLeft = mViewWidth - sFavoriteHitWidth; 497 498 switch (event.getAction()) { 499 case MotionEvent.ACTION_DOWN: 500 if (touchX < checkRight || touchX > starLeft) { 501 mDownEvent = true; 502 if ((touchX < checkRight) || (touchX > starLeft)) { 503 handled = true; 504 } 505 } 506 break; 507 508 case MotionEvent.ACTION_CANCEL: 509 mDownEvent = false; 510 break; 511 512 case MotionEvent.ACTION_UP: 513 if (mDownEvent) { 514 if (touchX < checkRight) { 515 mAdapter.toggleSelected(this); 516 handled = true; 517 } else if (touchX > starLeft) { 518 mIsFavorite = !mIsFavorite; 519 mAdapter.updateFavorite(this, mIsFavorite); 520 handled = true; 521 } 522 } 523 break; 524 } 525 526 if (handled) { 527 invalidate(); 528 } else { 529 handled = super.onTouchEvent(event); 530 } 531 532 return handled; 533 } 534} 535