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