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