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