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