ConversationItemView.java revision 4584a0d83e160444f931cb565185a2eea39b1683
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.LinearGradient; 30import android.graphics.Paint; 31import android.graphics.Rect; 32import android.graphics.Shader; 33import android.graphics.Typeface; 34import android.graphics.drawable.Drawable; 35import android.text.Layout.Alignment; 36import android.text.Spannable; 37import android.text.SpannableStringBuilder; 38import android.text.StaticLayout; 39import android.text.TextPaint; 40import android.text.TextUtils; 41import android.text.TextUtils.TruncateAt; 42import android.text.format.DateUtils; 43import android.text.style.CharacterStyle; 44import android.text.style.ForegroundColorSpan; 45import android.text.style.StyleSpan; 46import android.text.util.Rfc822Token; 47import android.text.util.Rfc822Tokenizer; 48import android.util.SparseArray; 49import android.view.MotionEvent; 50import android.view.View; 51import android.widget.ListView; 52 53import com.android.mail.R; 54import com.android.mail.browse.ConversationItemViewModel.SenderFragment; 55import com.android.mail.perf.Timer; 56import com.android.mail.providers.Address; 57import com.android.mail.providers.Conversation; 58import com.android.mail.providers.Folder; 59import com.android.mail.providers.UIProvider.ConversationColumns; 60import com.android.mail.ui.ConversationSelectionSet; 61import com.android.mail.ui.FolderDisplayer; 62import com.android.mail.ui.ViewMode; 63import com.android.mail.utils.Utils; 64 65import java.util.Map; 66 67public class ConversationItemView extends View { 68 // Timer. 69 private static int sLayoutCount = 0; 70 private static Timer sTimer; // Create the sTimer here if you need to do perf analysis. 71 private static final int PERF_LAYOUT_ITERATIONS = 50; 72 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 73 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 74 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 75 private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; 76 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 77 78 // Static bitmaps. 79 private static Bitmap CHECKMARK_OFF; 80 private static Bitmap CHECKMARK_ON; 81 private static Bitmap STAR_OFF; 82 private static Bitmap STAR_ON; 83 private static Bitmap ATTACHMENT; 84 private static Bitmap ONLY_TO_ME; 85 private static Bitmap TO_ME_AND_OTHERS; 86 private static Bitmap IMPORTANT_ONLY_TO_ME; 87 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 88 private static Bitmap IMPORTANT_TO_OTHERS; 89 private static Bitmap DATE_BACKGROUND; 90 91 // Static colors. 92 private static int DEFAULT_TEXT_COLOR; 93 private static int ACTIVATED_TEXT_COLOR; 94 private static int LIGHT_TEXT_COLOR; 95 private static int DRAFT_TEXT_COLOR; 96 private static int SUBJECT_TEXT_COLOR_READ; 97 private static int SUBJECT_TEXT_COLOR_UNREAD; 98 private static int SNIPPET_TEXT_COLOR_READ; 99 private static int SNIPPET_TEXT_COLOR_UNREAD; 100 private static int SENDERS_TEXT_COLOR_READ; 101 private static int SENDERS_TEXT_COLOR_UNREAD; 102 private static int DATE_TEXT_COLOR_READ; 103 private static int DATE_TEXT_COLOR_UNREAD; 104 private static int DATE_BACKGROUND_PADDING_LEFT; 105 private static int TOUCH_SLOP; 106 private static int sDateBackgroundHeight; 107 private static int sStandardScaledDimen; 108 private static CharacterStyle sLightTextStyle; 109 private static CharacterStyle sNormalTextStyle; 110 111 // Static paints. 112 private static TextPaint sPaint = new TextPaint(); 113 private static TextPaint sFoldersPaint = new TextPaint(); 114 115 // Backgrounds for different states. 116 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 117 118 // Dimensions and coordinates. 119 private int mViewWidth = -1; 120 private int mMode = -1; 121 private int mDateX; 122 private int mPaperclipX; 123 private int mFoldersXEnd; 124 private int mSendersWidth; 125 126 /** Whether we're running under test mode. */ 127 private boolean mTesting = false; 128 /** Whether we are on a tablet device or not */ 129 private final boolean mTabletDevice; 130 131 @VisibleForTesting 132 ConversationItemViewCoordinates mCoordinates; 133 134 private final Context mContext; 135 136 private String mAccount; 137 private ConversationItemViewModel mHeader; 138 private ViewMode mViewMode; 139 private boolean mDownEvent; 140 private boolean mChecked = false; 141 private static int sFadedColor = -1; 142 private static int sFadedActivatedColor = -1; 143 private ConversationSelectionSet mSelectedConversationSet; 144 private Folder mDisplayedFolder; 145 private static Bitmap MORE_FOLDERS; 146 147 static { 148 sPaint.setAntiAlias(true); 149 sFoldersPaint.setAntiAlias(true); 150 } 151 152 153 /** 154 * Handles displaying folders in a conversation header view. 155 */ 156 static class ConversationItemFolderDisplayer extends FolderDisplayer { 157 // Maximum number of folders to be displayed. 158 private static final int MAX_DISPLAYED_FOLDERS_COUNT = 4; 159 160 private int mFoldersCount; 161 private boolean mHasMoreFolders; 162 163 @Override 164 public void loadConversationFolders(Folder folder, String rawFolders) { 165 super.loadConversationFolders(folder, rawFolders); 166 167 mFoldersCount = mFolderValuesSortedSet.size(); 168 mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT; 169 mFoldersCount = Math.min(mFoldersCount, MAX_DISPLAYED_FOLDERS_COUNT); 170 } 171 172 public boolean hasVisibleFolders() { 173 return mFoldersCount > 0; 174 } 175 176 private int measureFolders(int mode) { 177 int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode); 178 int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode, 179 mFoldersCount); 180 181 int totalWidth = 0; 182 for (FolderValues labelValues : mFolderValuesSortedSet) { 183 String labelString = labelValues.name; 184 int width = (int) sFoldersPaint.measureText(labelString) + cellSize; 185 if (width % cellSize != 0) { 186 width += cellSize - (width % cellSize); 187 } 188 totalWidth += width; 189 if (totalWidth > availableSpace) { 190 break; 191 } 192 } 193 194 return totalWidth; 195 } 196 197 public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates, 198 int foldersXEnd, int mode) { 199 if (mFoldersCount == 0) { 200 return; 201 } 202 203 int xEnd = foldersXEnd; 204 int y = coordinates.foldersY - coordinates.foldersAscent; 205 int height = coordinates.foldersHeight; 206 int topPadding = coordinates.foldersTopPadding; 207 int ascent = coordinates.foldersAscent; 208 sFoldersPaint.setTextSize(coordinates.foldersFontSize); 209 210 // Initialize space and cell size based on the current mode. 211 int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode); 212 int averageWidth = availableSpace / mFoldersCount; 213 int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode, 214 mFoldersCount); 215 216 // First pass to calculate the starting point. 217 int totalWidth = measureFolders(mode); 218 int xStart = xEnd - Math.min(availableSpace, totalWidth); 219 220 // Second pass to draw folders. 221 for (FolderValues labelValues : mFolderValuesSortedSet) { 222 String folderstring = labelValues.name; 223 int width = cellSize; 224 boolean labelTooLong = false; 225 width = (int) sFoldersPaint.measureText(folderstring) + cellSize; 226 if (width % cellSize != 0) { 227 width += cellSize - (width % cellSize); 228 } 229 if (totalWidth > availableSpace && width > averageWidth) { 230 width = averageWidth; 231 labelTooLong = true; 232 } 233 234 // TODO (mindyp): how to we get this? 235 final boolean isMuted = false; 236 // labelValues.folderId == sGmail.getFolderMap(mAccount).getFolderIdIgnored(); 237 238 // Draw the box. 239 sFoldersPaint.setColor(labelValues.backgroundColor); 240 sFoldersPaint.setStyle(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE); 241 canvas.drawRect(xStart, y + ascent, xStart + width, y + ascent + height, 242 sFoldersPaint); 243 244 // Draw the text. 245 sFoldersPaint.setColor(labelValues.textColor); 246 int padding = getPadding(width, (int) sFoldersPaint.measureText(folderstring)); 247 if (labelTooLong) { 248 padding = cellSize / 2; 249 int rightBorder = xStart + width - padding; 250 Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y, 251 labelValues.textColor, 252 Utils.getTransparentColor(labelValues.textColor), 253 Shader.TileMode.CLAMP); 254 sFoldersPaint.setShader(shader); 255 } 256 canvas.drawText(folderstring, xStart + padding, y + topPadding, sFoldersPaint); 257 sFoldersPaint.setShader(null); 258 259 availableSpace -= width; 260 xStart += width; 261 if (availableSpace <= 0 && mHasMoreFolders) { 262 canvas.drawBitmap(MORE_FOLDERS, xEnd, y + ascent, sFoldersPaint); 263 return; 264 } 265 } 266 } 267 268 /** 269 * Helpers function to align an element in the center of a space. 270 */ 271 private static int getPadding(int space, int length) { 272 return (space - length) / 2; 273 } 274 } 275 276 public ConversationItemView(Context context, String account) { 277 super(context); 278 mContext = context.getApplicationContext(); 279 mTabletDevice = Utils.useTabletUI(mContext); 280 281 mAccount = account; 282 Resources res = mContext.getResources(); 283 284 if (CHECKMARK_OFF == null) { 285 // Initialize static bitmaps. 286 CHECKMARK_OFF = BitmapFactory.decodeResource(res, 287 R.drawable.btn_check_off_normal_holo_light); 288 CHECKMARK_ON = BitmapFactory.decodeResource(res, 289 R.drawable.btn_check_on_normal_holo_light); 290 STAR_OFF = BitmapFactory.decodeResource(res, 291 R.drawable.btn_star_off_normal_email_holo_light); 292 STAR_ON = BitmapFactory.decodeResource(res, 293 R.drawable.btn_star_on_normal_email_holo_light); 294 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double); 295 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single); 296 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res, 297 R.drawable.ic_email_caret_double_important_unread); 298 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, 299 R.drawable.ic_email_caret_single_important_unread); 300 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res, 301 R.drawable.ic_email_caret_none_important_unread); 302 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light); 303 MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more); 304 DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.folder_bg_holo_light); 305 306 // Initialize colors. 307 DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color); 308 ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white); 309 LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color); 310 DRAFT_TEXT_COLOR = res.getColor(R.color.drafts); 311 SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read); 312 SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread); 313 SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read); 314 SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread); 315 SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read); 316 SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread); 317 DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read); 318 DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread); 319 DATE_BACKGROUND_PADDING_LEFT = res 320 .getDimensionPixelSize(R.dimen.date_background_padding_left); 321 TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop); 322 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height); 323 sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen); 324 325 // Initialize static color. 326 sNormalTextStyle = new StyleSpan(Typeface.NORMAL); 327 sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR); 328 } 329 } 330 331 public void bind(Cursor cursor, String account, ViewMode viewMode, 332 ConversationSelectionSet set, Folder folder) { 333 mAccount = account; 334 mViewMode = viewMode; 335 mHeader = ConversationItemViewModel.forCursor(account, cursor); 336 mSelectedConversationSet = set; 337 mDisplayedFolder = folder; 338 setContentDescription(mHeader.getContentDescription(mContext)); 339 requestLayout(); 340 } 341 342 public Conversation getConversation() { 343 return mHeader.conversation; 344 } 345 346 /** 347 * Sets the mode. Only used for testing. 348 */ 349 @VisibleForTesting 350 void setMode(int mode) { 351 mMode = mode; 352 mTesting = true; 353 } 354 355 private static void startTimer(String tag) { 356 if (sTimer != null) { 357 sTimer.start(tag); 358 } 359 } 360 361 private static void pauseTimer(String tag) { 362 if (sTimer != null) { 363 sTimer.pause(tag); 364 } 365 } 366 367 @Override 368 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 369 startTimer(PERF_TAG_LAYOUT); 370 371 super.onLayout(changed, left, top, right, bottom); 372 373 int width = right - left; 374 if (width != mViewWidth) { 375 mViewWidth = width; 376 if (!mTesting) { 377 mMode = ConversationItemViewCoordinates.getMode(mContext, mViewMode); 378 } 379 } 380 mHeader.viewWidth = mViewWidth; 381 Resources res = getResources(); 382 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen); 383 if (mHeader.standardScaledDimen != sStandardScaledDimen) { 384 // Large Text has been toggle on/off. Update the static dimens. 385 sStandardScaledDimen = mHeader.standardScaledDimen; 386 ConversationItemViewCoordinates.refreshConversationHeights(mContext); 387 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height); 388 } 389 mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode, 390 mHeader.standardScaledDimen); 391 calculateTextsAndBitmaps(); 392 calculateCoordinates(); 393 mHeader.validate(mContext); 394 395 pauseTimer(PERF_TAG_LAYOUT); 396 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) { 397 sTimer.dumpResults(); 398 sTimer = new Timer(); 399 sLayoutCount = 0; 400 } 401 } 402 403 @Override 404 public void setBackgroundResource(int resourceId) { 405 Drawable drawable = mBackgrounds.get(resourceId); 406 if (drawable == null) { 407 drawable = getResources().getDrawable(resourceId); 408 mBackgrounds.put(resourceId, drawable); 409 } 410 if (getBackground() != drawable) { 411 super.setBackgroundDrawable(drawable); 412 } 413 } 414 415 private void calculateTextsAndBitmaps() { 416 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 417 if (mSelectedConversationSet != null) { 418 mChecked = mSelectedConversationSet.contains(mHeader.conversation); 419 } 420 // Update font color. 421 int fontColor = getFontColor(DEFAULT_TEXT_COLOR); 422 boolean fontChanged = false; 423 if (mHeader.fontColor != fontColor) { 424 fontChanged = true; 425 mHeader.fontColor = fontColor; 426 } 427 428 boolean isUnread = mHeader.unread; 429 430 final boolean checkboxEnabled = true; 431 if (mHeader.checkboxVisible != checkboxEnabled) { 432 mHeader.checkboxVisible = checkboxEnabled; 433 } 434 435 // Update background. 436 updateBackground(isUnread); 437 438 if (mHeader.isLayoutValid(mContext)) { 439 // Relayout subject if font color has changed. 440 if (fontChanged) { 441 createSubjectSpans(isUnread); 442 layoutSubject(); 443 } 444 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 445 return; 446 } 447 448 startTimer(PERF_TAG_CALCULATE_FOLDERS); 449 450 // Initialize folder displayer. 451 if (mCoordinates.showFolders) { 452 mHeader.folderDisplayer = new ConversationItemFolderDisplayer(); 453 mHeader.folderDisplayer.initialize(mContext, mAccount); 454 mHeader.folderDisplayer.loadConversationFolders(mDisplayedFolder, mHeader.rawFolders); 455 } 456 457 pauseTimer(PERF_TAG_CALCULATE_FOLDERS); 458 459 // Star. 460 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF; 461 462 // Date. 463 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, 464 mHeader.conversation.dateMs).toString(); 465 466 // Paper clip icon. 467 mHeader.paperclip = null; 468 if (mHeader.conversation.hasAttachments) { 469 mHeader.paperclip = ATTACHMENT; 470 } 471 472 // Personal level. 473 mHeader.personalLevelBitmap = null; 474 475 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 476 477 // Subject. 478 createSubjectSpans(isUnread); 479 480 // Parse senders fragments. 481 parseSendersFragments(isUnread); 482 483 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 484 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 485 } 486 487 private void createSubjectSpans(boolean isUnread) { 488 final String subject = filterTag(mHeader.conversation.subject); 489 final String snippet = mHeader.conversation.snippet; 490 int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ; 491 int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ; 492 mHeader.subjectText = new SpannableStringBuilder(mContext.getString( 493 R.string.subject_and_snippet, subject, snippet)); 494 if (isUnread) { 495 mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(), 496 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 497 } 498 int fontColor = getFontColor(subjectColor); 499 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0, 500 subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 501 fontColor = getFontColor(snippetColor); 502 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1, 503 mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 504 } 505 506 private int getFontColor(int defaultColor) { 507 return isActivated() && mTabletDevice ? ACTIVATED_TEXT_COLOR 508 : defaultColor; 509 } 510 511 private void layoutSubject() { 512 sPaint.setTextSize(mCoordinates.subjectFontSize); 513 sPaint.setColor(mHeader.fontColor); 514 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint, 515 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 516 if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) { 517 int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1); 518 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end), 519 sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 520 } 521 } 522 523 /** 524 * Parses senders text into small fragments. 525 */ 526 private void parseSendersFragments(boolean isUnread) { 527 if (TextUtils.isEmpty(mHeader.conversation.senders)) { 528 return; 529 } 530 mHeader.sendersText = formatSenders(mHeader.conversation.senders); 531 mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true); 532 } 533 534 private String formatSenders(String sendersString) { 535 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 536 String[] namesOnly = new String[senders.length]; 537 Rfc822Token[] senderTokens; 538 String display; 539 for (int i = 0; i < senders.length; i++) { 540 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 541 if (senderTokens != null && senderTokens.length > 0) { 542 display = senderTokens[0].getName(); 543 if (TextUtils.isEmpty(display)) { 544 display = senderTokens[0].getAddress(); 545 } 546 namesOnly[i] = display; 547 } 548 } 549 return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly); 550 } 551 552 private boolean canFitFragment(int width, int line, int fixedWidth) { 553 if (line == mCoordinates.sendersLineCount) { 554 return width + fixedWidth <= mSendersWidth; 555 } else { 556 return width <= mSendersWidth; 557 } 558 } 559 560 private void calculateCoordinates() { 561 startTimer(PERF_TAG_CALCULATE_COORDINATES); 562 563 sPaint.setTextSize(mCoordinates.dateFontSize); 564 sPaint.setTypeface(Typeface.DEFAULT); 565 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText); 566 567 mPaperclipX = mDateX - ATTACHMENT.getWidth(); 568 569 int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.folder_cell_width); 570 571 if (mCoordinates.showFolders) { 572 if (ConversationItemViewCoordinates.displayFoldersAboveDate(mCoordinates.showFolders, 573 mMode)) { 574 mFoldersXEnd = mCoordinates.dateXEnd; 575 mSendersWidth = mCoordinates.sendersWidth; 576 } else { 577 if (mHeader.paperclip != null) { 578 mFoldersXEnd = mPaperclipX; 579 } else { 580 mFoldersXEnd = mDateX - cellWidth / 2; 581 } 582 mSendersWidth = mFoldersXEnd - mCoordinates.sendersX - 2 * cellWidth; 583 if (mHeader.folderDisplayer.hasVisibleFolders()) { 584 mSendersWidth -= ConversationItemViewCoordinates.getFoldersWidth(mContext, 585 mMode); 586 } 587 } 588 } else { 589 int dateAttachmentStart = 0; 590 // Have this end near the paperclip or date, not the folders. 591 if (mHeader.paperclip != null) { 592 dateAttachmentStart = mPaperclipX; 593 } else { 594 dateAttachmentStart = mDateX; 595 } 596 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX - cellWidth; 597 } 598 599 if (mHeader.isLayoutValid(mContext)) { 600 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 601 return; 602 } 603 604 // Layout subject. 605 layoutSubject(); 606 607 // First pass to calculate width of each fragment. 608 int totalWidth = 0; 609 int fixedWidth = 0; 610 sPaint.setTextSize(mCoordinates.sendersFontSize); 611 sPaint.setTypeface(Typeface.DEFAULT); 612 for (SenderFragment senderFragment : mHeader.senderFragments) { 613 CharacterStyle style = senderFragment.style; 614 int start = senderFragment.start; 615 int end = senderFragment.end; 616 style.updateDrawState(sPaint); 617 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 618 boolean isFixed = senderFragment.isFixed; 619 if (isFixed) { 620 fixedWidth += senderFragment.width; 621 } 622 totalWidth += senderFragment.width; 623 } 624 625 // Second pass to layout each fragment. 626 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; 627 if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) { 628 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; 629 } 630 totalWidth = 0; 631 int currentLine = 1; 632 boolean ellipsize = false; 633 for (SenderFragment senderFragment : mHeader.senderFragments) { 634 CharacterStyle style = senderFragment.style; 635 int start = senderFragment.start; 636 int end = senderFragment.end; 637 int width = senderFragment.width; 638 boolean isFixed = senderFragment.isFixed; 639 style.updateDrawState(sPaint); 640 641 // No more width available, we'll only show fixed fragments. 642 if (ellipsize && !isFixed) { 643 senderFragment.shouldDisplay = false; 644 continue; 645 } 646 647 // New line and ellipsize text if needed. 648 senderFragment.ellipsizedText = null; 649 if (isFixed) { 650 fixedWidth -= width; 651 } 652 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 653 // The text is too long, new line won't help. We have to 654 // ellipsize text. 655 if (totalWidth == 0) { 656 ellipsize = true; 657 } else { 658 // New line. 659 if (currentLine < mCoordinates.sendersLineCount) { 660 currentLine++; 661 sendersY += mCoordinates.sendersLineHeight; 662 totalWidth = 0; 663 // The text is still too long, we have to ellipsize 664 // text. 665 if (totalWidth + width > mSendersWidth) { 666 ellipsize = true; 667 } 668 } else { 669 ellipsize = true; 670 } 671 } 672 673 if (ellipsize) { 674 width = mSendersWidth - totalWidth; 675 // No more new line, we have to reserve width for fixed 676 // fragments. 677 if (currentLine == mCoordinates.sendersLineCount) { 678 width -= fixedWidth; 679 } 680 senderFragment.ellipsizedText = TextUtils.ellipsize( 681 mHeader.sendersText.substring(start, end), sPaint, width, 682 TruncateAt.END).toString(); 683 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 684 } 685 } 686 senderFragment.x = mCoordinates.sendersX + totalWidth; 687 senderFragment.y = sendersY; 688 senderFragment.shouldDisplay = true; 689 totalWidth += width; 690 } 691 692 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 693 } 694 695 /** 696 * If the subject contains the tag of a mailing-list (text surrounded with 697 * []), return the subject with that tag ellipsized, e.g. 698 * "[android-gmail-team] Hello" -> "[andr...] Hello" 699 */ 700 private String filterTag(String subject) { 701 String result = subject; 702 String formatString = getContext().getResources().getString(R.string.filtered_tag); 703 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 704 int end = subject.indexOf(']'); 705 if (end > 0) { 706 String tag = subject.substring(1, end); 707 result = String.format(formatString, Utils.ellipsize(tag, 7), 708 subject.substring(end + 1)); 709 } 710 } 711 return result; 712 } 713 714 @Override 715 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 716 int width = measureWidth(widthMeasureSpec); 717 int height = measureHeight(heightMeasureSpec, 718 ConversationItemViewCoordinates.getMode(mContext, mViewMode)); 719 setMeasuredDimension(width, height); 720 } 721 722 /** 723 * Determine the width of this view. 724 * 725 * @param measureSpec A measureSpec packed into an int 726 * @return The width of the view, honoring constraints from measureSpec 727 */ 728 private int measureWidth(int measureSpec) { 729 int result = 0; 730 int specMode = MeasureSpec.getMode(measureSpec); 731 int specSize = MeasureSpec.getSize(measureSpec); 732 733 if (specMode == MeasureSpec.EXACTLY) { 734 // We were told how big to be 735 result = specSize; 736 } else { 737 // Measure the text 738 result = mViewWidth; 739 if (specMode == MeasureSpec.AT_MOST) { 740 // Respect AT_MOST value if that was what is called for by 741 // measureSpec 742 result = Math.min(result, specSize); 743 } 744 } 745 return result; 746 } 747 748 /** 749 * Determine the height of this view. 750 * 751 * @param measureSpec A measureSpec packed into an int 752 * @param mode The current mode of this view 753 * @return The height of the view, honoring constraints from measureSpec 754 */ 755 private int measureHeight(int measureSpec, int mode) { 756 int result = 0; 757 int specMode = MeasureSpec.getMode(measureSpec); 758 int specSize = MeasureSpec.getSize(measureSpec); 759 760 if (specMode == MeasureSpec.EXACTLY) { 761 // We were told how big to be 762 result = specSize; 763 } else { 764 // Measure the text 765 result = ConversationItemViewCoordinates.getHeight(mContext, mode); 766 if (specMode == MeasureSpec.AT_MOST) { 767 // Respect AT_MOST value if that was what is called for by 768 // measureSpec 769 result = Math.min(result, specSize); 770 } 771 } 772 return result; 773 } 774 775 @Override 776 protected void onDraw(Canvas canvas) { 777 // Check mark. 778 if (mHeader.checkboxVisible) { 779 Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF; 780 canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint); 781 } 782 783 // Personal Level. 784 if (mHeader.personalLevelBitmap != null) { 785 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX, 786 mCoordinates.personalLevelY, sPaint); 787 } 788 789 // Senders. 790 sPaint.setTextSize(mCoordinates.sendersFontSize); 791 sPaint.setTypeface(Typeface.DEFAULT); 792 boolean isUnread = mHeader.unread; 793 int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD 794 : SENDERS_TEXT_COLOR_READ); 795 sPaint.setColor(sendersColor); 796 for (SenderFragment fragment : mHeader.senderFragments) { 797 if (fragment.shouldDisplay) { 798 sPaint.setTypeface(Typeface.DEFAULT); 799 fragment.style.updateDrawState(sPaint); 800 if (fragment.ellipsizedText != null) { 801 canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint); 802 } else { 803 canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x, 804 fragment.y, sPaint); 805 } 806 } 807 } 808 809 // Subject. 810 sPaint.setTextSize(mCoordinates.subjectFontSize); 811 sPaint.setTypeface(Typeface.DEFAULT); 812 sPaint.setColor(mHeader.fontColor); 813 canvas.save(); 814 canvas.translate(mCoordinates.subjectX, 815 mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding()); 816 mHeader.subjectLayout.draw(canvas); 817 canvas.restore(); 818 819 // Folders. 820 if (mCoordinates.showFolders) { 821 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode); 822 } 823 824 // Date background: shown when there is an attachment or a visible 825 // folder. 826 if (!isActivated() 827 && mHeader.conversation.hasAttachments 828 && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) { 829 mHeader.dateBackground = DATE_BACKGROUND; 830 int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX) 831 - DATE_BACKGROUND_PADDING_LEFT; 832 int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY; 833 Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground 834 .getHeight()); 835 Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight); 836 canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint); 837 } else { 838 mHeader.dateBackground = null; 839 } 840 841 // Date. 842 sPaint.setTextSize(mCoordinates.dateFontSize); 843 sPaint.setTypeface(Typeface.DEFAULT); 844 sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ); 845 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent, 846 sPaint); 847 848 // Paper clip icon. 849 if (mHeader.paperclip != null) { 850 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 851 } 852 853 if (mHeader.faded) { 854 int fadedColor = -1; 855 if (sFadedActivatedColor == -1) { 856 sFadedActivatedColor = mContext.getResources().getColor( 857 R.color.faded_activated_conversation_header); 858 } 859 fadedColor = sFadedActivatedColor; 860 int restoreState = canvas.save(); 861 Rect bounds = canvas.getClipBounds(); 862 canvas.clipRect(bounds.left, bounds.top, bounds.right 863 - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width), 864 bounds.bottom); 865 canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor), 866 Color.green(fadedColor), Color.blue(fadedColor)); 867 canvas.restoreToCount(restoreState); 868 } 869 870 // Star. 871 canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint); 872 } 873 874 private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 875 canvas.drawText(s, 0, s.length(), x, y, paint); 876 } 877 878 private void updateBackground(boolean isUnread) { 879 if (isUnread) { 880 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) { 881 if (mChecked) { 882 setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo); 883 } else { 884 setBackgroundResource(R.drawable.conversation_wide_unread_selector); 885 } 886 } else { 887 if (mChecked) { 888 setCheckedActivatedBackground(); 889 } else { 890 setBackgroundResource(R.drawable.conversation_unread_selector); 891 } 892 } 893 } else { 894 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) { 895 if (mChecked) { 896 setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo); 897 } else { 898 setBackgroundResource(R.drawable.conversation_wide_read_selector); 899 } 900 } else { 901 if (mChecked) { 902 setCheckedActivatedBackground(); 903 } else { 904 setBackgroundResource(R.drawable.conversation_read_selector); 905 } 906 } 907 } 908 } 909 910 private void setCheckedActivatedBackground() { 911 if (isActivated() && mTabletDevice) { 912 setBackgroundResource(R.drawable.list_arrow_selected_holo); 913 } else { 914 setBackgroundResource(R.drawable.list_selected_holo); 915 } 916 } 917 918 /** 919 * Toggle the check mark on this view and update the conversation 920 */ 921 public void toggleCheckMark() { 922 mChecked = !mChecked; 923 Conversation conv = mHeader.conversation; 924 // Set the list position of this item in the conversation 925 conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this) 926 : Conversation.NO_POSITION; 927 if (mSelectedConversationSet != null) { 928 mSelectedConversationSet.toggle(conv); 929 } 930 // We update the background after the checked state has changed now that 931 // we have a selected background asset. Setting the background usually 932 // waits for a layout pass, but we don't need a full layout, just an 933 // update to the background. 934 requestLayout(); 935 } 936 937 /** 938 * Toggle the star on this view and update the conversation. 939 */ 940 private void toggleStar() { 941 mHeader.starred = !mHeader.starred; 942 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF; 943 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 944 + mHeader.starBitmap.getWidth(), 945 mCoordinates.starY + mHeader.starBitmap.getHeight()); 946 // Generalize this... 947 mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred); 948 } 949 950 private boolean touchCheckmark(float x, float y) { 951 // Everything before senders and include a touch slop. 952 return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP; 953 } 954 955 private boolean touchStar(float x, float y) { 956 // Everything after the star and include a touch slop. 957 return x > mCoordinates.starX - TOUCH_SLOP; 958 } 959 960 @Override 961 public boolean onTouchEvent(MotionEvent event) { 962 boolean handled = false; 963 964 int x = (int) event.getX(); 965 int y = (int) event.getY(); 966 switch (event.getAction()) { 967 case MotionEvent.ACTION_DOWN: 968 mDownEvent = true; 969 if (touchCheckmark(x, y) || touchStar(x, y)) { 970 handled = true; 971 } 972 break; 973 974 case MotionEvent.ACTION_CANCEL: 975 mDownEvent = false; 976 break; 977 978 case MotionEvent.ACTION_UP: 979 if (mDownEvent) { 980 if (touchCheckmark(x, y)) { 981 // Touch on the check mark 982 toggleCheckMark(); 983 } else if (touchStar(x, y)) { 984 // Touch on the star 985 toggleStar(); 986 } 987 handled = true; 988 } 989 break; 990 } 991 992 if (!handled) { 993 handled = super.onTouchEvent(event); 994 } 995 996 return handled; 997 } 998} 999