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