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 18 package com.android.mail.browse; 19 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Paint.FontMetricsInt; 23 import android.graphics.Typeface; 24 import android.util.SparseArray; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.View.MeasureSpec; 28 import android.view.ViewGroup; 29 import android.view.ViewGroup.LayoutParams; 30 import android.widget.FrameLayout; 31 import android.widget.TextView; 32 33 import com.android.mail.R; 34 import com.android.mail.R.dimen; 35 import com.android.mail.R.id; 36 import com.android.mail.ui.ViewMode; 37 import com.android.mail.utils.Utils; 38 import com.google.common.base.Objects; 39 40 /** 41 * Represents the coordinates of elements inside a CanvasConversationHeaderView 42 * (eg, checkmark, star, subject, sender, folders, etc.) It will inflate a view, 43 * and record the coordinates of each element after layout. This will allows us 44 * to easily improve performance by creating custom view while still defining 45 * layout in XML files. 46 * 47 * @author phamm 48 */ 49 public class ConversationItemViewCoordinates { 50 // Modes 51 static final int MODE_COUNT = 2; 52 static final int WIDE_MODE = 0; 53 static final int NORMAL_MODE = 1; 54 55 // Left-side gadget modes 56 static final int GADGET_NONE = 0; 57 static final int GADGET_CONTACT_PHOTO = 1; 58 static final int GADGET_CHECKBOX = 2; 59 60 // Attachment previews modes 61 static final int ATTACHMENT_PREVIEW_NONE = 0; 62 static final int ATTACHMENT_PREVIEW_UNREAD = 1; 63 static final int ATTACHMENT_PREVIEW_READ = 2; 64 65 // For combined views 66 private static int COLOR_BLOCK_WIDTH = -1; 67 private static int COLOR_BLOCK_HEIGHT = -1; 68 69 /** 70 * Simple holder class for an item's abstract configuration state. ListView binding creates an 71 * instance per item, and {@link #forConfig(Context, Config, SparseArray)} uses it to hide/show 72 * optional views and determine the correct coordinates for that item configuration. 73 */ 74 public static final class Config { 75 private int mWidth; 76 private int mViewMode = ViewMode.UNKNOWN; 77 private int mGadgetMode = GADGET_NONE; 78 private int mAttachmentPreviewMode = ATTACHMENT_PREVIEW_NONE; 79 private boolean mShowFolders = false; 80 private boolean mShowReplyState = false; 81 private boolean mShowColorBlock = false; 82 private boolean mShowPersonalIndicator = false; 83 84 public Config setViewMode(int viewMode) { 85 mViewMode = viewMode; 86 return this; 87 } 88 89 public Config withGadget(int gadget) { 90 mGadgetMode = gadget; 91 return this; 92 } 93 94 public Config withAttachmentPreviews(int attachmentPreviewMode) { 95 mAttachmentPreviewMode = attachmentPreviewMode; 96 return this; 97 } 98 99 public Config showFolders() { 100 mShowFolders = true; 101 return this; 102 } 103 104 public Config showReplyState() { 105 mShowReplyState = true; 106 return this; 107 } 108 109 public Config showColorBlock() { 110 mShowColorBlock = true; 111 return this; 112 } 113 114 public Config showPersonalIndicator() { 115 mShowPersonalIndicator = true; 116 return this; 117 } 118 119 public Config updateWidth(int width) { 120 mWidth = width; 121 return this; 122 } 123 124 public int getWidth() { 125 return mWidth; 126 } 127 128 public int getViewMode() { 129 return mViewMode; 130 } 131 132 public int getGadgetMode() { 133 return mGadgetMode; 134 } 135 136 public int getAttachmentPreviewMode() { 137 return mAttachmentPreviewMode; 138 } 139 140 public boolean areFoldersVisible() { 141 return mShowFolders; 142 } 143 144 public boolean isReplyStateVisible() { 145 return mShowReplyState; 146 } 147 148 public boolean isColorBlockVisible() { 149 return mShowColorBlock; 150 } 151 152 public boolean isPersonalIndicatorVisible() { 153 return mShowPersonalIndicator; 154 } 155 156 private int getCacheKey() { 157 // hash the attributes that contribute to item height and child view geometry 158 return Objects.hashCode(mWidth, mViewMode, mGadgetMode, mAttachmentPreviewMode, 159 mShowFolders, mShowReplyState, mShowPersonalIndicator); 160 } 161 162 } 163 164 public static class CoordinatesCache { 165 private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache 166 = new SparseArray<ConversationItemViewCoordinates>(); 167 private final SparseArray<View> mViewsCache = new SparseArray<View>(); 168 169 public ConversationItemViewCoordinates getCoordinates(final int key) { 170 return mCoordinatesCache.get(key); 171 } 172 173 public View getView(final int layoutId) { 174 return mViewsCache.get(layoutId); 175 } 176 177 public void put(final int key, final ConversationItemViewCoordinates coords) { 178 mCoordinatesCache.put(key, coords); 179 } 180 181 public void put(final int layoutId, final View view) { 182 mViewsCache.put(layoutId, view); 183 } 184 } 185 186 /** 187 * One of either NORMAL_MODE or WIDE_MODE. 188 */ 189 private final int mMode; 190 191 final int height; 192 193 // Star. 194 final int starX; 195 final int starY; 196 final int starWidth; 197 198 // Senders. 199 final int sendersX; 200 final int sendersY; 201 final int sendersWidth; 202 final int sendersHeight; 203 final int sendersLineCount; 204 final int sendersLineHeight; 205 final float sendersFontSize; 206 207 // Subject. 208 final int subjectX; 209 final int subjectY; 210 final int subjectWidth; 211 final int subjectHeight; 212 final int subjectLineCount; 213 final float subjectFontSize; 214 215 // Folders. 216 final int foldersX; 217 final int foldersXEnd; 218 final int foldersY; 219 final int foldersHeight; 220 final Typeface foldersTypeface; 221 final float foldersFontSize; 222 final int foldersTextBottomPadding; 223 224 // Info icon 225 final int infoIconXEnd; 226 final int infoIconY; 227 228 // Date. 229 final int dateXEnd; 230 final int dateY; 231 final int datePaddingLeft; 232 final float dateFontSize; 233 final int dateYBaseline; 234 235 // Paperclip. 236 final int paperclipY; 237 final int paperclipPaddingLeft; 238 239 // Color block. 240 final int colorBlockX; 241 final int colorBlockY; 242 final int colorBlockWidth; 243 final int colorBlockHeight; 244 245 // Reply state of a conversation. 246 final int replyStateX; 247 final int replyStateY; 248 249 final int personalIndicatorX; 250 final int personalIndicatorY; 251 252 final int contactImagesHeight; 253 final int contactImagesWidth; 254 final int contactImagesX; 255 final int contactImagesY; 256 257 // Attachment previews 258 public final int attachmentPreviewsX; 259 public final int attachmentPreviewsY; 260 final int attachmentPreviewsWidth; 261 final int attachmentPreviewsHeight; 262 public final int attachmentPreviewsDecodeHeight; 263 264 // Attachment previews overflow badge and count 265 public final int overflowXEnd; 266 public final int overflowYEnd; 267 public final int overflowDiameter; 268 public final float overflowFontSize; 269 public final Typeface overflowTypeface; 270 271 // Attachment previews placeholder 272 final int placeholderY; 273 public final int placeholderWidth; 274 public final int placeholderHeight; 275 // Attachment previews progress bar 276 final int progressBarY; 277 public final int progressBarWidth; 278 public final int progressBarHeight; 279 280 /** 281 * The smallest item width for which we use the "wide" layout. 282 */ 283 private final int mMinListWidthForWide; 284 /** 285 * The smallest item width for which we use the "spacious" variant of the normal layout, 286 * if the normal version is used at all. Larger than {@link #mMinListWidthForWide}, we use 287 * wide mode anyway, and this value is unused. 288 */ 289 private final int mMinListWidthIsSpacious; 290 private final int mFolderCellWidth; 291 private final int mFolderMinimumWidth; 292 293 private ConversationItemViewCoordinates(final Context context, final Config config, 294 final CoordinatesCache cache) { 295 Utils.traceBeginSection("CIV coordinates constructor"); 296 final Resources res = context.getResources(); 297 mFolderCellWidth = res.getDimensionPixelSize(R.dimen.folder_cell_width); 298 mMinListWidthForWide = res.getDimensionPixelSize(R.dimen.list_min_width_is_wide); 299 mMinListWidthIsSpacious = res.getDimensionPixelSize( 300 R.dimen.list_normal_mode_min_width_is_spacious); 301 mFolderMinimumWidth = res.getDimensionPixelSize(R.dimen.folder_minimum_width); 302 303 mMode = calculateMode(res, config); 304 305 final int layoutId; 306 if (mMode == WIDE_MODE) { 307 layoutId = R.layout.conversation_item_view_wide; 308 } else { 309 if (config.getWidth() >= mMinListWidthIsSpacious) { 310 layoutId = R.layout.conversation_item_view_normal_spacious; 311 } else { 312 layoutId = R.layout.conversation_item_view_normal; 313 } 314 } 315 316 ViewGroup view = (ViewGroup) cache.getView(layoutId); 317 if (view == null) { 318 view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null); 319 cache.put(layoutId, view); 320 } 321 322 // Show/hide optional views before measure/layout call 323 324 final View attachmentPreviews = view.findViewById(R.id.attachment_previews);; 325 if (config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE) { 326 final LayoutParams params = attachmentPreviews.getLayoutParams(); 327 attachmentPreviews.setVisibility(View.VISIBLE); 328 params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode()); 329 attachmentPreviews.setLayoutParams(params); 330 } else { 331 attachmentPreviews.setVisibility(View.GONE); 332 } 333 attachmentPreviewsDecodeHeight = getAttachmentPreviewsHeight(context, 334 ATTACHMENT_PREVIEW_UNREAD); 335 336 final TextView folders = (TextView) view.findViewById(R.id.folders); 337 folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE); 338 339 // Add margin between attachment previews and folders 340 final View attachmentPreviewsBottomMargin = view 341 .findViewById(R.id.attachment_previews_bottom_margin); 342 final boolean marginVisible = config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE 343 && config.areFoldersVisible(); 344 attachmentPreviewsBottomMargin.setVisibility(marginVisible ? View.VISIBLE : View.GONE); 345 346 View contactImagesView = view.findViewById(R.id.contact_image); 347 348 switch (config.getGadgetMode()) { 349 case GADGET_CONTACT_PHOTO: 350 contactImagesView.setVisibility(View.VISIBLE); 351 break; 352 case GADGET_CHECKBOX: 353 contactImagesView.setVisibility(View.GONE); 354 contactImagesView = null; 355 break; 356 default: 357 contactImagesView.setVisibility(View.GONE); 358 contactImagesView = null; 359 break; 360 } 361 362 final View replyState = view.findViewById(R.id.reply_state); 363 replyState.setVisibility(config.isReplyStateVisible() ? View.VISIBLE : View.GONE); 364 365 final View personalIndicator = view.findViewById(R.id.personal_indicator); 366 personalIndicator.setVisibility( 367 config.isPersonalIndicatorVisible() ? View.VISIBLE : View.GONE); 368 369 // Layout the appropriate view. 370 final int widthSpec = MeasureSpec.makeMeasureSpec(config.getWidth(), MeasureSpec.EXACTLY); 371 final int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 372 373 view.measure(widthSpec, heightSpec); 374 view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 375 376 // Utils.dumpViewTree((ViewGroup) view); 377 378 // Records coordinates. 379 380 // Contact images view 381 if (contactImagesView != null) { 382 contactImagesWidth = contactImagesView.getWidth(); 383 contactImagesHeight = contactImagesView.getHeight(); 384 contactImagesX = getX(contactImagesView); 385 contactImagesY = getY(contactImagesView); 386 } else { 387 contactImagesX = contactImagesY = contactImagesWidth = contactImagesHeight = 0; 388 } 389 390 final View star = view.findViewById(R.id.star); 391 starX = getX(star); 392 starY = getY(star); 393 starWidth = star.getWidth(); 394 395 final TextView senders = (TextView) view.findViewById(R.id.senders); 396 final int sendersTopAdjust = getLatinTopAdjustment(senders); 397 sendersX = getX(senders); 398 sendersY = getY(senders) + sendersTopAdjust; 399 sendersWidth = senders.getWidth(); 400 sendersHeight = senders.getHeight(); 401 sendersLineCount = getLineCount(senders); 402 sendersLineHeight = senders.getLineHeight(); 403 sendersFontSize = senders.getTextSize(); 404 405 final TextView subject = (TextView) view.findViewById(R.id.subject); 406 final int subjectTopAdjust = getLatinTopAdjustment(subject); 407 subjectX = getX(subject); 408 if (isWide()) { 409 subjectY = getY(subject) + subjectTopAdjust; 410 } else { 411 subjectY = getY(subject) + sendersTopAdjust; 412 } 413 subjectWidth = subject.getWidth(); 414 subjectHeight = subject.getHeight(); 415 subjectLineCount = getLineCount(subject); 416 subjectFontSize = subject.getTextSize(); 417 418 if (config.areFoldersVisible()) { 419 // vertically align folders min left edge with subject 420 foldersX = subjectX; 421 foldersXEnd = getX(folders) + folders.getWidth(); 422 if (isWide()) { 423 foldersY = getY(folders); 424 } else { 425 foldersY = getY(folders) + sendersTopAdjust; 426 } 427 foldersHeight = folders.getHeight(); 428 foldersTypeface = folders.getTypeface(); 429 foldersTextBottomPadding = res 430 .getDimensionPixelSize(R.dimen.folders_text_bottom_padding); 431 foldersFontSize = folders.getTextSize(); 432 } else { 433 foldersX = 0; 434 foldersXEnd = 0; 435 foldersY = 0; 436 foldersHeight = 0; 437 foldersTypeface = null; 438 foldersTextBottomPadding = 0; 439 foldersFontSize = 0; 440 } 441 442 final View colorBlock = view.findViewById(R.id.color_block); 443 if (config.isColorBlockVisible() && colorBlock != null) { 444 colorBlockX = getX(colorBlock); 445 colorBlockY = getY(colorBlock); 446 colorBlockWidth = colorBlock.getWidth(); 447 colorBlockHeight = colorBlock.getHeight(); 448 } else { 449 colorBlockX = colorBlockY = colorBlockWidth = colorBlockHeight = 0; 450 } 451 452 if (config.isReplyStateVisible()) { 453 replyStateX = getX(replyState); 454 replyStateY = getY(replyState); 455 } else { 456 replyStateX = replyStateY = 0; 457 } 458 459 if (config.isPersonalIndicatorVisible()) { 460 personalIndicatorX = getX(personalIndicator); 461 personalIndicatorY = getY(personalIndicator); 462 } else { 463 personalIndicatorX = personalIndicatorY = 0; 464 } 465 466 final View infoIcon = view.findViewById(R.id.info_icon); 467 infoIconXEnd = getX(infoIcon) + infoIcon.getWidth(); 468 infoIconY = getY(infoIcon); 469 470 final TextView date = (TextView) view.findViewById(R.id.date); 471 dateXEnd = getX(date) + date.getWidth(); 472 dateY = getY(date); 473 datePaddingLeft = date.getPaddingLeft(); 474 dateFontSize = date.getTextSize(); 475 dateYBaseline = dateY + getLatinTopAdjustment(date) + date.getBaseline(); 476 477 final View paperclip = view.findViewById(R.id.paperclip); 478 paperclipY = getY(paperclip); 479 paperclipPaddingLeft = paperclip.getPaddingLeft(); 480 481 if (attachmentPreviews != null) { 482 attachmentPreviewsX = subjectX; 483 attachmentPreviewsY = getY(attachmentPreviews) + sendersTopAdjust; 484 attachmentPreviewsWidth = subjectWidth; 485 attachmentPreviewsHeight = attachmentPreviews.getHeight(); 486 487 // We only care about the right and bottom of the overflow count 488 final TextView overflow = (TextView) view.findViewById(id.ap_overflow); 489 final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) overflow 490 .getLayoutParams(); 491 overflowXEnd = attachmentPreviewsX + attachmentPreviewsWidth - params.rightMargin; 492 overflowYEnd = attachmentPreviewsY + attachmentPreviewsHeight - params.bottomMargin; 493 overflowDiameter = overflow.getWidth(); 494 overflowFontSize = overflow.getTextSize(); 495 overflowTypeface = overflow.getTypeface(); 496 497 final View placeholder = view.findViewById(id.ap_placeholder); 498 placeholderWidth = placeholder.getWidth(); 499 placeholderHeight = placeholder.getHeight(); 500 placeholderY = attachmentPreviewsY + attachmentPreviewsHeight / 2 501 - placeholderHeight / 2; 502 503 final View progressBar = view.findViewById(id.ap_progress_bar); 504 progressBarWidth = progressBar.getWidth(); 505 progressBarHeight = progressBar.getHeight(); 506 progressBarY = attachmentPreviewsY + attachmentPreviewsHeight / 2 507 - progressBarHeight / 2; 508 } else { 509 attachmentPreviewsX = 0; 510 attachmentPreviewsY = 0; 511 attachmentPreviewsWidth = 0; 512 attachmentPreviewsHeight = 0; 513 overflowXEnd = 0; 514 overflowYEnd = 0; 515 overflowDiameter = 0; 516 overflowFontSize = 0; 517 overflowTypeface = null; 518 placeholderY = 0; 519 placeholderWidth = 0; 520 placeholderHeight = 0; 521 progressBarY = 0; 522 progressBarWidth = 0; 523 progressBarHeight = 0; 524 } 525 526 height = view.getHeight() + (isWide() ? 0 : sendersTopAdjust); 527 Utils.traceEndSection(); 528 } 529 530 public int getMode() { 531 return mMode; 532 } 533 534 public boolean isWide() { 535 return mMode == WIDE_MODE; 536 } 537 538 /** 539 * Returns a negative corrective value that you can apply to a TextView's vertical dimensions 540 * that will nudge the first line of text upwards such that uppercase Latin characters are 541 * truly top-aligned. 542 * <p> 543 * N.B. this will cause other characters to draw above the top! only use this if you have 544 * adequate top margin. 545 * 546 */ 547 private static int getLatinTopAdjustment(TextView t) { 548 final FontMetricsInt fmi = t.getPaint().getFontMetricsInt(); 549 return (fmi.top - fmi.ascent); 550 } 551 552 /** 553 * Returns the mode of the header view (Wide/Normal). 554 */ 555 private int calculateMode(Resources res, Config config) { 556 switch (config.getViewMode()) { 557 case ViewMode.CONVERSATION_LIST: 558 return config.getWidth() >= mMinListWidthForWide ? WIDE_MODE : NORMAL_MODE; 559 560 case ViewMode.SEARCH_RESULTS_LIST: 561 return res.getInteger(R.integer.conversation_list_search_header_mode); 562 563 default: 564 return res.getInteger(R.integer.conversation_header_mode); 565 } 566 } 567 568 private int getAttachmentPreviewsHeight(final Context context, 569 final int attachmentPreviewMode) { 570 final Resources res = context.getResources(); 571 switch (attachmentPreviewMode) { 572 case ATTACHMENT_PREVIEW_UNREAD: 573 return (int) (isWide() ? res.getDimension(dimen.attachment_preview_height_tall_wide) 574 : res.getDimension(dimen.attachment_preview_height_tall)); 575 case ATTACHMENT_PREVIEW_READ: 576 return (int) res.getDimension(dimen.attachment_preview_height_short); 577 default: 578 return 0; 579 } 580 } 581 582 /** 583 * Returns the x coordinates of a view by tracing up its hierarchy. 584 */ 585 private static int getX(View view) { 586 int x = 0; 587 while (view != null) { 588 x += (int) view.getX(); 589 view = (View) view.getParent(); 590 } 591 return x; 592 } 593 594 /** 595 * Returns the y coordinates of a view by tracing up its hierarchy. 596 */ 597 private static int getY(View view) { 598 int y = 0; 599 while (view != null) { 600 y += (int) view.getY(); 601 view = (View) view.getParent(); 602 } 603 return y; 604 } 605 606 /** 607 * Returns the number of lines of this text view. Delegates to built-in TextView logic on JB+. 608 */ 609 private static int getLineCount(TextView textView) { 610 if (Utils.isRunningJellybeanOrLater()) { 611 return textView.getMaxLines(); 612 } else { 613 return Math.round(((float) textView.getHeight()) / textView.getLineHeight()); 614 } 615 } 616 617 /** 618 * Returns the length (maximum of characters) of subject in this mode. 619 */ 620 public static int getSendersLength(Context context, int mode, boolean hasAttachments) { 621 final Resources res = context.getResources(); 622 if (hasAttachments) { 623 return res.getIntArray(R.array.senders_with_attachment_lengths)[mode]; 624 } else { 625 return res.getIntArray(R.array.senders_lengths)[mode]; 626 } 627 } 628 629 @Deprecated 630 public static int getColorBlockWidth(Context context) { 631 Resources res = context.getResources(); 632 if (COLOR_BLOCK_WIDTH <= 0) { 633 COLOR_BLOCK_WIDTH = res.getDimensionPixelSize(R.dimen.color_block_width); 634 } 635 return COLOR_BLOCK_WIDTH; 636 } 637 638 @Deprecated 639 public static int getColorBlockHeight(Context context) { 640 Resources res = context.getResources(); 641 if (COLOR_BLOCK_HEIGHT <= 0) { 642 COLOR_BLOCK_HEIGHT = res.getDimensionPixelSize(R.dimen.color_block_height); 643 } 644 return COLOR_BLOCK_HEIGHT; 645 } 646 647 public static boolean displaySendersInline(int mode) { 648 switch (mode) { 649 case WIDE_MODE: 650 return false; 651 case NORMAL_MODE: 652 return true; 653 default: 654 throw new IllegalArgumentException("Unknown conversation header view mode " + mode); 655 } 656 } 657 658 /** 659 * Returns coordinates for elements inside a conversation header view given 660 * the view width. 661 */ 662 public static ConversationItemViewCoordinates forConfig(final Context context, 663 final Config config, final CoordinatesCache cache) { 664 final int cacheKey = config.getCacheKey(); 665 ConversationItemViewCoordinates coordinates = cache.getCoordinates(cacheKey); 666 if (coordinates != null) { 667 return coordinates; 668 } 669 670 coordinates = new ConversationItemViewCoordinates(context, config, cache); 671 cache.put(cacheKey, coordinates); 672 return coordinates; 673 } 674 675 /** 676 * Return the minimum width of a folder cell with no text. Essentially this is the left+right 677 * intra-cell margin within cells. 678 * 679 */ 680 public int getFolderCellWidth() { 681 return mFolderCellWidth; 682 } 683 684 /** 685 * Return the minimum width of a folder cell, period. This will affect the 686 * maximum number of folders we can display. 687 */ 688 public int getFolderMinimumWidth() { 689 return mFolderMinimumWidth; 690 } 691 692 public static boolean isWideMode(int mode) { 693 return mode == WIDE_MODE; 694 } 695 696 } 697