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.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