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