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