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
18package com.android.mail.browse;
19
20import android.annotation.SuppressLint;
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Paint.FontMetricsInt;
24import android.graphics.Typeface;
25import android.support.v4.view.ViewCompat;
26import android.util.SparseArray;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.View.MeasureSpec;
30import android.view.ViewGroup;
31import android.widget.TextView;
32
33import com.android.mail.R;
34import com.android.mail.utils.Utils;
35import com.android.mail.utils.ViewUtils;
36import 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 */
47public 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