Home | History | Annotate | Download | only in quickcontact
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.quickcontact;
     17 
     18 import android.animation.ObjectAnimator;
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.content.res.Resources;
     22 import android.graphics.ColorFilter;
     23 import android.graphics.Rect;
     24 import android.graphics.drawable.Drawable;
     25 import android.support.v7.widget.CardView;
     26 import android.text.Spannable;
     27 import android.text.TextUtils;
     28 import android.transition.ChangeBounds;
     29 import android.transition.ChangeScroll;
     30 import android.transition.Fade;
     31 import android.transition.Transition;
     32 import android.transition.Transition.TransitionListener;
     33 import android.transition.TransitionManager;
     34 import android.transition.TransitionSet;
     35 import android.util.AttributeSet;
     36 import android.util.Log;
     37 import android.view.ContextMenu.ContextMenuInfo;
     38 import android.view.LayoutInflater;
     39 import android.view.MotionEvent;
     40 import android.view.View;
     41 import android.view.ViewConfiguration;
     42 import android.view.ViewGroup;
     43 import android.widget.ImageView;
     44 import android.widget.LinearLayout;
     45 import android.widget.RelativeLayout;
     46 import android.widget.TextView;
     47 
     48 import com.android.contacts.R;
     49 
     50 import java.util.ArrayList;
     51 import java.util.List;
     52 
     53 /**
     54  * Display entries in a LinearLayout that can be expanded to show all entries.
     55  */
     56 public class ExpandingEntryCardView extends CardView {
     57 
     58     private static final String TAG = "ExpandingEntryCardView";
     59     private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200;
     60     private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100;
     61 
     62     public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300;
     63     public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300;
     64 
     65     /**
     66      * Entry data.
     67      */
     68     public static final class Entry {
     69 
     70         private final int mId;
     71         private final Drawable mIcon;
     72         private final String mHeader;
     73         private final String mSubHeader;
     74         private final Drawable mSubHeaderIcon;
     75         private final String mText;
     76         private final Drawable mTextIcon;
     77         private Spannable mPrimaryContentDescription;
     78         private final Intent mIntent;
     79         private final Drawable mAlternateIcon;
     80         private final Intent mAlternateIntent;
     81         private final String mAlternateContentDescription;
     82         private final boolean mShouldApplyColor;
     83         private final boolean mIsEditable;
     84         private final EntryContextMenuInfo mEntryContextMenuInfo;
     85         private final Drawable mThirdIcon;
     86         private final Intent mThirdIntent;
     87         private final String mThirdContentDescription;
     88         private final int mIconResourceId;
     89 
     90         public Entry(int id, Drawable mainIcon, String header, String subHeader,
     91                 Drawable subHeaderIcon, String text, Drawable textIcon,
     92                 Spannable primaryContentDescription, Intent intent,
     93                 Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription,
     94                 boolean shouldApplyColor, boolean isEditable,
     95                 EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent,
     96                 String thirdContentDescription, int iconResourceId) {
     97             mId = id;
     98             mIcon = mainIcon;
     99             mHeader = header;
    100             mSubHeader = subHeader;
    101             mSubHeaderIcon = subHeaderIcon;
    102             mText = text;
    103             mTextIcon = textIcon;
    104             mPrimaryContentDescription = primaryContentDescription;
    105             mIntent = intent;
    106             mAlternateIcon = alternateIcon;
    107             mAlternateIntent = alternateIntent;
    108             mAlternateContentDescription = alternateContentDescription;
    109             mShouldApplyColor = shouldApplyColor;
    110             mIsEditable = isEditable;
    111             mEntryContextMenuInfo = entryContextMenuInfo;
    112             mThirdIcon = thirdIcon;
    113             mThirdIntent = thirdIntent;
    114             mThirdContentDescription = thirdContentDescription;
    115             mIconResourceId = iconResourceId;
    116         }
    117 
    118         Drawable getIcon() {
    119             return mIcon;
    120         }
    121 
    122         String getHeader() {
    123             return mHeader;
    124         }
    125 
    126         String getSubHeader() {
    127             return mSubHeader;
    128         }
    129 
    130         Drawable getSubHeaderIcon() {
    131             return mSubHeaderIcon;
    132         }
    133 
    134         public String getText() {
    135             return mText;
    136         }
    137 
    138         Drawable getTextIcon() {
    139             return mTextIcon;
    140         }
    141 
    142         Spannable getPrimaryContentDescription() {
    143             return mPrimaryContentDescription;
    144         }
    145 
    146         Intent getIntent() {
    147             return mIntent;
    148         }
    149 
    150         Drawable getAlternateIcon() {
    151             return mAlternateIcon;
    152         }
    153 
    154         Intent getAlternateIntent() {
    155             return mAlternateIntent;
    156         }
    157 
    158         String getAlternateContentDescription() {
    159             return mAlternateContentDescription;
    160         }
    161 
    162         boolean shouldApplyColor() {
    163             return mShouldApplyColor;
    164         }
    165 
    166         boolean isEditable() {
    167             return mIsEditable;
    168         }
    169 
    170         int getId() {
    171             return mId;
    172         }
    173 
    174         EntryContextMenuInfo getEntryContextMenuInfo() {
    175             return mEntryContextMenuInfo;
    176         }
    177 
    178         Drawable getThirdIcon() {
    179             return mThirdIcon;
    180         }
    181 
    182         Intent getThirdIntent() {
    183             return mThirdIntent;
    184         }
    185 
    186         String getThirdContentDescription() {
    187             return mThirdContentDescription;
    188         }
    189 
    190         int getIconResourceId() {
    191             return mIconResourceId;
    192         }
    193     }
    194 
    195     public interface ExpandingEntryCardViewListener {
    196         void onCollapse(int heightDelta);
    197         void onExpand(int heightDelta);
    198     }
    199 
    200     private View mExpandCollapseButton;
    201     private TextView mExpandCollapseTextView;
    202     private TextView mTitleTextView;
    203     private CharSequence mExpandButtonText;
    204     private CharSequence mCollapseButtonText;
    205     private OnClickListener mOnClickListener;
    206     private OnCreateContextMenuListener mOnCreateContextMenuListener;
    207     private boolean mIsExpanded = false;
    208     /**
    209      * The max number of entries to show in a collapsed card. If there are less entries passed in,
    210      * then they are all shown.
    211      */
    212     private int mCollapsedEntriesCount;
    213     private ExpandingEntryCardViewListener mListener;
    214     private List<List<Entry>> mEntries;
    215     private int mNumEntries = 0;
    216     private boolean mAllEntriesInflated = false;
    217     private List<List<View>> mEntryViews;
    218     private LinearLayout mEntriesViewGroup;
    219     private final ImageView mExpandCollapseArrow;
    220     private int mThemeColor;
    221     private ColorFilter mThemeColorFilter;
    222     private boolean mIsAlwaysExpanded;
    223     /** The ViewGroup to run the expand/collapse animation on */
    224     private ViewGroup mAnimationViewGroup;
    225     private LinearLayout mBadgeContainer;
    226     private final List<ImageView> mBadges;
    227     private final List<Integer> mBadgeIds;
    228     /**
    229      * List to hold the separators. This saves us from reconstructing every expand/collapse and
    230      * provides a smoother animation.
    231      */
    232     private List<View> mSeparators;
    233     private LinearLayout mContainer;
    234 
    235     private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
    236         @Override
    237         public void onClick(View v) {
    238             if (mIsExpanded) {
    239                 collapse();
    240             } else {
    241                 expand();
    242             }
    243         }
    244     };
    245 
    246     public ExpandingEntryCardView(Context context) {
    247         this(context, null);
    248     }
    249 
    250     public ExpandingEntryCardView(Context context, AttributeSet attrs) {
    251         super(context, attrs);
    252         LayoutInflater inflater = LayoutInflater.from(context);
    253         View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
    254         mEntriesViewGroup = (LinearLayout)
    255                 expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
    256         mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
    257         mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container);
    258 
    259         mExpandCollapseButton = inflater.inflate(
    260                 R.layout.quickcontact_expanding_entry_card_button, this, false);
    261         mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
    262         mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow);
    263         mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
    264         mBadgeContainer = (LinearLayout) mExpandCollapseButton.findViewById(R.id.badge_container);
    265 
    266         mBadges = new ArrayList<ImageView>();
    267         mBadgeIds = new ArrayList<Integer>();
    268     }
    269 
    270     /**
    271      * Sets the Entry list to display.
    272      *
    273      * @param entries The Entry list to display.
    274      */
    275     public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
    276             boolean isExpanded, boolean isAlwaysExpanded,
    277             ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) {
    278         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
    279         mIsExpanded = isExpanded;
    280         mIsAlwaysExpanded = isAlwaysExpanded;
    281         // If isAlwaysExpanded is true, mIsExpanded should be true
    282         mIsExpanded |= mIsAlwaysExpanded;
    283         mEntryViews = new ArrayList<List<View>>(entries.size());
    284         mEntries = entries;
    285         mNumEntries = 0;
    286         mAllEntriesInflated = false;
    287         for (List<Entry> entryList : mEntries) {
    288             mNumEntries += entryList.size();
    289             mEntryViews.add(new ArrayList<View>());
    290         }
    291         mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
    292         // We need a separator between each list, but not after the last one
    293         if (entries.size() > 1) {
    294             mSeparators = new ArrayList<>(entries.size() - 1);
    295         }
    296         mListener = listener;
    297         mAnimationViewGroup = animationViewGroup;
    298 
    299         if (mIsExpanded) {
    300             updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0);
    301             inflateAllEntries(layoutInflater);
    302         } else {
    303             updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0);
    304             inflateInitialEntries(layoutInflater);
    305         }
    306         insertEntriesIntoViewGroup();
    307         applyColor();
    308     }
    309 
    310     /**
    311      * Sets the text for the expand button.
    312      *
    313      * @param expandButtonText The expand button text.
    314      */
    315     public void setExpandButtonText(CharSequence expandButtonText) {
    316         mExpandButtonText = expandButtonText;
    317         if (mExpandCollapseTextView != null && !mIsExpanded) {
    318             mExpandCollapseTextView.setText(expandButtonText);
    319         }
    320     }
    321 
    322     /**
    323      * Sets the text for the expand button.
    324      *
    325      * @param expandButtonText The expand button text.
    326      */
    327     public void setCollapseButtonText(CharSequence expandButtonText) {
    328         mCollapseButtonText = expandButtonText;
    329         if (mExpandCollapseTextView != null && mIsExpanded) {
    330             mExpandCollapseTextView.setText(mCollapseButtonText);
    331         }
    332     }
    333 
    334     @Override
    335     public void setOnClickListener(OnClickListener listener) {
    336         mOnClickListener = listener;
    337     }
    338 
    339     @Override
    340     public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) {
    341         mOnCreateContextMenuListener = listener;
    342     }
    343 
    344     private void insertEntriesIntoViewGroup() {
    345         mEntriesViewGroup.removeAllViews();
    346 
    347         if (mIsExpanded) {
    348             for (int i = 0; i < mEntryViews.size(); i++) {
    349                 List<View> viewList = mEntryViews.get(i);
    350                 if (i > 0) {
    351                     View separator;
    352                     if (mSeparators.size() <= i - 1) {
    353                         separator = generateSeparator(viewList.get(0));
    354                         mSeparators.add(separator);
    355                     } else {
    356                         separator = mSeparators.get(i - 1);
    357                     }
    358                     mEntriesViewGroup.addView(separator);
    359                 }
    360                 for (View view : viewList) {
    361                     addEntry(view);
    362                 }
    363             }
    364         } else {
    365             // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the
    366             // number of entries that need to be added that are not the head element of a list
    367             // to reach mCollapsedEntriesCount.
    368             int numInViewGroup = 0;
    369             int extraEntries = mCollapsedEntriesCount - mEntryViews.size();
    370             for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount;
    371                     i++) {
    372                 List<View> entryViewList = mEntryViews.get(i);
    373                 if (i > 0) {
    374                     View separator;
    375                     if (mSeparators.size() <= i - 1) {
    376                         separator = generateSeparator(entryViewList.get(0));
    377                         mSeparators.add(separator);
    378                     } else {
    379                         separator = mSeparators.get(i - 1);
    380                     }
    381                     mEntriesViewGroup.addView(separator);
    382                 }
    383                 addEntry(entryViewList.get(0));
    384                 numInViewGroup++;
    385                 // Insert entries in this list to hit mCollapsedEntriesCount.
    386                 for (int j = 1;
    387                         j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount &&
    388                         extraEntries > 0;
    389                         j++) {
    390                     addEntry(entryViewList.get(j));
    391                     numInViewGroup++;
    392                     extraEntries--;
    393                 }
    394             }
    395         }
    396 
    397         removeView(mExpandCollapseButton);
    398         if (mCollapsedEntriesCount < mNumEntries
    399                 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) {
    400             mContainer.addView(mExpandCollapseButton, -1);
    401         }
    402     }
    403 
    404     private void addEntry(View entry) {
    405         // If no title and the first entry in the group, add extra padding
    406         if (TextUtils.isEmpty(mTitleTextView.getText()) &&
    407                 mEntriesViewGroup.getChildCount() == 0) {
    408             entry.setPadding(entry.getPaddingLeft(),
    409                     getResources().getDimensionPixelSize(
    410                             R.dimen.expanding_entry_card_item_padding_top) +
    411                     getResources().getDimensionPixelSize(
    412                             R.dimen.expanding_entry_card_null_title_top_extra_padding),
    413                     entry.getPaddingRight(),
    414                     entry.getPaddingBottom());
    415         }
    416         mEntriesViewGroup.addView(entry);
    417     }
    418 
    419     private View generateSeparator(View entry) {
    420         View separator = new View(getContext());
    421         Resources res = getResources();
    422 
    423         separator.setBackgroundColor(res.getColor(
    424                 R.color.divider_line_color_light));
    425         LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
    426                 ViewGroup.LayoutParams.MATCH_PARENT,
    427                 res.getDimensionPixelSize(R.dimen.divider_line_height));
    428         // The separator is aligned with the text in the entry. This is offset by a default
    429         // margin. If there is an icon present, the icon's width and margin are added
    430         int marginStart = res.getDimensionPixelSize(
    431                 R.dimen.expanding_entry_card_item_padding_start);
    432         ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon);
    433         if (entryIcon.getVisibility() == View.VISIBLE) {
    434             int imageWidthAndMargin =
    435                     res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) +
    436                     res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing);
    437             marginStart += imageWidthAndMargin;
    438         }
    439         layoutParams.setMarginStart(marginStart);
    440         separator.setLayoutParams(layoutParams);
    441         return separator;
    442     }
    443 
    444     private CharSequence getExpandButtonText() {
    445         if (!TextUtils.isEmpty(mExpandButtonText)) {
    446             return mExpandButtonText;
    447         } else {
    448             // Default to "See more".
    449             return getResources().getText(R.string.expanding_entry_card_view_see_more);
    450         }
    451     }
    452 
    453     private CharSequence getCollapseButtonText() {
    454         if (!TextUtils.isEmpty(mCollapseButtonText)) {
    455             return mCollapseButtonText;
    456         } else {
    457             // Default to "See less".
    458             return getResources().getText(R.string.expanding_entry_card_view_see_less);
    459         }
    460     }
    461 
    462     /**
    463      * Inflates the initial entries to be shown.
    464      */
    465     private void inflateInitialEntries(LayoutInflater layoutInflater) {
    466         // If the number of collapsed entries equals total entries, inflate all
    467         if (mCollapsedEntriesCount == mNumEntries) {
    468             inflateAllEntries(layoutInflater);
    469         } else {
    470             // Otherwise inflate the top entry from each list
    471             // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached.
    472             int numInflated = 0;
    473             int extraEntries = mCollapsedEntriesCount - mEntries.size();
    474             for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) {
    475                 List<Entry> entryList = mEntries.get(i);
    476                 List<View> entryViewList = mEntryViews.get(i);
    477 
    478                 entryViewList.add(createEntryView(layoutInflater, entryList.get(0),
    479                         /* showIcon = */ View.VISIBLE));
    480                 numInflated++;
    481                 // Inflate entries in this list to hit mCollapsedEntriesCount.
    482                 for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount &&
    483                         extraEntries > 0; j++) {
    484                     entryViewList.add(createEntryView(layoutInflater, entryList.get(j),
    485                             /* showIcon = */ View.INVISIBLE));
    486                     numInflated++;
    487                     extraEntries--;
    488                 }
    489             }
    490         }
    491     }
    492 
    493     /**
    494      * Inflates all entries.
    495      */
    496     private void inflateAllEntries(LayoutInflater layoutInflater) {
    497         if (mAllEntriesInflated) {
    498             return;
    499         }
    500         for (int i = 0; i < mEntries.size(); i++) {
    501             List<Entry> entryList = mEntries.get(i);
    502             List<View> viewList = mEntryViews.get(i);
    503             for (int j = viewList.size(); j < entryList.size(); j++) {
    504                 final int iconVisibility;
    505                 final Entry entry = entryList.get(j);
    506                 // If the entry does not have an icon, mark gone. Else if it has an icon, show
    507                 // for the first Entry in the list only
    508                 if (entry.getIcon() == null) {
    509                     iconVisibility = View.GONE;
    510                 } else if (j == 0) {
    511                     iconVisibility = View.VISIBLE;
    512                 } else {
    513                     iconVisibility = View.INVISIBLE;
    514                 }
    515                 viewList.add(createEntryView(layoutInflater, entry, iconVisibility));
    516             }
    517         }
    518         mAllEntriesInflated = true;
    519     }
    520 
    521     public void setColorAndFilter(int color, ColorFilter colorFilter) {
    522         mThemeColor = color;
    523         mThemeColorFilter = colorFilter;
    524         applyColor();
    525     }
    526 
    527     public void setEntryHeaderColor(int color) {
    528         if (mEntries != null) {
    529             for (List<View> entryList : mEntryViews) {
    530                 for (View entryView : entryList) {
    531                     TextView header = (TextView) entryView.findViewById(R.id.header);
    532                     if (header != null) {
    533                         header.setTextColor(color);
    534                     }
    535                 }
    536             }
    537         }
    538     }
    539 
    540     /**
    541      * The ColorFilter is passed in along with the color so that a new one only needs to be created
    542      * once for the entire activity.
    543      * 1. Title
    544      * 2. Entry icons
    545      * 3. Expand/Collapse Text
    546      * 4. Expand/Collapse Button
    547      */
    548     public void applyColor() {
    549         if (mThemeColor != 0 && mThemeColorFilter != null) {
    550             // Title
    551             if (mTitleTextView != null) {
    552                 mTitleTextView.setTextColor(mThemeColor);
    553             }
    554 
    555             // Entry icons
    556             if (mEntries != null) {
    557                 for (List<Entry> entryList : mEntries) {
    558                     for (Entry entry : entryList) {
    559                         if (entry.shouldApplyColor()) {
    560                             Drawable icon = entry.getIcon();
    561                             if (icon != null) {
    562                                 icon.mutate();
    563                                 icon.setColorFilter(mThemeColorFilter);
    564                             }
    565                         }
    566                         Drawable alternateIcon = entry.getAlternateIcon();
    567                         if (alternateIcon != null) {
    568                             alternateIcon.mutate();
    569                             alternateIcon.setColorFilter(mThemeColorFilter);
    570                         }
    571                         Drawable thirdIcon = entry.getThirdIcon();
    572                         if (thirdIcon != null) {
    573                             thirdIcon.mutate();
    574                             thirdIcon.setColorFilter(mThemeColorFilter);
    575                         }
    576                     }
    577                 }
    578             }
    579 
    580             // Expand/Collapse
    581             mExpandCollapseTextView.setTextColor(mThemeColor);
    582             mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
    583         }
    584     }
    585 
    586     private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
    587             int iconVisibility) {
    588         final EntryView view = (EntryView) layoutInflater.inflate(
    589                 R.layout.expanding_entry_card_item, this, false);
    590 
    591         view.setContextMenuInfo(entry.getEntryContextMenuInfo());
    592         if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) {
    593             view.setContentDescription(entry.getPrimaryContentDescription());
    594         }
    595 
    596         final ImageView icon = (ImageView) view.findViewById(R.id.icon);
    597         icon.setVisibility(iconVisibility);
    598         if (entry.getIcon() != null) {
    599             icon.setImageDrawable(entry.getIcon());
    600         }
    601         final TextView header = (TextView) view.findViewById(R.id.header);
    602         if (!TextUtils.isEmpty(entry.getHeader())) {
    603             header.setText(entry.getHeader());
    604         } else {
    605             header.setVisibility(View.GONE);
    606         }
    607 
    608         final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
    609         if (!TextUtils.isEmpty(entry.getSubHeader())) {
    610             subHeader.setText(entry.getSubHeader());
    611         } else {
    612             subHeader.setVisibility(View.GONE);
    613         }
    614 
    615         final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
    616         if (entry.getSubHeaderIcon() != null) {
    617             subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
    618         } else {
    619             subHeaderIcon.setVisibility(View.GONE);
    620         }
    621 
    622         final TextView text = (TextView) view.findViewById(R.id.text);
    623         if (!TextUtils.isEmpty(entry.getText())) {
    624             text.setText(entry.getText());
    625         } else {
    626             text.setVisibility(View.GONE);
    627         }
    628 
    629         final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
    630         if (entry.getTextIcon() != null) {
    631             textIcon.setImageDrawable(entry.getTextIcon());
    632         } else {
    633             textIcon.setVisibility(View.GONE);
    634         }
    635 
    636         if (entry.getIntent() != null) {
    637             view.setOnClickListener(mOnClickListener);
    638             view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
    639         }
    640 
    641         if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
    642             // Remove the click effect
    643             view.setBackground(null);
    644         }
    645 
    646         // If only the header is visible, add a top margin to match icon's top margin.
    647         // Also increase the space below the header for visual comfort.
    648         if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
    649                 text.getVisibility() == View.GONE) {
    650             RelativeLayout.LayoutParams headerLayoutParams =
    651                     (RelativeLayout.LayoutParams) header.getLayoutParams();
    652             headerLayoutParams.topMargin = (int) (getResources().getDimension(
    653                     R.dimen.expanding_entry_card_item_header_only_margin_top));
    654             headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
    655                     R.dimen.expanding_entry_card_item_header_only_margin_bottom));
    656             header.setLayoutParams(headerLayoutParams);
    657         }
    658 
    659         // Adjust the top padding size for entries with an invisible icon. The padding depends on
    660         // if there is a sub header or text section
    661         if (iconVisibility == View.INVISIBLE &&
    662                 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
    663             view.setPaddingRelative(view.getPaddingStart(),
    664                     getResources().getDimensionPixelSize(
    665                             R.dimen.expanding_entry_card_item_no_icon_margin_top),
    666                     view.getPaddingEnd(),
    667                     view.getPaddingBottom());
    668         } else if (iconVisibility == View.INVISIBLE &&  TextUtils.isEmpty(entry.getSubHeader())
    669                 && TextUtils.isEmpty(entry.getText())) {
    670             view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
    671                     view.getPaddingBottom());
    672         }
    673 
    674         final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
    675         final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
    676 
    677         if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
    678             alternateIcon.setImageDrawable(entry.getAlternateIcon());
    679             alternateIcon.setOnClickListener(mOnClickListener);
    680             alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
    681             alternateIcon.setVisibility(View.VISIBLE);
    682             alternateIcon.setContentDescription(entry.getAlternateContentDescription());
    683         }
    684 
    685         if (entry.getThirdIcon() != null && entry.getThirdIntent() != null) {
    686             thirdIcon.setImageDrawable(entry.getThirdIcon());
    687             thirdIcon.setOnClickListener(mOnClickListener);
    688             thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
    689             thirdIcon.setVisibility(View.VISIBLE);
    690             thirdIcon.setContentDescription(entry.getThirdContentDescription());
    691         }
    692 
    693         // Set a custom touch listener for expanding the extra icon touch areas
    694         view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
    695         view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
    696 
    697         return view;
    698     }
    699 
    700     private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
    701         if (mIsExpanded) {
    702             final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
    703                     "rotation", 180);
    704             animator.setDuration(duration);
    705             animator.start();
    706         } else {
    707             final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
    708                     "rotation", 0);
    709             animator.setDuration(duration);
    710             animator.start();
    711         }
    712         updateBadges();
    713 
    714         mExpandCollapseTextView.setText(buttonText);
    715     }
    716 
    717     private void updateBadges() {
    718         if (mIsExpanded) {
    719             mBadgeContainer.removeAllViews();
    720         } else {
    721             // Inflate badges if not yet created
    722             if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) {
    723                 for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) {
    724                     Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
    725                     int badgeResourceId = mEntries.get(i).get(0).getIconResourceId();
    726                     // Do not add the same badge twice
    727                     if (badgeResourceId != 0 && mBadgeIds.contains(badgeResourceId)) {
    728                         continue;
    729                     }
    730                     if (badgeDrawable != null) {
    731                         ImageView badgeView = new ImageView(getContext());
    732                         LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams(
    733                                 (int) getResources().getDimension(
    734                                         R.dimen.expanding_entry_card_item_icon_width),
    735                                 (int) getResources().getDimension(
    736                                         R.dimen.expanding_entry_card_item_icon_height));
    737                         badgeViewParams.setMarginEnd((int) getResources().getDimension(
    738                                 R.dimen.expanding_entry_card_badge_separator_margin));
    739                         badgeView.setLayoutParams(badgeViewParams);
    740                         badgeView.setImageDrawable(badgeDrawable);
    741                         mBadges.add(badgeView);
    742                         mBadgeIds.add(badgeResourceId);
    743                     }
    744                 }
    745             }
    746             mBadgeContainer.removeAllViews();
    747             for (ImageView badge : mBadges) {
    748                 mBadgeContainer.addView(badge);
    749             }
    750         }
    751     }
    752 
    753     private void expand() {
    754         ChangeBounds boundsTransition = new ChangeBounds();
    755         boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
    756 
    757         Fade fadeIn = new Fade(Fade.IN);
    758         fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
    759         fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
    760 
    761         TransitionSet transitionSet = new TransitionSet();
    762         transitionSet.addTransition(boundsTransition);
    763         transitionSet.addTransition(fadeIn);
    764 
    765         transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
    766 
    767         final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
    768                 this : mAnimationViewGroup;
    769 
    770         transitionSet.addListener(new TransitionListener() {
    771             @Override
    772             public void onTransitionStart(Transition transition) {
    773                 // The listener is used to turn off suppressing, the proper delta is not necessary
    774                 mListener.onExpand(0);
    775             }
    776 
    777             @Override
    778             public void onTransitionEnd(Transition transition) {
    779             }
    780 
    781             @Override
    782             public void onTransitionCancel(Transition transition) {
    783             }
    784 
    785             @Override
    786             public void onTransitionPause(Transition transition) {
    787             }
    788 
    789             @Override
    790             public void onTransitionResume(Transition transition) {
    791             }
    792         });
    793 
    794         TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
    795 
    796         mIsExpanded = true;
    797         // In order to insert new entries, we may need to inflate them for the first time
    798         inflateAllEntries(LayoutInflater.from(getContext()));
    799         insertEntriesIntoViewGroup();
    800         updateExpandCollapseButton(getCollapseButtonText(),
    801                 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
    802     }
    803 
    804     private void collapse() {
    805         final int startingHeight = mEntriesViewGroup.getMeasuredHeight();
    806         mIsExpanded = false;
    807         updateExpandCollapseButton(getExpandButtonText(),
    808                 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
    809 
    810         final ChangeBounds boundsTransition = new ChangeBounds();
    811         boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
    812 
    813         final ChangeScroll scrollTransition = new ChangeScroll();
    814         scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
    815 
    816         TransitionSet transitionSet = new TransitionSet();
    817         transitionSet.addTransition(boundsTransition);
    818         transitionSet.addTransition(scrollTransition);
    819 
    820         transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
    821 
    822         final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
    823                 this : mAnimationViewGroup;
    824 
    825         boundsTransition.addListener(new TransitionListener() {
    826             @Override
    827             public void onTransitionStart(Transition transition) {
    828                 /*
    829                  * onTransitionStart is called after the view hierarchy has been changed but before
    830                  * the animation begins.
    831                  */
    832                 int finishingHeight = mEntriesViewGroup.getMeasuredHeight();
    833                 mListener.onCollapse(startingHeight - finishingHeight);
    834             }
    835 
    836             @Override
    837             public void onTransitionEnd(Transition transition) {
    838             }
    839 
    840             @Override
    841             public void onTransitionCancel(Transition transition) {
    842             }
    843 
    844             @Override
    845             public void onTransitionPause(Transition transition) {
    846             }
    847 
    848             @Override
    849             public void onTransitionResume(Transition transition) {
    850             }
    851         });
    852 
    853         TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
    854 
    855         insertEntriesIntoViewGroup();
    856     }
    857 
    858     /**
    859      * Returns whether the view is currently in its expanded state.
    860      */
    861     public boolean isExpanded() {
    862         return mIsExpanded;
    863     }
    864 
    865     /**
    866      * Sets the title text of this ExpandingEntryCardView.
    867      * @param title The title to set. A null title will result in the title being removed.
    868      */
    869     public void setTitle(String title) {
    870         if (mTitleTextView == null) {
    871             Log.e(TAG, "mTitleTextView is null");
    872         }
    873         mTitleTextView.setText(title);
    874         mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
    875         findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
    876                 View.GONE : View.VISIBLE);
    877         // If the title is set after children have been added, reset the top entry's padding to
    878         // the default. Else if the title is cleared after children have been added, set
    879         // the extra top padding
    880         if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
    881             View firstEntry = mEntriesViewGroup.getChildAt(0);
    882             firstEntry.setPadding(firstEntry.getPaddingLeft(),
    883                     getResources().getDimensionPixelSize(
    884                             R.dimen.expanding_entry_card_item_padding_top),
    885                     firstEntry.getPaddingRight(),
    886                     firstEntry.getPaddingBottom());
    887         } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
    888             View firstEntry = mEntriesViewGroup.getChildAt(0);
    889             firstEntry.setPadding(firstEntry.getPaddingLeft(),
    890                     getResources().getDimensionPixelSize(
    891                             R.dimen.expanding_entry_card_item_padding_top) +
    892                             getResources().getDimensionPixelSize(
    893                                     R.dimen.expanding_entry_card_null_title_top_extra_padding),
    894                     firstEntry.getPaddingRight(),
    895                     firstEntry.getPaddingBottom());
    896         }
    897     }
    898 
    899     public boolean shouldShow() {
    900         return mEntries != null && mEntries.size() > 0;
    901     }
    902 
    903     public static final class EntryView extends RelativeLayout {
    904         private EntryContextMenuInfo mEntryContextMenuInfo;
    905 
    906         public EntryView(Context context) {
    907             super(context);
    908         }
    909 
    910         public EntryView(Context context, AttributeSet attrs) {
    911             super(context, attrs);
    912         }
    913 
    914         public void setContextMenuInfo(EntryContextMenuInfo info) {
    915             mEntryContextMenuInfo = info;
    916         }
    917 
    918         @Override
    919         protected ContextMenuInfo getContextMenuInfo() {
    920             return mEntryContextMenuInfo;
    921         }
    922     }
    923 
    924     public static final class EntryContextMenuInfo implements ContextMenuInfo {
    925         private final String mCopyText;
    926         private final String mCopyLabel;
    927         private final String mMimeType;
    928         private final long mId;
    929         private final boolean mIsSuperPrimary;
    930 
    931         public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id,
    932                 boolean isSuperPrimary) {
    933             mCopyText = copyText;
    934             mCopyLabel = copyLabel;
    935             mMimeType = mimeType;
    936             mId = id;
    937             mIsSuperPrimary = isSuperPrimary;
    938         }
    939 
    940         public String getCopyText() {
    941             return mCopyText;
    942         }
    943 
    944         public String getCopyLabel() {
    945             return mCopyLabel;
    946         }
    947 
    948         public String getMimeType() {
    949             return mMimeType;
    950         }
    951 
    952         public long getId() {
    953             return mId;
    954         }
    955 
    956         public boolean isSuperPrimary() {
    957             return mIsSuperPrimary;
    958         }
    959     }
    960 
    961     static final class EntryTag {
    962         private final int mId;
    963         private final Intent mIntent;
    964 
    965         public EntryTag(int id, Intent intent) {
    966             mId = id;
    967             mIntent = intent;
    968         }
    969 
    970         public int getId() {
    971             return mId;
    972         }
    973 
    974         public Intent getIntent() {
    975             return mIntent;
    976         }
    977     }
    978 
    979     /**
    980      * This custom touch listener increases the touch area for the second and third icons, if
    981      * they are present. This is necessary to maintain other properties on an entry view, like
    982      * using a top padding on entry. Based off of {@link android.view.TouchDelegate}
    983      */
    984     private static final class EntryTouchListener implements View.OnTouchListener {
    985         private final View mEntry;
    986         private final ImageView mAlternateIcon;
    987         private final ImageView mThirdIcon;
    988         /** mTouchedView locks in a view on touch down */
    989         private View mTouchedView;
    990         /** mSlop adds some space to account for touches that are just outside the hit area */
    991         private int mSlop;
    992 
    993         public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) {
    994             mEntry = entry;
    995             mAlternateIcon = alternateIcon;
    996             mThirdIcon = thirdIcon;
    997             mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop();
    998         }
    999 
   1000         @Override
   1001         public boolean onTouch(View v, MotionEvent event) {
   1002             View touchedView = mTouchedView;
   1003             boolean sendToTouched = false;
   1004             boolean hit = true;
   1005             boolean handled = false;
   1006 
   1007             switch (event.getAction()) {
   1008                 case MotionEvent.ACTION_DOWN:
   1009                     if (hitThirdIcon(event)) {
   1010                         mTouchedView = mThirdIcon;
   1011                         sendToTouched = true;
   1012                     } else if (hitAlternateIcon(event)) {
   1013                         mTouchedView = mAlternateIcon;
   1014                         sendToTouched = true;
   1015                     } else {
   1016                         mTouchedView = mEntry;
   1017                         sendToTouched = false;
   1018                     }
   1019                     touchedView = mTouchedView;
   1020                     break;
   1021                 case MotionEvent.ACTION_UP:
   1022                 case MotionEvent.ACTION_MOVE:
   1023                     sendToTouched = mTouchedView != null && mTouchedView != mEntry;
   1024                     if (sendToTouched) {
   1025                         final Rect slopBounds = new Rect();
   1026                         touchedView.getHitRect(slopBounds);
   1027                         slopBounds.inset(-mSlop, -mSlop);
   1028                         if (!slopBounds.contains((int) event.getX(), (int) event.getY())) {
   1029                             hit = false;
   1030                         }
   1031                     }
   1032                     break;
   1033                 case MotionEvent.ACTION_CANCEL:
   1034                     sendToTouched = mTouchedView != null && mTouchedView != mEntry;
   1035                     mTouchedView = null;
   1036                     break;
   1037             }
   1038             if (sendToTouched) {
   1039                 if (hit) {
   1040                     event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2);
   1041                 } else {
   1042                     // Offset event coordinates to be outside the target view (in case it does
   1043                     // something like tracking pressed state)
   1044                     event.setLocation(-(mSlop * 2), -(mSlop * 2));
   1045                 }
   1046                 handled = touchedView.dispatchTouchEvent(event);
   1047             }
   1048             return handled;
   1049         }
   1050 
   1051         private boolean hitThirdIcon(MotionEvent event) {
   1052             if (mEntry.isLayoutRtl()) {
   1053                 return mThirdIcon.getVisibility() == View.VISIBLE &&
   1054                         event.getX() < mThirdIcon.getRight();
   1055             } else {
   1056                 return mThirdIcon.getVisibility() == View.VISIBLE &&
   1057                         event.getX() > mThirdIcon.getLeft();
   1058             }
   1059         }
   1060 
   1061         /**
   1062          * Should be used after checking if third icon was hit
   1063          */
   1064         private boolean hitAlternateIcon(MotionEvent event) {
   1065             // LayoutParams used to add the start margin to the touch area
   1066             final RelativeLayout.LayoutParams alternateIconParams =
   1067                     (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams();
   1068             if (mEntry.isLayoutRtl()) {
   1069                 return mAlternateIcon.getVisibility() == View.VISIBLE &&
   1070                         event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin;
   1071             } else {
   1072                 return mAlternateIcon.getVisibility() == View.VISIBLE &&
   1073                         event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin;
   1074             }
   1075         }
   1076     }
   1077 }
   1078