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