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 
withGadget(int gadget)70         public Config withGadget(int gadget) {
71             mGadgetMode = gadget;
72             return this;
73         }
74 
showFolders()75         public Config showFolders() {
76             mShowFolders = true;
77             return this;
78         }
79 
showReplyState()80         public Config showReplyState() {
81             mShowReplyState = true;
82             return this;
83         }
84 
showColorBlock()85         public Config showColorBlock() {
86             mShowColorBlock = true;
87             return this;
88         }
89 
showPersonalIndicator()90         public Config showPersonalIndicator() {
91             mShowPersonalIndicator  = true;
92             return this;
93         }
94 
updateWidth(int width)95         public Config updateWidth(int width) {
96             mWidth = width;
97             return this;
98         }
99 
getWidth()100         public int getWidth() {
101             return mWidth;
102         }
103 
getGadgetMode()104         public int getGadgetMode() {
105             return mGadgetMode;
106         }
107 
areFoldersVisible()108         public boolean areFoldersVisible() {
109             return mShowFolders;
110         }
111 
isReplyStateVisible()112         public boolean isReplyStateVisible() {
113             return mShowReplyState;
114         }
115 
isColorBlockVisible()116         public boolean isColorBlockVisible() {
117             return mShowColorBlock;
118         }
119 
isPersonalIndicatorVisible()120         public boolean isPersonalIndicatorVisible() {
121             return mShowPersonalIndicator;
122         }
123 
getCacheKey()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 
setLayoutDirection(int layoutDirection)130         public Config setLayoutDirection(int layoutDirection) {
131             mLayoutDirection = layoutDirection;
132             return this;
133         }
134 
getLayoutDirection()135         public int getLayoutDirection() {
136             return mLayoutDirection;
137         }
138 
setUseFullMargins(boolean useFullMargins)139         public Config setUseFullMargins(boolean useFullMargins) {
140             mUseFullMargins = useFullMargins;
141             return this;
142         }
143 
useFullPadding()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 
getCoordinates(final int key)154         public ConversationItemViewCoordinates getCoordinates(final int key) {
155             return mCoordinatesCache.get(key);
156         }
157 
getView(final int layoutId)158         public View getView(final int layoutId) {
159             return mViewsCache.get(layoutId);
160         }
161 
put(final int key, final ConversationItemViewCoordinates coords)162         public void put(final int key, final ConversationItemViewCoordinates coords) {
163             mCoordinatesCache.put(key, coords);
164         }
165 
put(final int layoutId, final View view)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 
ConversationItemViewCoordinates(final Context context, final Config config, final CoordinatesCache cache)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")
setFramePadding(Context context, ViewGroup view, boolean useFullPadding)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      */
getLatinTopAdjustment(TextView t)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      */
getX(View view)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      */
getY(View view)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      */
getSendersLength(Context context, boolean hasAttachments)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      */
forConfig(final Context context, final Config config, final CoordinatesCache cache)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