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.annotation.SuppressLint; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Paint.FontMetricsInt; 24 import android.graphics.Typeface; 25 import android.support.v4.view.ViewCompat; 26 import android.util.SparseArray; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.View.MeasureSpec; 30 import android.view.ViewGroup; 31 import android.widget.TextView; 32 33 import com.android.mail.R; 34 import com.android.mail.ui.ViewMode; 35 import com.android.mail.utils.Utils; 36 import com.android.mail.utils.ViewUtils; 37 import com.google.common.base.Objects; 38 39 /** 40 * Represents the coordinates of elements inside a CanvasConversationHeaderView 41 * (eg, checkmark, star, subject, sender, folders, etc.) It will inflate a view, 42 * and record the coordinates of each element after layout. This will allows us 43 * to easily improve performance by creating custom view while still defining 44 * layout in XML files. 45 * 46 * @author phamm 47 */ 48 public class ConversationItemViewCoordinates { 49 private static final int SINGLE_LINE = 1; 50 51 // Modes 52 static final int MODE_COUNT = 2; 53 static final int WIDE_MODE = 0; 54 static final int NORMAL_MODE = 1; 55 56 // Left-side gadget modes 57 static final int GADGET_NONE = 0; 58 static final int GADGET_CONTACT_PHOTO = 1; 59 static final int GADGET_CHECKBOX = 2; 60 61 /** 62 * Simple holder class for an item's abstract configuration state. ListView binding creates an 63 * instance per item, and {@link #forConfig(Context, Config, CoordinatesCache)} uses it to 64 * hide/show optional views and determine the correct coordinates for that item configuration. 65 */ 66 public static final class Config { 67 private int mWidth; 68 private int mViewMode = ViewMode.UNKNOWN; 69 private int mGadgetMode = GADGET_NONE; 70 private int mLayoutDirection = View.LAYOUT_DIRECTION_LTR; 71 private boolean mShowFolders = false; 72 private boolean mShowReplyState = false; 73 private boolean mShowColorBlock = false; 74 private boolean mShowPersonalIndicator = false; 75 private boolean mUseFullMargins = false; 76 setViewMode(int viewMode)77 public Config setViewMode(int viewMode) { 78 mViewMode = viewMode; 79 return this; 80 } 81 withGadget(int gadget)82 public Config withGadget(int gadget) { 83 mGadgetMode = gadget; 84 return this; 85 } 86 showFolders()87 public Config showFolders() { 88 mShowFolders = true; 89 return this; 90 } 91 showReplyState()92 public Config showReplyState() { 93 mShowReplyState = true; 94 return this; 95 } 96 showColorBlock()97 public Config showColorBlock() { 98 mShowColorBlock = true; 99 return this; 100 } 101 showPersonalIndicator()102 public Config showPersonalIndicator() { 103 mShowPersonalIndicator = true; 104 return this; 105 } 106 updateWidth(int width)107 public Config updateWidth(int width) { 108 mWidth = width; 109 return this; 110 } 111 getWidth()112 public int getWidth() { 113 return mWidth; 114 } 115 getViewMode()116 public int getViewMode() { 117 return mViewMode; 118 } 119 getGadgetMode()120 public int getGadgetMode() { 121 return mGadgetMode; 122 } 123 areFoldersVisible()124 public boolean areFoldersVisible() { 125 return mShowFolders; 126 } 127 isReplyStateVisible()128 public boolean isReplyStateVisible() { 129 return mShowReplyState; 130 } 131 isColorBlockVisible()132 public boolean isColorBlockVisible() { 133 return mShowColorBlock; 134 } 135 isPersonalIndicatorVisible()136 public boolean isPersonalIndicatorVisible() { 137 return mShowPersonalIndicator; 138 } 139 getCacheKey()140 private int getCacheKey() { 141 // hash the attributes that contribute to item height and child view geometry 142 return Objects.hashCode(mWidth, mViewMode, mGadgetMode, mShowFolders, mShowReplyState, 143 mShowPersonalIndicator, mLayoutDirection, mUseFullMargins); 144 } 145 setLayoutDirection(int layoutDirection)146 public Config setLayoutDirection(int layoutDirection) { 147 mLayoutDirection = layoutDirection; 148 return this; 149 } 150 getLayoutDirection()151 public int getLayoutDirection() { 152 return mLayoutDirection; 153 } 154 setUseFullMargins(boolean useFullMargins)155 public Config setUseFullMargins(boolean useFullMargins) { 156 mUseFullMargins = useFullMargins; 157 return this; 158 } 159 useFullPadding()160 public boolean useFullPadding() { 161 return mUseFullMargins; 162 } 163 } 164 165 public static class CoordinatesCache { 166 private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache 167 = new SparseArray<ConversationItemViewCoordinates>(); 168 private final SparseArray<View> mViewsCache = new SparseArray<View>(); 169 getCoordinates(final int key)170 public ConversationItemViewCoordinates getCoordinates(final int key) { 171 return mCoordinatesCache.get(key); 172 } 173 getView(final int layoutId)174 public View getView(final int layoutId) { 175 return mViewsCache.get(layoutId); 176 } 177 put(final int key, final ConversationItemViewCoordinates coords)178 public void put(final int key, final ConversationItemViewCoordinates coords) { 179 mCoordinatesCache.put(key, coords); 180 } 181 put(final int layoutId, final View view)182 public void put(final int layoutId, final View view) { 183 mViewsCache.put(layoutId, view); 184 } 185 } 186 187 /** 188 * One of either NORMAL_MODE or WIDE_MODE. 189 */ 190 private final int mMode; 191 192 final int height; 193 194 // Star. 195 final int starX; 196 final int starY; 197 final int starWidth; 198 199 // Senders. 200 final int sendersX; 201 final int sendersY; 202 final int sendersWidth; 203 final int sendersHeight; 204 final int sendersLineCount; 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 float subjectFontSize; 213 214 // Snippet. 215 final int snippetX; 216 final int snippetY; 217 final int maxSnippetWidth; 218 final int snippetHeight; 219 final float snippetFontSize; 220 221 // Folders. 222 final int folderLayoutWidth; 223 final int folderCellWidth; 224 final int foldersLeft; 225 final int foldersRight; 226 final int foldersY; 227 final int foldersHeight; 228 final Typeface foldersTypeface; 229 final float foldersFontSize; 230 final int foldersTextBottomPadding; 231 232 // Info icon 233 final int infoIconX; 234 final int infoIconXRight; 235 final int infoIconY; 236 237 // Date. 238 final int dateX; 239 final int dateXRight; 240 final int dateY; 241 final int datePaddingStart; 242 final float dateFontSize; 243 final int dateYBaseline; 244 245 // Paperclip. 246 final int paperclipY; 247 final int paperclipPaddingStart; 248 249 // Color block. 250 final int colorBlockX; 251 final int colorBlockY; 252 final int colorBlockWidth; 253 final int colorBlockHeight; 254 255 // Reply state of a conversation. 256 final int replyStateX; 257 final int replyStateY; 258 259 final int personalIndicatorX; 260 final int personalIndicatorY; 261 262 final int contactImagesHeight; 263 final int contactImagesWidth; 264 final int contactImagesX; 265 final int contactImagesY; 266 267 268 /** 269 * The smallest item width for which we use the "wide" layout. 270 */ 271 private final int mMinListWidthForWide; 272 ConversationItemViewCoordinates(final Context context, final Config config, final CoordinatesCache cache)273 private ConversationItemViewCoordinates(final Context context, final Config config, 274 final CoordinatesCache cache) { 275 Utils.traceBeginSection("CIV coordinates constructor"); 276 final Resources res = context.getResources(); 277 mMinListWidthForWide = res.getDimensionPixelSize(R.dimen.list_min_width_is_wide); 278 279 mMode = calculateMode(res, config); 280 281 final int layoutId = R.layout.conversation_item_view; 282 283 ViewGroup view = (ViewGroup) cache.getView(layoutId); 284 if (view == null) { 285 view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null); 286 cache.put(layoutId, view); 287 } 288 289 // Show/hide optional views before measure/layout call 290 final TextView folders = (TextView) view.findViewById(R.id.folders); 291 folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE); 292 293 View contactImagesView = view.findViewById(R.id.contact_image); 294 295 switch (config.getGadgetMode()) { 296 case GADGET_CONTACT_PHOTO: 297 contactImagesView.setVisibility(View.VISIBLE); 298 break; 299 case GADGET_CHECKBOX: 300 contactImagesView.setVisibility(View.GONE); 301 contactImagesView = null; 302 break; 303 default: 304 contactImagesView.setVisibility(View.GONE); 305 contactImagesView = null; 306 break; 307 } 308 309 final View replyState = view.findViewById(R.id.reply_state); 310 replyState.setVisibility(config.isReplyStateVisible() ? View.VISIBLE : View.GONE); 311 312 final View personalIndicator = view.findViewById(R.id.personal_indicator); 313 personalIndicator.setVisibility( 314 config.isPersonalIndicatorVisible() ? View.VISIBLE : View.GONE); 315 316 setFramePadding(context, view, config.useFullPadding()); 317 318 // Layout the appropriate view. 319 ViewCompat.setLayoutDirection(view, config.getLayoutDirection()); 320 final int widthSpec = MeasureSpec.makeMeasureSpec(config.getWidth(), MeasureSpec.EXACTLY); 321 final int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 322 323 view.measure(widthSpec, heightSpec); 324 view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 325 326 // Once the view is measured, let's calculate the dynamic width variables. 327 folderLayoutWidth = (int) (view.getWidth() * 328 res.getInteger(R.integer.folder_max_width_proportion) / 100.0); 329 folderCellWidth = (int) (view.getWidth() * 330 res.getInteger(R.integer.folder_cell_max_width_proportion) / 100.0); 331 332 // Utils.dumpViewTree((ViewGroup) view); 333 334 // Records coordinates. 335 336 // Contact images view 337 if (contactImagesView != null) { 338 contactImagesWidth = contactImagesView.getWidth(); 339 contactImagesHeight = contactImagesView.getHeight(); 340 contactImagesX = getX(contactImagesView); 341 contactImagesY = getY(contactImagesView); 342 } else { 343 contactImagesX = contactImagesY = contactImagesWidth = contactImagesHeight = 0; 344 } 345 346 final boolean isRtl = ViewUtils.isViewRtl(view); 347 348 final View star = view.findViewById(R.id.star); 349 final int starPadding = res.getDimensionPixelSize(R.dimen.conv_list_star_padding_start); 350 starX = getX(star) + (isRtl ? 0 : starPadding); 351 starY = getY(star); 352 starWidth = star.getWidth(); 353 354 final TextView senders = (TextView) view.findViewById(R.id.senders); 355 final int sendersTopAdjust = getLatinTopAdjustment(senders); 356 sendersX = getX(senders); 357 sendersY = getY(senders) + sendersTopAdjust; 358 sendersWidth = senders.getWidth(); 359 sendersHeight = senders.getHeight(); 360 sendersLineCount = SINGLE_LINE; 361 sendersFontSize = senders.getTextSize(); 362 363 final TextView subject = (TextView) view.findViewById(R.id.subject); 364 final int subjectTopAdjust = getLatinTopAdjustment(subject); 365 subjectX = getX(subject); 366 subjectY = getY(subject) + subjectTopAdjust; 367 subjectWidth = subject.getWidth(); 368 subjectHeight = subject.getHeight(); 369 subjectFontSize = subject.getTextSize(); 370 371 final TextView snippet = (TextView) view.findViewById(R.id.snippet); 372 final int snippetTopAdjust = getLatinTopAdjustment(snippet); 373 snippetX = getX(snippet); 374 snippetY = getY(snippet) + snippetTopAdjust; 375 maxSnippetWidth = snippet.getWidth(); 376 snippetHeight = snippet.getHeight(); 377 snippetFontSize = snippet.getTextSize(); 378 379 if (config.areFoldersVisible()) { 380 // vertically align folders min left edge with subject 381 foldersLeft = getX(folders); 382 foldersRight = foldersLeft + folders.getWidth(); 383 foldersY = getY(folders) + sendersTopAdjust; 384 foldersHeight = folders.getHeight(); 385 foldersTypeface = folders.getTypeface(); 386 foldersTextBottomPadding = res 387 .getDimensionPixelSize(R.dimen.folders_text_bottom_padding); 388 foldersFontSize = folders.getTextSize(); 389 } else { 390 foldersLeft = 0; 391 foldersRight = 0; 392 foldersY = 0; 393 foldersHeight = 0; 394 foldersTypeface = null; 395 foldersTextBottomPadding = 0; 396 foldersFontSize = 0; 397 } 398 399 final View colorBlock = view.findViewById(R.id.color_block); 400 if (config.isColorBlockVisible() && colorBlock != null) { 401 colorBlockX = getX(colorBlock); 402 colorBlockY = getY(colorBlock); 403 colorBlockWidth = colorBlock.getWidth(); 404 colorBlockHeight = colorBlock.getHeight(); 405 } else { 406 colorBlockX = colorBlockY = colorBlockWidth = colorBlockHeight = 0; 407 } 408 409 if (config.isReplyStateVisible()) { 410 replyStateX = getX(replyState); 411 replyStateY = getY(replyState); 412 } else { 413 replyStateX = replyStateY = 0; 414 } 415 416 if (config.isPersonalIndicatorVisible()) { 417 personalIndicatorX = getX(personalIndicator); 418 personalIndicatorY = getY(personalIndicator); 419 } else { 420 personalIndicatorX = personalIndicatorY = 0; 421 } 422 423 final View infoIcon = view.findViewById(R.id.info_icon); 424 infoIconX = getX(infoIcon); 425 infoIconXRight = infoIconX + infoIcon.getWidth(); 426 infoIconY = getY(infoIcon); 427 428 final TextView date = (TextView) view.findViewById(R.id.date); 429 dateX = getX(date); 430 dateXRight = dateX + date.getWidth(); 431 dateY = getY(date); 432 datePaddingStart = ViewUtils.getPaddingStart(date); 433 dateFontSize = date.getTextSize(); 434 dateYBaseline = dateY + getLatinTopAdjustment(date) + date.getBaseline(); 435 436 final View paperclip = view.findViewById(R.id.paperclip); 437 paperclipY = getY(paperclip); 438 paperclipPaddingStart = ViewUtils.getPaddingStart(paperclip); 439 440 height = view.getHeight() + sendersTopAdjust; 441 Utils.traceEndSection(); 442 } 443 444 @SuppressLint("NewApi") setFramePadding(Context context, ViewGroup view, boolean useFullPadding)445 private static void setFramePadding(Context context, ViewGroup view, boolean useFullPadding) { 446 final Resources res = context.getResources(); 447 final int padding = res.getDimensionPixelSize(useFullPadding ? 448 R.dimen.conv_list_card_border_padding : R.dimen.conv_list_no_border_padding); 449 450 final View frame = view.findViewById(R.id.conversation_item_frame); 451 if (Utils.isRunningJBMR1OrLater()) { 452 // start, top, end, bottom 453 frame.setPaddingRelative(frame.getPaddingStart(), padding, 454 frame.getPaddingEnd(), padding); 455 } else { 456 frame.setPadding(frame.getPaddingLeft(), padding, frame.getPaddingRight(), padding); 457 } 458 } 459 getMode()460 public int getMode() { 461 return mMode; 462 } 463 464 /** 465 * Returns a negative corrective value that you can apply to a TextView's vertical dimensions 466 * that will nudge the first line of text upwards such that uppercase Latin characters are 467 * truly top-aligned. 468 * <p> 469 * N.B. this will cause other characters to draw above the top! only use this if you have 470 * adequate top margin. 471 * 472 */ getLatinTopAdjustment(TextView t)473 private static int getLatinTopAdjustment(TextView t) { 474 final FontMetricsInt fmi = t.getPaint().getFontMetricsInt(); 475 return (fmi.top - fmi.ascent); 476 } 477 478 /** 479 * Returns the mode of the header view (Wide/Normal). 480 */ calculateMode(Resources res, Config config)481 private int calculateMode(Resources res, Config config) { 482 switch (config.getViewMode()) { 483 case ViewMode.CONVERSATION_LIST: 484 return config.getWidth() >= mMinListWidthForWide ? WIDE_MODE : NORMAL_MODE; 485 486 case ViewMode.SEARCH_RESULTS_LIST: 487 return res.getInteger(R.integer.conversation_list_search_header_mode); 488 489 default: 490 return res.getInteger(R.integer.conversation_header_mode); 491 } 492 } 493 494 /** 495 * Returns the x coordinates of a view by tracing up its hierarchy. 496 */ getX(View view)497 private static int getX(View view) { 498 int x = 0; 499 while (view != null) { 500 x += (int) view.getX(); 501 view = (View) view.getParent(); 502 } 503 return x; 504 } 505 506 /** 507 * Returns the y coordinates of a view by tracing up its hierarchy. 508 */ getY(View view)509 private static int getY(View view) { 510 int y = 0; 511 while (view != null) { 512 y += (int) view.getY(); 513 view = (View) view.getParent(); 514 } 515 return y; 516 } 517 518 /** 519 * Returns the length (maximum of characters) of subject in this mode. 520 */ getSendersLength(Context context, int mode, boolean hasAttachments)521 public static int getSendersLength(Context context, int mode, boolean hasAttachments) { 522 final Resources res = context.getResources(); 523 if (hasAttachments) { 524 return res.getIntArray(R.array.senders_with_attachment_lengths)[mode]; 525 } else { 526 return res.getIntArray(R.array.senders_lengths)[mode]; 527 } 528 } 529 530 /** 531 * Returns coordinates for elements inside a conversation header view given 532 * the view width. 533 */ forConfig(final Context context, final Config config, final CoordinatesCache cache)534 public static ConversationItemViewCoordinates forConfig(final Context context, 535 final Config config, final CoordinatesCache cache) { 536 final int cacheKey = config.getCacheKey(); 537 ConversationItemViewCoordinates coordinates = cache.getCoordinates(cacheKey); 538 if (coordinates != null) { 539 return coordinates; 540 } 541 542 coordinates = new ConversationItemViewCoordinates(context, config, cache); 543 cache.put(cacheKey, coordinates); 544 return coordinates; 545 } 546 } 547