ConversationItemView.java revision 8d69d4e10a9a36ff790babb2f3a098a12d0dc732
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mail.browse; 19 20import com.google.common.annotations.VisibleForTesting; 21 22import android.content.Context; 23import android.content.res.Resources; 24import android.database.Cursor; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.Canvas; 28import android.graphics.Color; 29import android.graphics.Rect; 30import android.graphics.Typeface; 31import android.graphics.drawable.Drawable; 32import android.text.Layout.Alignment; 33import android.text.Spannable; 34import android.text.SpannableStringBuilder; 35import android.text.StaticLayout; 36import android.text.TextPaint; 37import android.text.TextUtils; 38import android.text.TextUtils.TruncateAt; 39import android.text.format.DateUtils; 40import android.text.style.CharacterStyle; 41import android.text.style.ForegroundColorSpan; 42import android.text.style.StyleSpan; 43import android.text.util.Rfc822Token; 44import android.text.util.Rfc822Tokenizer; 45import android.util.SparseArray; 46import android.view.MotionEvent; 47import android.view.View; 48 49import com.android.mail.R; 50import com.android.mail.browse.ConversationItemViewModel.SenderFragment; 51import com.android.mail.perf.Timer; 52import com.android.mail.providers.Address; 53import com.android.mail.providers.Conversation; 54import com.android.mail.providers.UIProvider.ConversationColumns; 55import com.android.mail.ui.ConversationSelectionSet; 56import com.android.mail.ui.ViewMode; 57import com.android.mail.utils.Utils; 58 59public class ConversationItemView extends View { 60 // Timer. 61 private static int sLayoutCount = 0; 62 private static Timer sTimer; // Create the sTimer here if you need to do perf analysis. 63 private static final int PERF_LAYOUT_ITERATIONS = 50; 64 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 65 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 66 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 67 private static final String PERF_TAG_CALCULATE_LABELS = "CCHV.labels"; 68 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 69 70 // Static bitmaps. 71 private static Bitmap CHECKMARK_OFF; 72 private static Bitmap CHECKMARK_ON; 73 private static Bitmap STAR_OFF; 74 private static Bitmap STAR_ON; 75 private static Bitmap ATTACHMENT; 76 private static Bitmap ONLY_TO_ME; 77 private static Bitmap TO_ME_AND_OTHERS; 78 private static Bitmap IMPORTANT_ONLY_TO_ME; 79 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 80 private static Bitmap IMPORTANT_TO_OTHERS; 81 private static Bitmap DATE_BACKGROUND; 82 83 // Static colors. 84 private static int DEFAULT_TEXT_COLOR; 85 private static int ACTIVATED_TEXT_COLOR; 86 private static int LIGHT_TEXT_COLOR; 87 private static int DRAFT_TEXT_COLOR; 88 private static int SUBJECT_TEXT_COLOR_READ; 89 private static int SUBJECT_TEXT_COLOR_UNREAD; 90 private static int SNIPPET_TEXT_COLOR_READ; 91 private static int SNIPPET_TEXT_COLOR_UNREAD; 92 private static int SENDERS_TEXT_COLOR_READ; 93 private static int SENDERS_TEXT_COLOR_UNREAD; 94 private static int DATE_TEXT_COLOR_READ; 95 private static int DATE_TEXT_COLOR_UNREAD; 96 private static int DATE_BACKGROUND_PADDING_LEFT; 97 private static int TOUCH_SLOP; 98 private static int sDateBackgroundHeight; 99 private static int sStandardScaledDimen; 100 private static CharacterStyle sLightTextStyle; 101 private static CharacterStyle sNormalTextStyle; 102 103 // Static paints. 104 private static TextPaint sPaint = new TextPaint(); 105 private static TextPaint sLabelsPaint = new TextPaint(); 106 107 // Backgrounds for different states. 108 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 109 110 // Dimensions and coordinates. 111 private int mViewWidth = -1; 112 private int mMode = -1; 113 private int mDateX; 114 private int mPaperclipX; 115 private int mLabelsXEnd; 116 private int mSendersWidth; 117 118 /** Whether we're running under test mode. */ 119 private boolean mTesting = false; 120 121 @VisibleForTesting 122 ConversationItemViewCoordinates mCoordinates; 123 124 private final Context mContext; 125 126 private String mAccount; 127 private ConversationItemViewModel mHeader; 128 private ViewMode mViewMode; 129 private boolean mDownEvent; 130 private boolean mChecked; 131 private static int sFadedColor = -1; 132 private static int sFadedActivatedColor = -1; 133 private ConversationSelectionSet mSelectedConversationSet; 134 135 static { 136 sPaint.setAntiAlias(true); 137 sLabelsPaint.setAntiAlias(true); 138 } 139 140 /** 141 * This handler will be called when user toggle a star in a conversation 142 * header view. It can be used to update the state of other views to ensure 143 * UI consistency. 144 */ 145 public static interface StarHandler { 146 public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId); 147 } 148 149 public ConversationItemView(Context context, String account) { 150 super(context); 151 mContext = context.getApplicationContext(); 152 mAccount = account; 153 Resources res = mContext.getResources(); 154 155 if (CHECKMARK_OFF == null) { 156 // Initialize static bitmaps. 157 CHECKMARK_OFF = BitmapFactory.decodeResource(res, 158 R.drawable.btn_check_off_normal_holo_light); 159 CHECKMARK_ON = BitmapFactory.decodeResource(res, 160 R.drawable.btn_check_on_normal_holo_light); 161 STAR_OFF = BitmapFactory.decodeResource(res, 162 R.drawable.btn_star_off_normal_email_holo_light); 163 STAR_ON = BitmapFactory.decodeResource(res, 164 R.drawable.btn_star_on_normal_email_holo_light); 165 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 166 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 167 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 168 R.drawable.ic_email_caret_double_important_unread); 169 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 170 R.drawable.ic_email_caret_single_important_unread); 171 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 172 R.drawable.ic_email_caret_none_important_unread); 173 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 174 DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.label_bg_holo_light); 175 176 // Initialize colors. 177 DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color); 178 ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white); 179 LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color); 180 DRAFT_TEXT_COLOR = res.getColor(R.color.drafts); 181 SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read); 182 SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread); 183 SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read); 184 SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread); 185 SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read); 186 SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread); 187 DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read); 188 DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread); 189 DATE_BACKGROUND_PADDING_LEFT = res 190 .getDimensionPixelSize(R.dimen.date_background_padding_left); 191 TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop); 192 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height); 193 sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen); 194 195 // Initialize static color. 196 sNormalTextStyle = new StyleSpan(Typeface.NORMAL); 197 sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR); 198 } 199 } 200 201 /** 202 * Bind this view to the content of the cursor and request layout. 203 */ 204 public void bind(ConversationItemViewModel model, StarHandler starHandler, String account, 205 CharSequence displayedLabel, ViewMode viewMode, ConversationSelectionSet set) { 206 mAccount = account; 207 mViewMode = viewMode; 208 mHeader = model; 209 mSelectedConversationSet = set; 210 setContentDescription(mHeader.getContentDescription(mContext)); 211 requestLayout(); 212 } 213 214 public void bind(Cursor cursor, StarHandler starHandler, String account, 215 CharSequence displayedLabel, ViewMode viewMode, ConversationSelectionSet set) { 216 mAccount = account; 217 mViewMode = viewMode; 218 mHeader = ConversationItemViewModel.forCursor(account, cursor); 219 mSelectedConversationSet = set; 220 setContentDescription(mHeader.getContentDescription(mContext)); 221 requestLayout(); 222 } 223 224 public Conversation getConversation() { 225 return mHeader.conversation; 226 } 227 228 /** 229 * Sets the mode. Only used for testing. 230 */ 231 @VisibleForTesting 232 void setMode(int mode) { 233 mMode = mode; 234 mTesting = true; 235 } 236 237 private static void startTimer(String tag) { 238 if (sTimer != null) { 239 sTimer.start(tag); 240 } 241 } 242 243 private static void pauseTimer(String tag) { 244 if (sTimer != null) { 245 sTimer.pause(tag); 246 } 247 } 248 249 @Override 250 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 251 startTimer(PERF_TAG_LAYOUT); 252 253 super.onLayout(changed, left, top, right, bottom); 254 255 int width = right - left; 256 if (width != mViewWidth) { 257 mViewWidth = width; 258 if (!mTesting) { 259 mMode = ConversationItemViewCoordinates.getMode(mContext, mViewMode); 260 } 261 } 262 mHeader.viewWidth = mViewWidth; 263 Resources res = getResources(); 264 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 265 if (mHeader.standardScaledDimen != sStandardScaledDimen) { 266 // Large Text has been toggle on/off. Update the static dimens. 267 sStandardScaledDimen = mHeader.standardScaledDimen; 268 ConversationItemViewCoordinates.refreshConversationHeights(mContext); 269 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height); 270 } 271 mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode, 272 mHeader.standardScaledDimen); 273 calculateTextsAndBitmaps(); 274 calculateCoordinates(); 275 mHeader.validate(mContext); 276 277 pauseTimer(PERF_TAG_LAYOUT); 278 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 279 sTimer.dumpResults(); 280 sTimer = new Timer(); 281 sLayoutCount = 0; 282 } 283 } 284 285 @Override 286 public void setBackgroundResource(int resourceId) { 287 Drawable drawable = mBackgrounds.get(resourceId); 288 if (drawable == null) { 289 drawable = getResources().getDrawable(resourceId); 290 mBackgrounds.put(resourceId, drawable); 291 } 292 if (getBackground() != drawable) { 293 super.setBackgroundDrawable(drawable); 294 } 295 } 296 297 private void calculateTextsAndBitmaps() { 298 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 299 mChecked = mSelectedConversationSet != null 300 && mSelectedConversationSet.containsKey(mHeader.conversation.id); 301 // Update font color. 302 int fontColor = getFontColor(DEFAULT_TEXT_COLOR); 303 boolean fontChanged = false; 304 if (mHeader.fontColor != fontColor) { 305 fontChanged = true; 306 mHeader.fontColor = fontColor; 307 } 308 309 boolean isUnread = mHeader.unread; 310 311 final boolean checkboxEnabled = true; 312 if (mHeader.checkboxVisible != checkboxEnabled) { 313 mHeader.checkboxVisible = checkboxEnabled; 314 } 315 316 // Update background. 317 updateBackground(isUnread); 318 319 if (mHeader.isLayoutValid(mContext)) { 320 // Relayout subject if font color has changed. 321 if (fontChanged) { 322 createSubjectSpans(isUnread); 323 layoutSubject(); 324 } 325 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 326 return; 327 } 328 329 // Initialize label displayer. 330 startTimer(PERF_TAG_CALCULATE_LABELS); 331 332 pauseTimer(PERF_TAG_CALCULATE_LABELS); 333 334 // Star. 335 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF; 336 337 // Date. 338 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 339 mHeader.conversation.dateMs).toString(); 340 341 // Paper clip icon. 342 mHeader.paperclip = null; 343 if (mHeader.conversation.hasAttachments) { 344 mHeader.paperclip = ATTACHMENT; 345 } 346 347 // Personal level. 348 mHeader.personalLevelBitmap = null; 349 350 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 351 352 // Subject. 353 createSubjectSpans(isUnread); 354 355 // Parse senders fragments. 356 parseSendersFragments(isUnread); 357 358 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 359 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 360 } 361 362 private void createSubjectSpans(boolean isUnread) { 363 final String subject = filterTag(mHeader.conversation.subject); 364 final String snippet = mHeader.conversation.snippet; 365 int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ; 366 int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ; 367 mHeader.subjectText = new SpannableStringBuilder(mContext.getString( 368 R.string.subject_and_snippet, subject, snippet)); 369 if (isUnread) { 370 mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(), 371 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 372 } 373 int fontColor = getFontColor(subjectColor); 374 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0, 375 subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 376 fontColor = getFontColor(snippetColor); 377 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1, 378 mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 379 } 380 381 private int getFontColor(int defaultColor) { 382 return isActivated() && mViewMode.isTwoPane() ? ACTIVATED_TEXT_COLOR 383 : defaultColor; 384 } 385 386 private void layoutSubject() { 387 sPaint.setTextSize(mCoordinates.subjectFontSize); 388 sPaint.setColor(mHeader.fontColor); 389 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint, 390 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 391 if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) { 392 int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1); 393 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end), 394 sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 395 } 396 } 397 398 /** 399 * Parses senders text into small fragments. 400 */ 401 private void parseSendersFragments(boolean isUnread) { 402 if (TextUtils.isEmpty(mHeader.conversation.senders)) { 403 return; 404 } 405 mHeader.sendersText = formatSenders(mHeader.conversation.senders); 406 mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true); 407 } 408 409 private String formatSenders(String sendersString) { 410 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 411 String[] namesOnly = new String[senders.length]; 412 Rfc822Token[] senderTokens; 413 String display; 414 for (int i = 0; i < senders.length; i++) { 415 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 416 if (senderTokens != null && senderTokens.length > 0) { 417 display = senderTokens[0].getName(); 418 if (TextUtils.isEmpty(display)) { 419 display = senderTokens[0].getAddress(); 420 } 421 namesOnly[i] = display; 422 } 423 } 424 return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly); 425 } 426 427 private boolean canFitFragment(int width, int line, int fixedWidth) { 428 if (line == mCoordinates.sendersLineCount) { 429 return width + fixedWidth <= mSendersWidth; 430 } else { 431 return width <= mSendersWidth; 432 } 433 } 434 435 private void calculateCoordinates() { 436 startTimer(PERF_TAG_CALCULATE_COORDINATES); 437 438 sPaint.setTextSize(mCoordinates.dateFontSize); 439 sPaint.setTypeface(Typeface.DEFAULT); 440 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText); 441 442 mPaperclipX = mDateX - ATTACHMENT.getWidth(); 443 444 int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.label_cell_width); 445 446 if (ConversationItemViewCoordinates.displayLabelsAboveDate(mMode)) { 447 mLabelsXEnd = mCoordinates.dateXEnd; 448 mSendersWidth = mCoordinates.sendersWidth; 449 } else { 450 if (mHeader.paperclip != null) { 451 mLabelsXEnd = mPaperclipX; 452 } else { 453 mLabelsXEnd = mDateX - cellWidth / 2; 454 } 455 mSendersWidth = mLabelsXEnd - mCoordinates.sendersX - 2 * cellWidth; 456 } 457 458 if (mHeader.isLayoutValid(mContext)) { 459 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 460 return; 461 } 462 463 // Layout subject. 464 layoutSubject(); 465 466 // First pass to calculate width of each fragment. 467 int totalWidth = 0; 468 int fixedWidth = 0; 469 sPaint.setTextSize(mCoordinates.sendersFontSize); 470 sPaint.setTypeface(Typeface.DEFAULT); 471 for (SenderFragment senderFragment : mHeader.senderFragments) { 472 CharacterStyle style = senderFragment.style; 473 int start = senderFragment.start; 474 int end = senderFragment.end; 475 style.updateDrawState(sPaint); 476 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 477 boolean isFixed = senderFragment.isFixed; 478 if (isFixed) { 479 fixedWidth += senderFragment.width; 480 } 481 totalWidth += senderFragment.width; 482 } 483 484 // Second pass to layout each fragment. 485 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; 486 if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) { 487 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; 488 } 489 totalWidth = 0; 490 int currentLine = 1; 491 boolean ellipsize = false; 492 for (SenderFragment senderFragment : mHeader.senderFragments) { 493 CharacterStyle style = senderFragment.style; 494 int start = senderFragment.start; 495 int end = senderFragment.end; 496 int width = senderFragment.width; 497 boolean isFixed = senderFragment.isFixed; 498 style.updateDrawState(sPaint); 499 500 // No more width available, we'll only show fixed fragments. 501 if (ellipsize && !isFixed) { 502 senderFragment.shouldDisplay = false; 503 continue; 504 } 505 506 // New line and ellipsize text if needed. 507 senderFragment.ellipsizedText = null; 508 if (isFixed) { 509 fixedWidth -= width; 510 } 511 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 512 // The text is too long, new line won't help. We have to 513 // ellipsize text. 514 if (totalWidth == 0) { 515 ellipsize = true; 516 } else { 517 // New line. 518 if (currentLine < mCoordinates.sendersLineCount) { 519 currentLine++; 520 sendersY += mCoordinates.sendersLineHeight; 521 totalWidth = 0; 522 // The text is still too long, we have to ellipsize 523 // text. 524 if (totalWidth + width > mSendersWidth) { 525 ellipsize = true; 526 } 527 } else { 528 ellipsize = true; 529 } 530 } 531 532 if (ellipsize) { 533 width = mSendersWidth - totalWidth; 534 // No more new line, we have to reserve width for fixed 535 // fragments. 536 if (currentLine == mCoordinates.sendersLineCount) { 537 width -= fixedWidth; 538 } 539 senderFragment.ellipsizedText = TextUtils.ellipsize( 540 mHeader.sendersText.substring(start, end), sPaint, width, 541 TruncateAt.END).toString(); 542 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 543 } 544 } 545 senderFragment.x = mCoordinates.sendersX + totalWidth; 546 senderFragment.y = sendersY; 547 senderFragment.shouldDisplay = true; 548 totalWidth += width; 549 } 550 551 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 552 } 553 554 /** 555 * If the subject contains the tag of a mailing-list (text surrounded with 556 * []), return the subject with that tag ellipsized, e.g. 557 * "[android-gmail-team] Hello" -> "[andr...] Hello" 558 */ 559 private String filterTag(String subject) { 560 String result = subject; 561 String formatString = getContext().getResources().getString(R.string.filtered_tag); 562 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 563 int end = subject.indexOf(']'); 564 if (end > 0) { 565 String tag = subject.substring(1, end); 566 result = String.format(formatString, Utils.ellipsize(tag, 7), 567 subject.substring(end + 1)); 568 } 569 } 570 return result; 571 } 572 573 @Override 574 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 575 int width = measureWidth(widthMeasureSpec); 576 int height = measureHeight(heightMeasureSpec, 577 ConversationItemViewCoordinates.getMode(mContext, mViewMode)); 578 setMeasuredDimension(width, height); 579 } 580 581 /** 582 * Determine the width of this view. 583 * 584 * @param measureSpec A measureSpec packed into an int 585 * @return The width of the view, honoring constraints from measureSpec 586 */ 587 private int measureWidth(int measureSpec) { 588 int result = 0; 589 int specMode = MeasureSpec.getMode(measureSpec); 590 int specSize = MeasureSpec.getSize(measureSpec); 591 592 if (specMode == MeasureSpec.EXACTLY) { 593 // We were told how big to be 594 result = specSize; 595 } else { 596 // Measure the text 597 result = mViewWidth; 598 if (specMode == MeasureSpec.AT_MOST) { 599 // Respect AT_MOST value if that was what is called for by 600 // measureSpec 601 result = Math.min(result, specSize); 602 } 603 } 604 return result; 605 } 606 607 /** 608 * Determine the height of this view. 609 * 610 * @param measureSpec A measureSpec packed into an int 611 * @param mode The current mode of this view 612 * @return The height of the view, honoring constraints from measureSpec 613 */ 614 private int measureHeight(int measureSpec, int mode) { 615 int result = 0; 616 int specMode = MeasureSpec.getMode(measureSpec); 617 int specSize = MeasureSpec.getSize(measureSpec); 618 619 if (specMode == MeasureSpec.EXACTLY) { 620 // We were told how big to be 621 result = specSize; 622 } else { 623 // Measure the text 624 result = ConversationItemViewCoordinates.getHeight(mContext, mode); 625 if (specMode == MeasureSpec.AT_MOST) { 626 // Respect AT_MOST value if that was what is called for by 627 // measureSpec 628 result = Math.min(result, specSize); 629 } 630 } 631 return result; 632 } 633 634 @Override 635 protected void onDraw(Canvas canvas) { 636 // Check mark. 637 if (mHeader.checkboxVisible) { 638 Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF; 639 canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint); 640 } 641 642 // Personal Level. 643 if (mHeader.personalLevelBitmap != null) { 644 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX, 645 mCoordinates.personalLevelY, sPaint); 646 } 647 648 // Senders. 649 sPaint.setTextSize(mCoordinates.sendersFontSize); 650 sPaint.setTypeface(Typeface.DEFAULT); 651 boolean isUnread = mHeader.unread; 652 int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD 653 : SENDERS_TEXT_COLOR_READ); 654 sPaint.setColor(sendersColor); 655 for (SenderFragment fragment : mHeader.senderFragments) { 656 if (fragment.shouldDisplay) { 657 sPaint.setTypeface(Typeface.DEFAULT); 658 fragment.style.updateDrawState(sPaint); 659 if (fragment.ellipsizedText != null) { 660 canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint); 661 } else { 662 canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x, 663 fragment.y, sPaint); 664 } 665 } 666 } 667 668 // Subject. 669 sPaint.setTextSize(mCoordinates.subjectFontSize); 670 sPaint.setTypeface(Typeface.DEFAULT); 671 sPaint.setColor(mHeader.fontColor); 672 canvas.save(); 673 canvas.translate(mCoordinates.subjectX, 674 mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding()); 675 mHeader.subjectLayout.draw(canvas); 676 canvas.restore(); 677 678 // Date background: shown when there is an attachment or a visible 679 // label. 680 if (!isActivated() 681 && mHeader.conversation.hasAttachments 682 && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) { 683 mHeader.dateBackground = DATE_BACKGROUND; 684 int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX) 685 - DATE_BACKGROUND_PADDING_LEFT; 686 int top = mCoordinates.labelsY; 687 Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground 688 .getHeight()); 689 Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight); 690 canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint); 691 } else { 692 mHeader.dateBackground = null; 693 } 694 695 // Date. 696 sPaint.setTextSize(mCoordinates.dateFontSize); 697 sPaint.setTypeface(Typeface.DEFAULT); 698 sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ); 699 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent, 700 sPaint); 701 702 // Paper clip icon. 703 if (mHeader.paperclip != null) { 704 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 705 } 706 707 if (mHeader.faded) { 708 int fadedColor = -1; 709 if (sFadedActivatedColor == -1) { 710 sFadedActivatedColor = mContext.getResources().getColor( 711 R.color.faded_activated_conversation_header); 712 } 713 fadedColor = sFadedActivatedColor; 714 int restoreState = canvas.save(); 715 Rect bounds = canvas.getClipBounds(); 716 canvas.clipRect(bounds.left, bounds.top, bounds.right 717 - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width), 718 bounds.bottom); 719 canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor), 720 Color.green(fadedColor), Color.blue(fadedColor)); 721 canvas.restoreToCount(restoreState); 722 } 723 724 // Star. 725 canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint); 726 } 727 728 private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 729 canvas.drawText(s, 0, s.length(), x, y, paint); 730 } 731 732 private void updateBackground(boolean isUnread) { 733 if (isUnread) { 734 if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) { 735 if (mChecked) { 736 setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo); 737 } else { 738 setBackgroundResource(R.drawable.conversation_wide_unread_selector); 739 } 740 } else { 741 if (mChecked) { 742 setCheckedActivatedBackground(); 743 } else { 744 setBackgroundResource(R.drawable.conversation_unread_selector); 745 } 746 } 747 } else { 748 if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) { 749 if (mChecked) { 750 setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo); 751 } else { 752 setBackgroundResource(R.drawable.conversation_wide_read_selector); 753 } 754 } else { 755 if (mChecked) { 756 setCheckedActivatedBackground(); 757 } else { 758 setBackgroundResource(R.drawable.conversation_read_selector); 759 } 760 } 761 } 762 } 763 764 private void setCheckedActivatedBackground() { 765 if (isActivated() && mViewMode.isTwoPane()) { 766 setBackgroundResource(R.drawable.list_arrow_selected_holo); 767 } else { 768 setBackgroundResource(R.drawable.list_selected_holo); 769 } 770 } 771 772 /** 773 * Toggle the check mark on this view and update the conversation 774 */ 775 public void toggleCheckMark() { 776 mChecked = !mChecked; 777 mSelectedConversationSet.toggle(mHeader.conversation); 778 // We update the background after the checked state has changed now that 779 // we have a selected background asset. Setting the background usually 780 // waits for a layout pass, but we don't need a full layout, just an 781 // update to the background. 782 requestLayout(); 783 } 784 785 /** 786 * Toggle the star on this view and update the conversation. 787 */ 788 private void toggleStar() { 789 mHeader.starred = !mHeader.starred; 790 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF; 791 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 792 + mHeader.starBitmap.getWidth(), 793 mCoordinates.starY + mHeader.starBitmap.getHeight()); 794 // Generalize this... 795 mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred); 796 } 797 798 private boolean touchCheckmark(float x, float y) { 799 // Everything before senders and include a touch slop. 800 return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP; 801 } 802 803 private boolean touchStar(float x, float y) { 804 // Everything after the star and include a touch slop. 805 return x > mCoordinates.starX - TOUCH_SLOP; 806 } 807 808 @Override 809 public boolean onTouchEvent(MotionEvent event) { 810 boolean handled = false; 811 812 int x = (int) event.getX(); 813 int y = (int) event.getY(); 814 switch (event.getAction()) { 815 case MotionEvent.ACTION_DOWN: 816 mDownEvent = true; 817 if (touchCheckmark(x, y) || touchStar(x, y)) { 818 handled = true; 819 } 820 break; 821 822 case MotionEvent.ACTION_CANCEL: 823 mDownEvent = false; 824 break; 825 826 case MotionEvent.ACTION_UP: 827 if (mDownEvent) { 828 if (touchCheckmark(x, y)) { 829 // Touch on the check mark 830 toggleCheckMark(); 831 } else if (touchStar(x, y)) { 832 // Touch on the star 833 toggleStar(); 834 } 835 handled = true; 836 } 837 break; 838 } 839 840 if (!handled) { 841 handled = super.onTouchEvent(event); 842 } 843 844 return handled; 845 } 846} 847