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