ConversationItemView.java revision 3cb938f638cc3cec08c9c42d20192e65b1e7d343
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; 60import com.android.mail.providers.UIProvider.ConversationColumns; 61import com.android.mail.ui.ConversationSelectionSet; 62import com.android.mail.ui.FolderDisplayer; 63import com.android.mail.ui.ViewMode; 64import com.android.mail.utils.Utils; 65 66public class ConversationItemView extends View { 67 // Timer. 68 private static int sLayoutCount = 0; 69 private static Timer sTimer; // Create the sTimer here if you need to do perf analysis. 70 private static final int PERF_LAYOUT_ITERATIONS = 50; 71 private static final String PERF_TAG_LAYOUT = "CCHV.layout"; 72 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps"; 73 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj"; 74 private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders"; 75 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates"; 76 77 // Static bitmaps. 78 private static Bitmap CHECKMARK_OFF; 79 private static Bitmap CHECKMARK_ON; 80 private static Bitmap STAR_OFF; 81 private static Bitmap STAR_ON; 82 private static Bitmap ATTACHMENT; 83 private static Bitmap ONLY_TO_ME; 84 private static Bitmap TO_ME_AND_OTHERS; 85 private static Bitmap IMPORTANT_ONLY_TO_ME; 86 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS; 87 private static Bitmap IMPORTANT_TO_OTHERS; 88 private static Bitmap DATE_BACKGROUND; 89 90 // Static colors. 91 private static int DEFAULT_TEXT_COLOR; 92 private static int ACTIVATED_TEXT_COLOR; 93 private static int LIGHT_TEXT_COLOR; 94 private static int DRAFT_TEXT_COLOR; 95 private static int SUBJECT_TEXT_COLOR_READ; 96 private static int SUBJECT_TEXT_COLOR_UNREAD; 97 private static int SNIPPET_TEXT_COLOR_READ; 98 private static int SNIPPET_TEXT_COLOR_UNREAD; 99 private static int SENDERS_TEXT_COLOR_READ; 100 private static int SENDERS_TEXT_COLOR_UNREAD; 101 private static int DATE_TEXT_COLOR_READ; 102 private static int DATE_TEXT_COLOR_UNREAD; 103 private static int DATE_BACKGROUND_PADDING_LEFT; 104 private static int TOUCH_SLOP; 105 private static int sDateBackgroundHeight; 106 private static int sStandardScaledDimen; 107 private static CharacterStyle sLightTextStyle; 108 private static CharacterStyle sNormalTextStyle; 109 110 // Static paints. 111 private static TextPaint sPaint = new TextPaint(); 112 private static TextPaint sFoldersPaint = new TextPaint(); 113 114 // Backgrounds for different states. 115 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>(); 116 117 // Dimensions and coordinates. 118 private int mViewWidth = -1; 119 private int mMode = -1; 120 private int mDateX; 121 private int mPaperclipX; 122 private int mFoldersXEnd; 123 private int mSendersWidth; 124 125 /** Whether we're running under test mode. */ 126 private boolean mTesting = false; 127 /** Whether we are on a tablet device or not */ 128 private final boolean mTabletDevice; 129 130 @VisibleForTesting 131 ConversationItemViewCoordinates mCoordinates; 132 133 private final Context mContext; 134 135 private String mAccount; 136 private ConversationItemViewModel mHeader; 137 private ViewMode mViewMode; 138 private boolean mDownEvent; 139 private boolean mChecked = false; 140 private static int sFadedColor = -1; 141 private static int sFadedActivatedColor = -1; 142 private ConversationSelectionSet mSelectedConversationSet; 143 private Folder mDisplayedFolder; 144 private boolean mPriorityMarkersEnabled; 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 // Personal level. 472 mHeader.personalLevelBitmap = null; 473 if (mCoordinates.showPersonalLevel) { 474 int personalLevel = mHeader.personalLevel; 475 final boolean isImportant = 476 mHeader.priority == UIProvider.ConversationPriority.IMPORTANT; 477 // TODO(mindyp): get whether importance indicators are enabled 478 // mPriorityMarkersEnabled = 479 // persistence.getPriorityInboxArrowsEnabled(mContext, mAccount); 480 boolean useImportantMarkers = isImportant && mPriorityMarkersEnabled; 481 482 if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) { 483 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME 484 : ONLY_TO_ME; 485 } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) { 486 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS 487 : TO_ME_AND_OTHERS; 488 } else if (useImportantMarkers) { 489 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS; 490 } 491 } 492 493 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 494 495 // Subject. 496 createSubjectSpans(isUnread); 497 498 // Parse senders fragments. 499 parseSendersFragments(isUnread); 500 501 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT); 502 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS); 503 } 504 505 private void createSubjectSpans(boolean isUnread) { 506 final String subject = filterTag(mHeader.conversation.subject); 507 final String snippet = mHeader.conversation.snippet; 508 int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ; 509 int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ; 510 mHeader.subjectText = new SpannableStringBuilder(mContext.getString( 511 R.string.subject_and_snippet, subject, snippet)); 512 if (isUnread) { 513 mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(), 514 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 515 } 516 int fontColor = getFontColor(subjectColor); 517 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0, 518 subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 519 fontColor = getFontColor(snippetColor); 520 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1, 521 mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 522 } 523 524 private int getFontColor(int defaultColor) { 525 return isActivated() && mTabletDevice ? ACTIVATED_TEXT_COLOR 526 : defaultColor; 527 } 528 529 private void layoutSubject() { 530 sPaint.setTextSize(mCoordinates.subjectFontSize); 531 sPaint.setColor(mHeader.fontColor); 532 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint, 533 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 534 if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) { 535 int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1); 536 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end), 537 sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true); 538 } 539 } 540 541 /** 542 * Parses senders text into small fragments. 543 */ 544 private void parseSendersFragments(boolean isUnread) { 545 if (TextUtils.isEmpty(mHeader.conversation.senders)) { 546 return; 547 } 548 mHeader.sendersText = formatSenders(mHeader.conversation.senders); 549 mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true); 550 } 551 552 private String formatSenders(String sendersString) { 553 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 554 String[] namesOnly = new String[senders.length]; 555 Rfc822Token[] senderTokens; 556 String display; 557 for (int i = 0; i < senders.length; i++) { 558 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 559 if (senderTokens != null && senderTokens.length > 0) { 560 display = senderTokens[0].getName(); 561 if (TextUtils.isEmpty(display)) { 562 display = senderTokens[0].getAddress(); 563 } 564 namesOnly[i] = display; 565 } 566 } 567 return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly); 568 } 569 570 private boolean canFitFragment(int width, int line, int fixedWidth) { 571 if (line == mCoordinates.sendersLineCount) { 572 return width + fixedWidth <= mSendersWidth; 573 } else { 574 return width <= mSendersWidth; 575 } 576 } 577 578 private void calculateCoordinates() { 579 startTimer(PERF_TAG_CALCULATE_COORDINATES); 580 581 sPaint.setTextSize(mCoordinates.dateFontSize); 582 sPaint.setTypeface(Typeface.DEFAULT); 583 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText); 584 585 mPaperclipX = mDateX - ATTACHMENT.getWidth(); 586 587 int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.folder_cell_width); 588 589 if (mCoordinates.showFolders) { 590 if (ConversationItemViewCoordinates.displayFoldersAboveDate(mCoordinates.showFolders, 591 mMode)) { 592 mFoldersXEnd = mCoordinates.dateXEnd; 593 mSendersWidth = mCoordinates.sendersWidth; 594 } else { 595 if (mHeader.paperclip != null) { 596 mFoldersXEnd = mPaperclipX; 597 } else { 598 mFoldersXEnd = mDateX - cellWidth / 2; 599 } 600 mSendersWidth = mFoldersXEnd - mCoordinates.sendersX - 2 * cellWidth; 601 if (mHeader.folderDisplayer.hasVisibleFolders()) { 602 mSendersWidth -= ConversationItemViewCoordinates.getFoldersWidth(mContext, 603 mMode); 604 } 605 } 606 } else { 607 int dateAttachmentStart = 0; 608 // Have this end near the paperclip or date, not the folders. 609 if (mHeader.paperclip != null) { 610 dateAttachmentStart = mPaperclipX; 611 } else { 612 dateAttachmentStart = mDateX; 613 } 614 mSendersWidth = dateAttachmentStart - mCoordinates.sendersX - cellWidth; 615 } 616 617 if (mHeader.isLayoutValid(mContext)) { 618 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 619 return; 620 } 621 622 // Layout subject. 623 layoutSubject(); 624 625 // First pass to calculate width of each fragment. 626 int totalWidth = 0; 627 int fixedWidth = 0; 628 sPaint.setTextSize(mCoordinates.sendersFontSize); 629 sPaint.setTypeface(Typeface.DEFAULT); 630 for (SenderFragment senderFragment : mHeader.senderFragments) { 631 CharacterStyle style = senderFragment.style; 632 int start = senderFragment.start; 633 int end = senderFragment.end; 634 style.updateDrawState(sPaint); 635 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end); 636 boolean isFixed = senderFragment.isFixed; 637 if (isFixed) { 638 fixedWidth += senderFragment.width; 639 } 640 totalWidth += senderFragment.width; 641 } 642 643 // Second pass to layout each fragment. 644 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent; 645 if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) { 646 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0; 647 } 648 totalWidth = 0; 649 int currentLine = 1; 650 boolean ellipsize = false; 651 for (SenderFragment senderFragment : mHeader.senderFragments) { 652 CharacterStyle style = senderFragment.style; 653 int start = senderFragment.start; 654 int end = senderFragment.end; 655 int width = senderFragment.width; 656 boolean isFixed = senderFragment.isFixed; 657 style.updateDrawState(sPaint); 658 659 // No more width available, we'll only show fixed fragments. 660 if (ellipsize && !isFixed) { 661 senderFragment.shouldDisplay = false; 662 continue; 663 } 664 665 // New line and ellipsize text if needed. 666 senderFragment.ellipsizedText = null; 667 if (isFixed) { 668 fixedWidth -= width; 669 } 670 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) { 671 // The text is too long, new line won't help. We have to 672 // ellipsize text. 673 if (totalWidth == 0) { 674 ellipsize = true; 675 } else { 676 // New line. 677 if (currentLine < mCoordinates.sendersLineCount) { 678 currentLine++; 679 sendersY += mCoordinates.sendersLineHeight; 680 totalWidth = 0; 681 // The text is still too long, we have to ellipsize 682 // text. 683 if (totalWidth + width > mSendersWidth) { 684 ellipsize = true; 685 } 686 } else { 687 ellipsize = true; 688 } 689 } 690 691 if (ellipsize) { 692 width = mSendersWidth - totalWidth; 693 // No more new line, we have to reserve width for fixed 694 // fragments. 695 if (currentLine == mCoordinates.sendersLineCount) { 696 width -= fixedWidth; 697 } 698 senderFragment.ellipsizedText = TextUtils.ellipsize( 699 mHeader.sendersText.substring(start, end), sPaint, width, 700 TruncateAt.END).toString(); 701 width = (int) sPaint.measureText(senderFragment.ellipsizedText); 702 } 703 } 704 senderFragment.x = mCoordinates.sendersX + totalWidth; 705 senderFragment.y = sendersY; 706 senderFragment.shouldDisplay = true; 707 totalWidth += width; 708 } 709 710 pauseTimer(PERF_TAG_CALCULATE_COORDINATES); 711 } 712 713 /** 714 * If the subject contains the tag of a mailing-list (text surrounded with 715 * []), return the subject with that tag ellipsized, e.g. 716 * "[android-gmail-team] Hello" -> "[andr...] Hello" 717 */ 718 private String filterTag(String subject) { 719 String result = subject; 720 String formatString = getContext().getResources().getString(R.string.filtered_tag); 721 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') { 722 int end = subject.indexOf(']'); 723 if (end > 0) { 724 String tag = subject.substring(1, end); 725 result = String.format(formatString, Utils.ellipsize(tag, 7), 726 subject.substring(end + 1)); 727 } 728 } 729 return result; 730 } 731 732 @Override 733 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 734 int width = measureWidth(widthMeasureSpec); 735 int height = measureHeight(heightMeasureSpec, 736 ConversationItemViewCoordinates.getMode(mContext, mViewMode)); 737 setMeasuredDimension(width, height); 738 } 739 740 /** 741 * Determine the width of this view. 742 * 743 * @param measureSpec A measureSpec packed into an int 744 * @return The width of the view, honoring constraints from measureSpec 745 */ 746 private int measureWidth(int measureSpec) { 747 int result = 0; 748 int specMode = MeasureSpec.getMode(measureSpec); 749 int specSize = MeasureSpec.getSize(measureSpec); 750 751 if (specMode == MeasureSpec.EXACTLY) { 752 // We were told how big to be 753 result = specSize; 754 } else { 755 // Measure the text 756 result = mViewWidth; 757 if (specMode == MeasureSpec.AT_MOST) { 758 // Respect AT_MOST value if that was what is called for by 759 // measureSpec 760 result = Math.min(result, specSize); 761 } 762 } 763 return result; 764 } 765 766 /** 767 * Determine the height of this view. 768 * 769 * @param measureSpec A measureSpec packed into an int 770 * @param mode The current mode of this view 771 * @return The height of the view, honoring constraints from measureSpec 772 */ 773 private int measureHeight(int measureSpec, int mode) { 774 int result = 0; 775 int specMode = MeasureSpec.getMode(measureSpec); 776 int specSize = MeasureSpec.getSize(measureSpec); 777 778 if (specMode == MeasureSpec.EXACTLY) { 779 // We were told how big to be 780 result = specSize; 781 } else { 782 // Measure the text 783 result = ConversationItemViewCoordinates.getHeight(mContext, mode); 784 if (specMode == MeasureSpec.AT_MOST) { 785 // Respect AT_MOST value if that was what is called for by 786 // measureSpec 787 result = Math.min(result, specSize); 788 } 789 } 790 return result; 791 } 792 793 @Override 794 protected void onDraw(Canvas canvas) { 795 // Check mark. 796 if (mHeader.checkboxVisible) { 797 Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF; 798 canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint); 799 } 800 801 // Personal Level. 802 if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) { 803 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX, 804 mCoordinates.personalLevelY, sPaint); 805 } 806 807 // Senders. 808 sPaint.setTextSize(mCoordinates.sendersFontSize); 809 sPaint.setTypeface(Typeface.DEFAULT); 810 boolean isUnread = mHeader.unread; 811 int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD 812 : SENDERS_TEXT_COLOR_READ); 813 sPaint.setColor(sendersColor); 814 for (SenderFragment fragment : mHeader.senderFragments) { 815 if (fragment.shouldDisplay) { 816 sPaint.setTypeface(Typeface.DEFAULT); 817 fragment.style.updateDrawState(sPaint); 818 if (fragment.ellipsizedText != null) { 819 canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint); 820 } else { 821 canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x, 822 fragment.y, sPaint); 823 } 824 } 825 } 826 827 // Subject. 828 sPaint.setTextSize(mCoordinates.subjectFontSize); 829 sPaint.setTypeface(Typeface.DEFAULT); 830 sPaint.setColor(mHeader.fontColor); 831 canvas.save(); 832 canvas.translate(mCoordinates.subjectX, 833 mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding()); 834 mHeader.subjectLayout.draw(canvas); 835 canvas.restore(); 836 837 // Folders. 838 if (mCoordinates.showFolders) { 839 mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode); 840 } 841 842 // Date background: shown when there is an attachment or a visible 843 // folder. 844 if (!isActivated() 845 && mHeader.conversation.hasAttachments 846 && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) { 847 mHeader.dateBackground = DATE_BACKGROUND; 848 int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX) 849 - DATE_BACKGROUND_PADDING_LEFT; 850 int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY; 851 Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground 852 .getHeight()); 853 Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight); 854 canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint); 855 } else { 856 mHeader.dateBackground = null; 857 } 858 859 // Date. 860 sPaint.setTextSize(mCoordinates.dateFontSize); 861 sPaint.setTypeface(Typeface.DEFAULT); 862 sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ); 863 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent, 864 sPaint); 865 866 // Paper clip icon. 867 if (mHeader.paperclip != null) { 868 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint); 869 } 870 871 if (mHeader.faded) { 872 int fadedColor = -1; 873 if (sFadedActivatedColor == -1) { 874 sFadedActivatedColor = mContext.getResources().getColor( 875 R.color.faded_activated_conversation_header); 876 } 877 fadedColor = sFadedActivatedColor; 878 int restoreState = canvas.save(); 879 Rect bounds = canvas.getClipBounds(); 880 canvas.clipRect(bounds.left, bounds.top, bounds.right 881 - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width), 882 bounds.bottom); 883 canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor), 884 Color.green(fadedColor), Color.blue(fadedColor)); 885 canvas.restoreToCount(restoreState); 886 } 887 888 // Star. 889 canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint); 890 } 891 892 private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 893 canvas.drawText(s, 0, s.length(), x, y, paint); 894 } 895 896 private void updateBackground(boolean isUnread) { 897 if (isUnread) { 898 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) { 899 if (mChecked) { 900 setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo); 901 } else { 902 setBackgroundResource(R.drawable.conversation_wide_unread_selector); 903 } 904 } else { 905 if (mChecked) { 906 setCheckedActivatedBackground(); 907 } else { 908 setBackgroundResource(R.drawable.conversation_unread_selector); 909 } 910 } 911 } else { 912 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) { 913 if (mChecked) { 914 setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo); 915 } else { 916 setBackgroundResource(R.drawable.conversation_wide_read_selector); 917 } 918 } else { 919 if (mChecked) { 920 setCheckedActivatedBackground(); 921 } else { 922 setBackgroundResource(R.drawable.conversation_read_selector); 923 } 924 } 925 } 926 } 927 928 private void setCheckedActivatedBackground() { 929 if (isActivated() && mTabletDevice) { 930 setBackgroundResource(R.drawable.list_arrow_selected_holo); 931 } else { 932 setBackgroundResource(R.drawable.list_selected_holo); 933 } 934 } 935 936 /** 937 * Toggle the check mark on this view and update the conversation 938 */ 939 public void toggleCheckMark() { 940 mChecked = !mChecked; 941 Conversation conv = mHeader.conversation; 942 // Set the list position of this item in the conversation 943 conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this) 944 : Conversation.NO_POSITION; 945 if (mSelectedConversationSet != null) { 946 mSelectedConversationSet.toggle(conv); 947 } 948 // We update the background after the checked state has changed now that 949 // we have a selected background asset. Setting the background usually 950 // waits for a layout pass, but we don't need a full layout, just an 951 // update to the background. 952 requestLayout(); 953 } 954 955 /** 956 * Toggle the star on this view and update the conversation. 957 */ 958 private void toggleStar() { 959 mHeader.starred = !mHeader.starred; 960 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF; 961 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 962 + mHeader.starBitmap.getWidth(), 963 mCoordinates.starY + mHeader.starBitmap.getHeight()); 964 // Generalize this... 965 mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred); 966 } 967 968 private boolean touchCheckmark(float x, float y) { 969 // Everything before senders and include a touch slop. 970 return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP; 971 } 972 973 private boolean touchStar(float x, float y) { 974 // Everything after the star and include a touch slop. 975 return x > mCoordinates.starX - TOUCH_SLOP; 976 } 977 978 @Override 979 public boolean onTouchEvent(MotionEvent event) { 980 boolean handled = false; 981 982 int x = (int) event.getX(); 983 int y = (int) event.getY(); 984 switch (event.getAction()) { 985 case MotionEvent.ACTION_DOWN: 986 mDownEvent = true; 987 if (touchCheckmark(x, y) || touchStar(x, y)) { 988 handled = true; 989 } 990 break; 991 992 case MotionEvent.ACTION_CANCEL: 993 mDownEvent = false; 994 break; 995 996 case MotionEvent.ACTION_UP: 997 if (mDownEvent) { 998 if (touchCheckmark(x, y)) { 999 // Touch on the check mark 1000 toggleCheckMark(); 1001 } else if (touchStar(x, y)) { 1002 // Touch on the star 1003 toggleStar(); 1004 } 1005 handled = true; 1006 } 1007 break; 1008 } 1009 1010 if (!handled) { 1011 handled = super.onTouchEvent(event); 1012 } 1013 1014 return handled; 1015 } 1016} 1017