Home | History | Annotate | Download | only in browse
      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 
     77         public Config setViewMode(int viewMode) {
     78             mViewMode = viewMode;
     79             return this;
     80         }
     81 
     82         public Config withGadget(int gadget) {
     83             mGadgetMode = gadget;
     84             return this;
     85         }
     86 
     87         public Config showFolders() {
     88             mShowFolders = true;
     89             return this;
     90         }
     91 
     92         public Config showReplyState() {
     93             mShowReplyState = true;
     94             return this;
     95         }
     96 
     97         public Config showColorBlock() {
     98             mShowColorBlock = true;
     99             return this;
    100         }
    101 
    102         public Config showPersonalIndicator() {
    103             mShowPersonalIndicator  = true;
    104             return this;
    105         }
    106 
    107         public Config updateWidth(int width) {
    108             mWidth = width;
    109             return this;
    110         }
    111 
    112         public int getWidth() {
    113             return mWidth;
    114         }
    115 
    116         public int getViewMode() {
    117             return mViewMode;
    118         }
    119 
    120         public int getGadgetMode() {
    121             return mGadgetMode;
    122         }
    123 
    124         public boolean areFoldersVisible() {
    125             return mShowFolders;
    126         }
    127 
    128         public boolean isReplyStateVisible() {
    129             return mShowReplyState;
    130         }
    131 
    132         public boolean isColorBlockVisible() {
    133             return mShowColorBlock;
    134         }
    135 
    136         public boolean isPersonalIndicatorVisible() {
    137             return mShowPersonalIndicator;
    138         }
    139 
    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 
    146         public Config setLayoutDirection(int layoutDirection) {
    147             mLayoutDirection = layoutDirection;
    148             return this;
    149         }
    150 
    151         public int getLayoutDirection() {
    152             return mLayoutDirection;
    153         }
    154 
    155         public Config setUseFullMargins(boolean useFullMargins) {
    156             mUseFullMargins = useFullMargins;
    157             return this;
    158         }
    159 
    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 
    170         public ConversationItemViewCoordinates getCoordinates(final int key) {
    171             return mCoordinatesCache.get(key);
    172         }
    173 
    174         public View getView(final int layoutId) {
    175             return mViewsCache.get(layoutId);
    176         }
    177 
    178         public void put(final int key, final ConversationItemViewCoordinates coords) {
    179             mCoordinatesCache.put(key, coords);
    180         }
    181 
    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 
    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")
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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