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