MessageListItem.java revision 8e779e627a2185320ab168fdce09477a73bf5f22
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; 20import com.android.emailcommon.utility.TextUtilities; 21 22import android.content.Context; 23import android.content.res.Resources; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.Typeface; 29import android.graphics.drawable.Drawable; 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; 43import android.view.accessibility.AccessibilityEvent; 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 private MessageListItemCoordinates mCoordinates; 58 private Context mContext; 59 60 private boolean mDownEvent; 61 62 public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL = 63 "com.android.email.MESSAGE_LIST_ITEMS"; 64 65 public MessageListItem(Context context) { 66 super(context); 67 init(context); 68 } 69 70 public MessageListItem(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 init(context); 73 } 74 75 public MessageListItem(Context context, AttributeSet attrs, int defStyle) { 76 super(context, attrs, defStyle); 77 init(context); 78 } 79 80 // Wide mode shows sender, snippet, time, and favorite spread out across the screen 81 private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE; 82 // Sentinel indicating that the view needs layout 83 public static final int NEEDS_LAYOUT = -1; 84 85 private static boolean sInit = false; 86 private static final TextPaint sDefaultPaint = new TextPaint(); 87 private static final TextPaint sBoldPaint = new TextPaint(); 88 private static final TextPaint sDatePaint = new TextPaint(); 89 private static final TextPaint sHighlightPaint = new TextPaint(); 90 private static Bitmap sAttachmentIcon; 91 private static Bitmap sInviteIcon; 92 private static Bitmap sFavoriteIconOff; 93 private static Bitmap sFavoriteIconOn; 94 private static Bitmap sSelectedIconOn; 95 private static Bitmap sSelectedIconOff; 96 private static Bitmap sStateReplied; 97 private static Bitmap sStateForwarded; 98 private static Bitmap sStateRepliedAndForwarded; 99 private static String sSubjectSnippetDivider; 100 private static String sSubjectDescription; 101 private static String sSubjectEmptyDescription; 102 103 public String mSender; 104 public CharSequence mText; 105 public CharSequence mSnippet; 106 private String mSubject; 107 private String mSubjectAndDescription; 108 private StaticLayout mSubjectLayout; 109 public boolean mRead; 110 public long mTimestamp; 111 public boolean mHasAttachment = false; 112 public boolean mHasInvite = true; 113 public boolean mIsFavorite = false; 114 public boolean mHasBeenRepliedTo = false; 115 public boolean mHasBeenForwarded = false; 116 /** {@link Paint} for account color chips. null if no chips should be drawn. */ 117 public Paint mColorChipPaint; 118 119 private int mMode = -1; 120 121 private int mViewWidth = 0; 122 private int mViewHeight = 0; 123 124 private static int sTextSize; 125 private static int sItemHeightWide; 126 private static int sItemHeightNarrow; 127 128 // Note: these cannot be shared Drawables because they are selectors which have state. 129 private Drawable mReadSelector; 130 private Drawable mUnreadSelector; 131 private Drawable mWideReadSelector; 132 private Drawable mWideUnreadSelector; 133 134 public int mSnippetLineCount = NEEDS_LAYOUT; 135 private CharSequence mFormattedSender; 136 private CharSequence mFormattedDate; 137 138 private void init(Context context) { 139 mContext = context; 140 if (!sInit) { 141 Resources r = context.getResources(); 142 sSubjectDescription = r.getString(R.string.message_subject_description).concat(", "); 143 sSubjectEmptyDescription = r.getString(R.string.message_is_empty_description); 144 sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider); 145 sTextSize = 146 r.getDimensionPixelSize(R.dimen.message_list_item_text_size); 147 sItemHeightWide = 148 r.getDimensionPixelSize(R.dimen.message_list_item_height_wide); 149 sItemHeightNarrow = 150 r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow); 151 152 sDefaultPaint.setTypeface(Typeface.DEFAULT); 153 sDefaultPaint.setTextSize(sTextSize); 154 sDefaultPaint.setAntiAlias(true); 155 sDatePaint.setTypeface(Typeface.DEFAULT); 156 sDatePaint.setAntiAlias(true); 157 sBoldPaint.setTextSize(sTextSize); 158 sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD); 159 sBoldPaint.setAntiAlias(true); 160 sHighlightPaint.setColor(TextUtilities.HIGHLIGHT_COLOR_INT); 161 sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment); 162 sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite); 163 sFavoriteIconOff = 164 BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light); 165 sFavoriteIconOn = 166 BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light); 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 //TODO: put the actual drawables when they exist. these are temps for visibile testing. 173 sStateReplied = 174 BitmapFactory.decodeResource(r, R.drawable.reply); 175 sStateForwarded = 176 BitmapFactory.decodeResource(r, R.drawable.forward); 177 sStateRepliedAndForwarded = 178 BitmapFactory.decodeResource(r, R.drawable.reply_all); 179 180 sInit = true; 181 } 182 } 183 184 /** 185 * Sets message subject safely, ensuring the cache is invalidated. 186 */ 187 public void setSubject(String subject) { 188 if (!subject.equals(mSubject)) { 189 mSubject = subject; 190 mSubjectAndDescription = null; 191 } 192 } 193 194 /** 195 * Determine the mode of this view (WIDE or NORMAL) 196 * 197 * @param width The width of the view 198 * @return The mode of the view 199 */ 200 private int getViewMode(int width) { 201 return MessageListItemCoordinates.getMode(mContext, width); 202 } 203 204 private Drawable mCurentBackground = null; // Only used by updateBackground() 205 206 private void updateBackground() { 207 final Drawable newBackground; 208 if (mRead) { 209 if (mMode == MODE_WIDE) { 210 if (mWideReadSelector == null) { 211 mWideReadSelector = getContext().getResources() 212 .getDrawable(R.drawable.message_list_wide_read_selector); 213 } 214 newBackground = mWideReadSelector; 215 } else { 216 if (mReadSelector == null) { 217 mReadSelector = getContext().getResources() 218 .getDrawable(R.drawable.message_list_read_selector); 219 } 220 newBackground = mReadSelector; 221 } 222 } else { 223 if (mMode == MODE_WIDE) { 224 if (mWideUnreadSelector == null) { 225 mWideUnreadSelector = getContext().getResources() 226 .getDrawable(R.drawable.message_list_wide_unread_selector); 227 } 228 newBackground = mWideUnreadSelector; 229 } else { 230 if (mUnreadSelector == null) { 231 mUnreadSelector = getContext().getResources() 232 .getDrawable(R.drawable.message_list_unread_selector); 233 } 234 newBackground = mUnreadSelector; 235 } 236 } 237 if (newBackground != mCurentBackground) { 238 // setBackgroundDrawable is a heavy operation. Only call it when really needed. 239 setBackgroundDrawable(newBackground); 240 mCurentBackground = newBackground; 241 } 242 } 243 244 private void calculateDrawingData() { 245 SpannableStringBuilder ssb = new SpannableStringBuilder(); 246 boolean hasSubject = false; 247 if (!TextUtils.isEmpty(mSubject)) { 248 SpannableString ss = new SpannableString(mSubject); 249 ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(), 250 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 251 ssb.append(ss); 252 hasSubject = true; 253 } 254 if (!TextUtils.isEmpty(mSnippet)) { 255 if (hasSubject) { 256 ssb.append(sSubjectSnippetDivider); 257 } 258 ssb.append(mSnippet); 259 } 260 mText = ssb; 261 262 sDefaultPaint.setTextSize(mCoordinates.subjectFontSize); 263 mSubjectLayout = new StaticLayout(mText, sDefaultPaint, 264 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 265 if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) { 266 // TODO: ellipsize. 267 int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1); 268 mSubjectLayout = new StaticLayout(mText.subSequence(0, end), 269 sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 270 } 271 272 // Now, format the sender for its width 273 TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint; 274 int senderWidth = mCoordinates.sendersWidth; 275 // And get the ellipsized string for the calculated width 276 if (TextUtils.isEmpty(mSender)) { 277 mFormattedSender = ""; 278 } else { 279 mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth, 280 TruncateAt.END); 281 } 282 // Get a nicely formatted date string (relative to today) 283 mFormattedDate = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString(); 284 } 285 286 @Override 287 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 288 if (widthMeasureSpec != 0 || mViewWidth == 0) { 289 mViewWidth = MeasureSpec.getSize(widthMeasureSpec); 290 int mode = getViewMode(mViewWidth); 291 if (mode != mMode) { 292 // If the mode has changed, set the snippet line count to indicate layout required 293 mMode = mode; 294 mSnippetLineCount = NEEDS_LAYOUT; 295 } 296 mViewHeight = measureHeight(heightMeasureSpec, mMode); 297 } 298 setMeasuredDimension(mViewWidth, mViewHeight); 299 } 300 301 /** 302 * Determine the height of this view 303 * 304 * @param measureSpec A measureSpec packed into an int 305 * @param mode The current mode of this view 306 * @return The height of the view, honoring constraints from measureSpec 307 */ 308 private int measureHeight(int measureSpec, int mode) { 309 int result = 0; 310 int specMode = MeasureSpec.getMode(measureSpec); 311 int specSize = MeasureSpec.getSize(measureSpec); 312 313 if (specMode == MeasureSpec.EXACTLY) { 314 // We were told how big to be 315 result = specSize; 316 } else { 317 // Measure the text 318 if (mMode == MODE_WIDE) { 319 result = sItemHeightWide; 320 } else { 321 result = sItemHeightNarrow; 322 } 323 if (specMode == MeasureSpec.AT_MOST) { 324 // Respect AT_MOST value if that was what is called for by 325 // measureSpec 326 result = Math.min(result, specSize); 327 } 328 } 329 return result; 330 } 331 332 @Override 333 public void draw(Canvas canvas) { 334 // Update the background, before View.draw() draws it. 335 setSelected(mAdapter.isSelected(this)); 336 updateBackground(); 337 super.draw(canvas); 338 } 339 340 @Override 341 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 342 super.onLayout(changed, left, top, right, bottom); 343 344 mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth); 345 } 346 347 @Override 348 protected void onDraw(Canvas canvas) { 349 if (mSnippetLineCount == NEEDS_LAYOUT) { 350 calculateDrawingData(); 351 } 352 353 // Draw the color chip indicating the mailbox this belongs to 354 if (mColorChipPaint != null) { 355 canvas.drawRect( 356 mCoordinates.chipX, mCoordinates.chipY, 357 mCoordinates.chipX + mCoordinates.chipWidth, 358 mCoordinates.chipY + mCoordinates.chipHeight, 359 mColorChipPaint); 360 } 361 362 // Draw the checkbox 363 canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff, 364 mCoordinates.checkmarkX, mCoordinates.checkmarkY, sDefaultPaint); 365 366 // Draw the sender name 367 canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), 368 mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent, 369 mRead ? sDefaultPaint : sBoldPaint); 370 371 // Draw the reply state. Draw nothing if neither replied nor forwarded. 372 if (mHasBeenRepliedTo && mHasBeenForwarded) { 373 canvas.drawBitmap(sStateRepliedAndForwarded, 374 mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint); 375 } else if (mHasBeenRepliedTo) { 376 canvas.drawBitmap(sStateReplied, 377 mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint); 378 } else if (mHasBeenForwarded) { 379 canvas.drawBitmap(sStateForwarded, 380 mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint); 381 } 382 383 // Subject and snippet. 384 sDefaultPaint.setTextSize(mCoordinates.subjectFontSize); 385 sDefaultPaint.setTypeface(Typeface.DEFAULT); 386 canvas.save(); 387 canvas.translate( 388 mCoordinates.subjectX, 389 mCoordinates.subjectY); 390 mSubjectLayout.draw(canvas); 391 canvas.restore(); 392 393 // Draw the date 394 sDatePaint.setTextSize(mCoordinates.dateFontSize); 395 int dateX = mCoordinates.dateXEnd 396 - (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length()); 397 398 canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), 399 dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint); 400 401 // Draw the favorite icon 402 canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, 403 mCoordinates.starX, mCoordinates.starY, sDefaultPaint); 404 405 // TODO: deal with the icon layouts better from the coordinate class so that this logic 406 // doesn't have to exist. 407 // Draw the attachment and invite icons, if necessary. 408 int iconsLeft = dateX; 409 if (mHasAttachment) { 410 iconsLeft = iconsLeft - sAttachmentIcon.getWidth(); 411 canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint); 412 } 413 if (mHasInvite) { 414 iconsLeft -= sInviteIcon.getWidth(); 415 canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint); 416 } 417 418 } 419 420 /** 421 * Called by the adapter at bindView() time 422 * 423 * @param adapter the adapter that creates this view 424 */ 425 public void bindViewInit(MessagesAdapter adapter) { 426 mAdapter = adapter; 427 } 428 429 /** 430 * Overriding this method allows us to "catch" clicks in the checkbox or star 431 * and process them accordingly. 432 */ 433 @Override 434 public boolean onTouchEvent(MotionEvent event) { 435 boolean handled = false; 436 int touchX = (int) event.getX(); 437 int checkRight = mCoordinates.checkmarkWidthIncludingMargins; 438 int starLeft = mViewWidth - mCoordinates.starWidthIncludingMargins; 439 440 switch (event.getAction()) { 441 case MotionEvent.ACTION_DOWN: 442 if (touchX < checkRight || touchX > starLeft) { 443 mDownEvent = true; 444 if ((touchX < checkRight) || (touchX > starLeft)) { 445 handled = true; 446 } 447 } 448 break; 449 450 case MotionEvent.ACTION_CANCEL: 451 mDownEvent = false; 452 break; 453 454 case MotionEvent.ACTION_UP: 455 if (mDownEvent) { 456 if (touchX < checkRight) { 457 mAdapter.toggleSelected(this); 458 handled = true; 459 } else if (touchX > starLeft) { 460 mIsFavorite = !mIsFavorite; 461 mAdapter.updateFavorite(this, mIsFavorite); 462 handled = true; 463 } 464 } 465 break; 466 } 467 468 if (handled) { 469 invalidate(); 470 } else { 471 handled = super.onTouchEvent(event); 472 } 473 474 return handled; 475 } 476 477 @Override 478 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 479 CharSequence contentDescription = getContentDescription(mContext); 480 if (!TextUtils.isEmpty(contentDescription)) { 481 event.setClassName(getClass().getName()); 482 event.setPackageName(getContext().getPackageName()); 483 event.setEnabled(true); 484 event.setContentDescription(contentDescription); 485 return true; 486 } 487 return false; 488 } 489 490 /** 491 * Get message information to use for accessibility. 492 */ 493 private CharSequence getContentDescription(Context context) { 494 if (mSubjectAndDescription == null) { 495 if (!TextUtils.isEmpty(mSubject)) { 496 mSubjectAndDescription = sSubjectDescription + mSubject; 497 } else { 498 mSubjectAndDescription = sSubjectEmptyDescription; 499 } 500 } 501 return mSubjectAndDescription; 502 } 503} 504