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