MessageListItem.java revision 64ac7a6cc81ef6dc84354153b978bd5db944e8b0
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 141 // Note: these cannot be shared Drawables because they are selectors which have state. 142 private Drawable mReadSelector; 143 private Drawable mUnreadSelector; 144 private Drawable mWideReadSelector; 145 private Drawable mWideUnreadSelector; 146 147 public int mSnippetLineCount = NEEDS_LAYOUT; 148 private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES]; 149 private CharSequence mFormattedSender; 150 private CharSequence mFormattedDate; 151 152 private void init(Context context) { 153 if (!sInit) { 154 Resources r = context.getResources(); 155 sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider); 156 sCheckboxHitWidth = 157 r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width); 158 sFavoriteHitWidth = 159 r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width); 160 sFavoritePaddingRight = 161 r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right); 162 sSenderPaddingTopNarrow = 163 r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow); 164 sDateIconWidthWide = 165 r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide); 166 sDateIconWidthNarrow = 167 r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow); 168 sSenderWidth = 169 r.getDimensionPixelSize(R.dimen.message_list_item_sender_width); 170 sPaddingLarge = 171 r.getDimensionPixelSize(R.dimen.message_list_item_padding_large); 172 sPaddingMedium = 173 r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium); 174 sPaddingSmall = 175 r.getDimensionPixelSize(R.dimen.message_list_item_padding_small); 176 sPaddingVerySmall = 177 r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small); 178 sTextSize = 179 r.getDimensionPixelSize(R.dimen.message_list_item_text_size); 180 sItemHeightWide = 181 r.getDimensionPixelSize(R.dimen.message_list_item_height_wide); 182 sItemHeightNarrow = 183 r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow); 184 sMinimumWidthWideMode = 185 r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode); 186 sColorTipWidth = 187 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width); 188 sColorTipHeight = 189 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height); 190 sColorTipRightMarginOnNarrow = 191 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow); 192 sColorTipRightMarginOnWide = 193 r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide); 194 195 sDefaultPaint.setTypeface(Typeface.DEFAULT); 196 sDefaultPaint.setTextSize(sTextSize); 197 sDefaultPaint.setAntiAlias(true); 198 sDatePaint.setTypeface(Typeface.DEFAULT); 199 sDatePaint.setTextSize(sTextSize - 1); 200 sDatePaint.setAntiAlias(true); 201 sDatePaint.setTextAlign(Align.RIGHT); 202 sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD); 203 sBoldPaint.setTextSize(sTextSize); 204 sBoldPaint.setAntiAlias(true); 205 sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment); 206 sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite); 207 sFavoriteIconOff = 208 BitmapFactory.decodeResource(r, R.drawable.ic_star_none_holo_light); 209 sFavoriteIconOn = 210 BitmapFactory.decodeResource(r, R.drawable.btn_star_big_buttonless_dark_on); 211 sSelectedIconOff = 212 BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light); 213 sSelectedIconOn = 214 BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light); 215 216 sFavoriteIconWidth = sFavoriteIconOff.getWidth(); 217 sInit = true; 218 } 219 } 220 221 /** 222 * Determine the mode of this view (WIDE or NORMAL) 223 * 224 * @param width The width of the view 225 * @return The mode of the view 226 */ 227 private int getViewMode(int width) { 228 int mode = MODE_NARROW; 229 if (width > sMinimumWidthWideMode) { 230 mode = MODE_WIDE; 231 } 232 return mode; 233 } 234 235 private Drawable mCurentBackground = null; // Only used by updateBackground() 236 237 /* package */ void updateBackground() { 238 final Drawable newBackground; 239 if (mRead) { 240 if (mMode == MODE_WIDE) { 241 if (mWideReadSelector == null) { 242 mWideReadSelector = getContext().getResources() 243 .getDrawable(R.drawable.message_list_wide_read_selector); 244 } 245 newBackground = mWideReadSelector; 246 } else { 247 if (mReadSelector == null) { 248 mReadSelector = getContext().getResources() 249 .getDrawable(R.drawable.message_list_read_selector); 250 } 251 newBackground = mReadSelector; 252 } 253 } else { 254 if (mMode == MODE_WIDE) { 255 if (mWideUnreadSelector == null) { 256 mWideUnreadSelector = getContext().getResources() 257 .getDrawable(R.drawable.message_list_wide_unread_selector); 258 } 259 newBackground = mWideUnreadSelector; 260 } else { 261 if (mUnreadSelector == null) { 262 mUnreadSelector = getContext().getResources() 263 .getDrawable(R.drawable.message_list_unread_selector); 264 } 265 newBackground = mUnreadSelector; 266 } 267 } 268 if (newBackground != mCurentBackground) { 269 // setBackgroundDrawable is a heavy operation. Only call it when really needed. 270 setBackgroundDrawable(newBackground); 271 mCurentBackground = newBackground; 272 } 273 } 274 275 private void calculateDrawingData() { 276 SpannableStringBuilder ssb = new SpannableStringBuilder(); 277 boolean hasSubject = false; 278 if (!TextUtils.isEmpty(mSubject)) { 279 SpannableString ss = new SpannableString(mSubject); 280 ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(), 281 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 282 ssb.append(ss); 283 hasSubject = true; 284 } 285 if (!TextUtils.isEmpty(mSnippet)) { 286 if (hasSubject) { 287 ssb.append(sSubjectSnippetDivider); 288 } 289 ssb.append(mSnippet); 290 } 291 mText = ssb; 292 293 if (mMode == MODE_WIDE) { 294 mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide; 295 } else { 296 mDateFaveWidth = sDateIconWidthNarrow; 297 } 298 mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth; 299 300 // In wide mode, we use 3/4 for snippet and 1/4 for sender 301 mSnippetWidth = mSenderSnippetWidth; 302 if (mMode == MODE_WIDE) { 303 mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge; 304 } 305 306 // Create a StaticLayout with our snippet to get the line breaks 307 StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint, 308 mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 309 // Get the number of lines needed to render the whole snippet 310 mSnippetLineCount = layout.getLineCount(); 311 // Go through our maximum number of lines, and save away what we'll end up displaying 312 // for those lines 313 for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) { 314 int start = layout.getLineStart(i); 315 if (i == MAX_SUBJECT_SNIPPET_LINES - 1) { 316 int end = mText.length(); 317 if (start > end) continue; 318 // For the final line, ellipsize the text to our width 319 mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint, 320 mSnippetWidth, TruncateAt.END); 321 } else { 322 // Just extract from start to end 323 mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i)); 324 } 325 } 326 327 // Now, format the sender for its width 328 TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint; 329 int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth; 330 // And get the ellipsized string for the calculated width 331 mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth, TruncateAt.END); 332 // Get a nicely formatted date string (relative to today) 333 String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString(); 334 // And make it fit to our size 335 mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END); 336 } 337 338 @Override 339 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 340 if (widthMeasureSpec != 0 || mViewWidth == 0) { 341 mViewWidth = MeasureSpec.getSize(widthMeasureSpec); 342 int mode = getViewMode(mViewWidth); 343 if (mode != mMode) { 344 // If the mode has changed, set the snippet line count to indicate layout required 345 mMode = mode; 346 mSnippetLineCount = NEEDS_LAYOUT; 347 } 348 mViewHeight = measureHeight(heightMeasureSpec, mMode); 349 } 350 setMeasuredDimension(mViewWidth, mViewHeight); 351 } 352 353 /** 354 * Determine the height of this view 355 * 356 * @param measureSpec A measureSpec packed into an int 357 * @param mode The current mode of this view 358 * @return The height of the view, honoring constraints from measureSpec 359 */ 360 private int measureHeight(int measureSpec, int mode) { 361 int result = 0; 362 int specMode = MeasureSpec.getMode(measureSpec); 363 int specSize = MeasureSpec.getSize(measureSpec); 364 365 if (specMode == MeasureSpec.EXACTLY) { 366 // We were told how big to be 367 result = specSize; 368 } else { 369 // Measure the text 370 if (mMode == MODE_WIDE) { 371 result = sItemHeightWide; 372 } else { 373 result = sItemHeightNarrow; 374 } 375 if (specMode == MeasureSpec.AT_MOST) { 376 // Respect AT_MOST value if that was what is called for by 377 // measureSpec 378 result = Math.min(result, specSize); 379 } 380 } 381 return result; 382 } 383 384 @Override 385 public void draw(Canvas canvas) { 386 // Update the background, before View.draw() draws it. 387 updateBackground(); 388 super.draw(canvas); 389 } 390 391 @Override 392 protected void onDraw(Canvas canvas) { 393 if (mSnippetLineCount == NEEDS_LAYOUT) { 394 calculateDrawingData(); 395 } 396 // Snippet starts at right of checkbox 397 int snippetX = sCheckboxHitWidth; 398 int snippetY; 399 int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall; 400 FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt(); 401 int ascent = fontMetrics.ascent; 402 int descent = fontMetrics.descent; 403 int senderY; 404 405 if (mMode == MODE_WIDE) { 406 // Get the right starting point for the snippet 407 snippetX += sSenderWidth + sPaddingLarge; 408 // And center the sender and snippet 409 senderY = (mViewHeight - descent - ascent) / 2; 410 snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent; 411 } else { 412 senderY = -ascent + sSenderPaddingTopNarrow; 413 snippetY = senderY + lineHeight + sPaddingVerySmall; 414 } 415 416 // Draw the color chip 417 if (mColorChipPaint != null) { 418 final int rightMargin = (mMode == MODE_WIDE) 419 ? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow; 420 final int x = mViewWidth - rightMargin - sColorTipWidth; 421 canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint); 422 } 423 424 // Draw the checkbox 425 int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2; 426 int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2; 427 canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff, 428 checkboxLeft, checkboxTop, sDefaultPaint); 429 430 // Draw the sender name 431 canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY, 432 mRead ? sDefaultPaint : sBoldPaint); 433 434 // Draw each of the snippet lines 435 int subjectEnd = (mSubject == null) ? 0 : mSubject.length(); 436 int lineStart = 0; 437 TextPaint subjectPaint = mRead ? sDefaultPaint : sBoldPaint; 438 for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) { 439 CharSequence line = mSnippetLines[i]; 440 int drawX = snippetX; 441 if (line != null) { 442 int defaultPaintStart = 0; 443 if (lineStart <= subjectEnd) { 444 int boldPaintEnd = subjectEnd - lineStart; 445 if (boldPaintEnd > line.length()) { 446 boldPaintEnd = line.length(); 447 } 448 // From 0 to end, do in bold or default depending on the read flag 449 canvas.drawText(line, 0, boldPaintEnd, drawX, snippetY, subjectPaint); 450 defaultPaintStart = boldPaintEnd; 451 drawX += subjectPaint.measureText(line, 0, boldPaintEnd); 452 } 453 canvas.drawText(line, defaultPaintStart, line.length(), drawX, snippetY, 454 sDefaultPaint); 455 snippetY += lineHeight; 456 lineStart += line.length(); 457 } 458 } 459 460 // Draw the attachment and invite icons, if necessary 461 int datePaddingRight; 462 if (mMode == MODE_WIDE) { 463 datePaddingRight = sFavoriteHitWidth; 464 } else { 465 datePaddingRight = sPaddingLarge; 466 } 467 int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate, 468 0, mFormattedDate.length()) - sPaddingMedium; 469 470 int iconTop; 471 if (mHasAttachment) { 472 left -= sAttachmentIcon.getWidth() + sPaddingSmall; 473 if (mMode == MODE_WIDE) { 474 iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2; 475 } else { 476 iconTop = senderY - sAttachmentIcon.getHeight(); 477 } 478 canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint); 479 } 480 if (mHasInvite) { 481 left -= sInviteIcon.getWidth() + sPaddingSmall; 482 if (mMode == MODE_WIDE) { 483 iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2; 484 } else { 485 iconTop = senderY - sInviteIcon.getHeight(); 486 } 487 canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint); 488 } 489 490 // Draw the date 491 canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight, 492 senderY, sDatePaint); 493 494 // Draw the favorite icon 495 int faveLeft = mViewWidth - sFavoriteIconWidth; 496 if (mMode == MODE_WIDE) { 497 faveLeft -= sFavoritePaddingRight; 498 } else { 499 faveLeft -= sPaddingLarge; 500 } 501 int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2; 502 if (mMode == MODE_NARROW) { 503 faveTop += sSenderPaddingTopNarrow; 504 } 505 canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop, 506 sDefaultPaint); 507 } 508 509 /** 510 * Called by the adapter at bindView() time 511 * 512 * @param adapter the adapter that creates this view 513 */ 514 public void bindViewInit(MessagesAdapter adapter) { 515 mAdapter = adapter; 516 } 517 518 /** 519 * Overriding this method allows us to "catch" clicks in the checkbox or star 520 * and process them accordingly. 521 */ 522 @Override 523 public boolean onTouchEvent(MotionEvent event) { 524 boolean handled = false; 525 int touchX = (int) event.getX(); 526 int checkRight = sCheckboxHitWidth; 527 int starLeft = mViewWidth - sFavoriteHitWidth; 528 529 switch (event.getAction()) { 530 case MotionEvent.ACTION_DOWN: 531 if (touchX < checkRight || touchX > starLeft) { 532 mDownEvent = true; 533 if ((touchX < checkRight) || (touchX > starLeft)) { 534 handled = true; 535 } 536 } 537 break; 538 539 case MotionEvent.ACTION_CANCEL: 540 mDownEvent = false; 541 break; 542 543 case MotionEvent.ACTION_UP: 544 if (mDownEvent) { 545 if (touchX < checkRight) { 546 mAdapter.toggleSelected(this); 547 handled = true; 548 } else if (touchX > starLeft) { 549 mIsFavorite = !mIsFavorite; 550 mAdapter.updateFavorite(this, mIsFavorite); 551 handled = true; 552 } 553 } 554 break; 555 } 556 557 if (handled) { 558 invalidate(); 559 } else { 560 handled = super.onTouchEvent(event); 561 } 562 563 return handled; 564 } 565} 566