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