• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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