ExpandingEntryCardView.java revision 43e0dead719568ed610f9f111f6e7614368445ef
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 */
16package com.android.contacts.quickcontact;
17
18import android.animation.ObjectAnimator;
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.graphics.ColorFilter;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.support.v7.widget.CardView;
26import android.text.TextUtils;
27import android.transition.ChangeBounds;
28import android.transition.ChangeScroll;
29import android.transition.Fade;
30import android.transition.Transition;
31import android.transition.Transition.TransitionListener;
32import android.transition.TransitionManager;
33import android.transition.TransitionSet;
34import android.util.AttributeSet;
35import android.util.Log;
36import android.view.ContextMenu.ContextMenuInfo;
37import android.view.LayoutInflater;
38import android.view.TouchDelegate;
39import android.view.View;
40import android.view.View.OnCreateContextMenuListener;
41import android.view.ViewGroup;
42import android.widget.FrameLayout;
43import android.widget.ImageView;
44import android.widget.LinearLayout;
45import android.widget.RelativeLayout;
46import android.widget.TextView;
47
48import com.android.contacts.R;
49
50import java.util.ArrayList;
51import java.util.List;
52
53/**
54 * Display entries in a LinearLayout that can be expanded to show all entries.
55 */
56public 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 final Intent mIntent;
78        private final Drawable mAlternateIcon;
79        private final Intent mAlternateIntent;
80        private final String mAlternateContentDescription;
81        private final boolean mShouldApplyColor;
82        private final boolean mIsEditable;
83        private final EntryContextMenuInfo mEntryContextMenuInfo;
84
85        public Entry(int id, Drawable icon, String header, String subHeader, String text,
86                Intent intent, Drawable alternateIcon, Intent alternateIntent,
87                String alternateContentDescription, boolean shouldApplyColor,
88                boolean isEditable, EntryContextMenuInfo entryContextMenuInfo) {
89            this(id, icon, header, subHeader, null, text, null, intent, alternateIcon,
90                    alternateIntent, alternateContentDescription, shouldApplyColor, isEditable,
91                    entryContextMenuInfo);
92        }
93
94        public Entry(int id, Drawable mainIcon, String header, String subHeader,
95                Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent,
96                Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription,
97                boolean shouldApplyColor, boolean isEditable,
98                EntryContextMenuInfo entryContextMenuInfo) {
99            mId = id;
100            mIcon = mainIcon;
101            mHeader = header;
102            mSubHeader = subHeader;
103            mSubHeaderIcon = subHeaderIcon;
104            mText = text;
105            mTextIcon = textIcon;
106            mIntent = intent;
107            mAlternateIcon = alternateIcon;
108            mAlternateIntent = alternateIntent;
109            mAlternateContentDescription = alternateContentDescription;
110            mShouldApplyColor = shouldApplyColor;
111            mIsEditable = isEditable;
112            mEntryContextMenuInfo = entryContextMenuInfo;
113        }
114
115        Drawable getIcon() {
116            return mIcon;
117        }
118
119        String getHeader() {
120            return mHeader;
121        }
122
123        String getSubHeader() {
124            return mSubHeader;
125        }
126
127        Drawable getSubHeaderIcon() {
128            return mSubHeaderIcon;
129        }
130
131        public String getText() {
132            return mText;
133        }
134
135        Drawable getTextIcon() {
136            return mTextIcon;
137        }
138
139        Intent getIntent() {
140            return mIntent;
141        }
142
143        Drawable getAlternateIcon() {
144            return mAlternateIcon;
145        }
146
147        Intent getAlternateIntent() {
148            return mAlternateIntent;
149        }
150
151        String getAlternateContentDescription() {
152            return mAlternateContentDescription;
153        }
154
155        boolean shouldApplyColor() {
156            return mShouldApplyColor;
157        }
158
159        boolean isEditable() {
160            return mIsEditable;
161        }
162
163        int getId() {
164            return mId;
165        }
166
167        EntryContextMenuInfo getEntryContextMenuInfo() {
168            return mEntryContextMenuInfo;
169        }
170    }
171
172    public interface ExpandingEntryCardViewListener {
173        void onCollapse(int heightDelta);
174        void onExpand(int heightDelta);
175    }
176
177    private View mExpandCollapseButton;
178    private TextView mExpandCollapseTextView;
179    private TextView mTitleTextView;
180    private CharSequence mExpandButtonText;
181    private CharSequence mCollapseButtonText;
182    private OnClickListener mOnClickListener;
183    private OnCreateContextMenuListener mOnCreateContextMenuListener;
184    private boolean mIsExpanded = false;
185    /**
186     * The max number of entries to show in a collapsed card. If there are less entries passed in,
187     * then they are all shown.
188     */
189    private int mCollapsedEntriesCount;
190    private ExpandingEntryCardViewListener mListener;
191    private List<List<Entry>> mEntries;
192    private int mNumEntries = 0;
193    private boolean mAllEntriesInflated = false;
194    private List<List<View>> mEntryViews;
195    private LinearLayout mEntriesViewGroup;
196    private final ImageView mExpandCollapseArrow;
197    private int mThemeColor;
198    private ColorFilter mThemeColorFilter;
199    private boolean mIsAlwaysExpanded;
200    /** The ViewGroup to run the expand/collapse animation on */
201    private ViewGroup mAnimationViewGroup;
202    private LinearLayout mBadgeContainer;
203    private final List<ImageView> mBadges;
204    private LinearLayout mContainer;
205
206    private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
207        @Override
208        public void onClick(View v) {
209            if (mIsExpanded) {
210                collapse();
211            } else {
212                expand();
213            }
214        }
215    };
216
217    public ExpandingEntryCardView(Context context) {
218        this(context, null);
219    }
220
221    public ExpandingEntryCardView(Context context, AttributeSet attrs) {
222        super(context, attrs);
223        LayoutInflater inflater = LayoutInflater.from(context);
224        View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
225        mEntriesViewGroup = (LinearLayout)
226                expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
227        mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
228        mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container);
229
230        mExpandCollapseButton = inflater.inflate(
231                R.layout.quickcontact_expanding_entry_card_button, this, false);
232        mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
233        mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow);
234        mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
235        mBadgeContainer = (LinearLayout) mExpandCollapseButton.findViewById(R.id.badge_container);
236
237        mBadges = new ArrayList<ImageView>();
238    }
239
240    /**
241     * Sets the Entry list to display.
242     *
243     * @param entries The Entry list to display.
244     */
245    public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
246            boolean isExpanded, boolean isAlwaysExpanded,
247            ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) {
248        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
249        mIsExpanded = isExpanded;
250        mIsAlwaysExpanded = isAlwaysExpanded;
251        // If isAlwaysExpanded is true, mIsExpanded should be true
252        mIsExpanded |= mIsAlwaysExpanded;
253        mEntryViews = new ArrayList<List<View>>(entries.size());
254        mEntries = entries;
255        mNumEntries = 0;
256        mAllEntriesInflated = false;
257        for (List<Entry> entryList : mEntries) {
258            mNumEntries += entryList.size();
259            mEntryViews.add(new ArrayList<View>());
260        }
261        mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
262        mListener = listener;
263        mAnimationViewGroup = animationViewGroup;
264
265        if (mIsExpanded) {
266            updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0);
267            inflateAllEntries(layoutInflater);
268        } else {
269            updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0);
270            inflateInitialEntries(layoutInflater);
271        }
272        insertEntriesIntoViewGroup();
273        applyColor();
274    }
275
276    /**
277     * Sets the text for the expand button.
278     *
279     * @param expandButtonText The expand button text.
280     */
281    public void setExpandButtonText(CharSequence expandButtonText) {
282        mExpandButtonText = expandButtonText;
283        if (mExpandCollapseTextView != null && !mIsExpanded) {
284            mExpandCollapseTextView.setText(expandButtonText);
285        }
286    }
287
288    /**
289     * Sets the text for the expand button.
290     *
291     * @param expandButtonText The expand button text.
292     */
293    public void setCollapseButtonText(CharSequence expandButtonText) {
294        mCollapseButtonText = expandButtonText;
295        if (mExpandCollapseTextView != null && mIsExpanded) {
296            mExpandCollapseTextView.setText(mCollapseButtonText);
297        }
298    }
299
300    @Override
301    public void setOnClickListener(OnClickListener listener) {
302        mOnClickListener = listener;
303    }
304
305    @Override
306    public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) {
307        mOnCreateContextMenuListener = listener;
308    }
309
310    private void insertEntriesIntoViewGroup() {
311        mEntriesViewGroup.removeAllViews();
312
313        if (mIsExpanded) {
314            for (List<View> viewList : mEntryViews) {
315                if (viewList != mEntryViews.get(0)) {
316                    addSeparator(viewList.get(0));
317                }
318                for (View view : viewList) {
319                    addEntry(view);
320                }
321            }
322        } else {
323            // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the
324            // number of entries that need to be added that are not the head element of a list
325            // to reach mCollapsedEntriesCount.
326            int numInViewGroup = 0;
327            int extraEntries = mCollapsedEntriesCount - mEntryViews.size();
328            for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount;
329                    i++) {
330                List<View> entryViewList = mEntryViews.get(i);
331                if (i > 0) {
332                    addSeparator(entryViewList.get(0));
333                }
334                addEntry(entryViewList.get(0));
335                numInViewGroup++;
336                // Insert entries in this list to hit mCollapsedEntriesCount.
337                for (int j = 1;
338                        j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount &&
339                        extraEntries > 0;
340                        j++) {
341                    addEntry(entryViewList.get(j));
342                    numInViewGroup++;
343                    extraEntries--;
344                }
345            }
346        }
347
348        removeView(mExpandCollapseButton);
349        if (mCollapsedEntriesCount < mNumEntries
350                && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) {
351            mContainer.addView(mExpandCollapseButton, -1);
352        }
353    }
354
355    private void addEntry(View entry) {
356        // If no title and the first entry in the group, add extra padding
357        if (TextUtils.isEmpty(mTitleTextView.getText()) &&
358                mEntriesViewGroup.getChildCount() == 0) {
359            entry.setPadding(entry.getPaddingLeft(),
360                    entry.getPaddingTop() + getResources().getDimensionPixelSize(
361                            R.dimen.expanding_entry_card_null_title_top_extra_padding),
362                    entry.getPaddingRight(),
363                    entry.getPaddingBottom());
364        }
365        mEntriesViewGroup.addView(entry);
366    }
367
368    private void addSeparator(View entry) {
369        View separator = new View(getContext());
370        Resources res = getResources();
371
372        separator.setBackgroundColor(res.getColor(
373                R.color.expanding_entry_card_item_separator_color));
374        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
375                ViewGroup.LayoutParams.MATCH_PARENT,
376                res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_separator_height));
377        // The separator is aligned with the text in the entry. This is offset by a default
378        // margin. If there is an icon present, the icon's width and margin are added
379        int marginStart = res.getDimensionPixelSize(
380                R.dimen.expanding_entry_card_item_padding_start);
381        ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon);
382        if (entryIcon.getVisibility() == View.VISIBLE) {
383            int imageWidthAndMargin =
384                    res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) +
385                    res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing);
386            marginStart += imageWidthAndMargin;
387        }
388        layoutParams.setMarginStart(marginStart);
389        separator.setLayoutParams(layoutParams);
390        mEntriesViewGroup.addView(separator);
391    }
392
393    private CharSequence getExpandButtonText() {
394        if (!TextUtils.isEmpty(mExpandButtonText)) {
395            return mExpandButtonText;
396        } else {
397            // Default to "See more".
398            return getResources().getText(R.string.expanding_entry_card_view_see_more);
399        }
400    }
401
402    private CharSequence getCollapseButtonText() {
403        if (!TextUtils.isEmpty(mCollapseButtonText)) {
404            return mCollapseButtonText;
405        } else {
406            // Default to "See less".
407            return getResources().getText(R.string.expanding_entry_card_view_see_less);
408        }
409    }
410
411    /**
412     * Inflates the initial entries to be shown.
413     */
414    private void inflateInitialEntries(LayoutInflater layoutInflater) {
415        // If the number of collapsed entries equals total entries, inflate all
416        if (mCollapsedEntriesCount == mNumEntries) {
417            inflateAllEntries(layoutInflater);
418        } else {
419            // Otherwise inflate the top entry from each list
420            // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached.
421            int numInflated = 0;
422            int extraEntries = mCollapsedEntriesCount - mEntries.size();
423            for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) {
424                List<Entry> entryList = mEntries.get(i);
425                List<View> entryViewList = mEntryViews.get(i);
426
427                entryViewList.add(createEntryView(layoutInflater, entryList.get(0),
428                        /* showIcon = */ View.VISIBLE));
429                numInflated++;
430                // Inflate entries in this list to hit mCollapsedEntriesCount.
431                for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount &&
432                        extraEntries > 0; j++) {
433                    entryViewList.add(createEntryView(layoutInflater, entryList.get(j),
434                            /* showIcon = */ View.INVISIBLE));
435                    numInflated++;
436                    extraEntries--;
437                }
438            }
439        }
440    }
441
442    /**
443     * Inflates all entries.
444     */
445    private void inflateAllEntries(LayoutInflater layoutInflater) {
446        if (mAllEntriesInflated) {
447            return;
448        }
449        for (int i = 0; i < mEntries.size(); i++) {
450            List<Entry> entryList = mEntries.get(i);
451            List<View> viewList = mEntryViews.get(i);
452            for (int j = viewList.size(); j < entryList.size(); j++) {
453                final int iconVisibility;
454                final Entry entry = entryList.get(j);
455                // If the entry does not have an icon, mark gone. Else if it has an icon, show
456                // for the first Entry in the list only
457                if (entry.getIcon() == null) {
458                    iconVisibility = View.GONE;
459                } else if (j == 0) {
460                    iconVisibility = View.VISIBLE;
461                } else {
462                    iconVisibility = View.INVISIBLE;
463                }
464                viewList.add(createEntryView(layoutInflater, entry, iconVisibility));
465            }
466        }
467        mAllEntriesInflated = true;
468    }
469
470    public void setColorAndFilter(int color, ColorFilter colorFilter) {
471        mThemeColor = color;
472        mThemeColorFilter = colorFilter;
473        applyColor();
474    }
475
476    public void setEntryHeaderColor(int color) {
477        if (mEntries != null) {
478            for (List<View> entryList : mEntryViews) {
479                for (View entryView : entryList) {
480                    TextView header = (TextView) entryView.findViewById(R.id.header);
481                    if (header != null) {
482                        header.setTextColor(color);
483                    }
484                }
485            }
486        }
487    }
488
489    /**
490     * The ColorFilter is passed in along with the color so that a new one only needs to be created
491     * once for the entire activity.
492     * 1. Title
493     * 2. Entry icons
494     * 3. Expand/Collapse Text
495     * 4. Expand/Collapse Button
496     */
497    public void applyColor() {
498        if (mThemeColor != 0 && mThemeColorFilter != null) {
499            // Title
500            if (mTitleTextView != null) {
501                mTitleTextView.setTextColor(mThemeColor);
502            }
503
504            // Entry icons
505            if (mEntries != null) {
506                for (List<Entry> entryList : mEntries) {
507                    for (Entry entry : entryList) {
508                        if (entry.shouldApplyColor()) {
509                            Drawable icon = entry.getIcon();
510                            if (icon != null) {
511                                icon.setColorFilter(mThemeColorFilter);
512                            }
513                        }
514                        Drawable alternateIcon = entry.getAlternateIcon();
515                        if (alternateIcon != null) {
516                            alternateIcon.setColorFilter(mThemeColorFilter);
517                        }
518                    }
519                }
520            }
521
522            // Expand/Collapse
523            mExpandCollapseTextView.setTextColor(mThemeColor);
524            mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
525        }
526    }
527
528    private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
529            int iconVisibility) {
530        final EntryView view = (EntryView) layoutInflater.inflate(
531                R.layout.expanding_entry_card_item, this, false);
532
533        view.setContextMenuInfo(entry.getEntryContextMenuInfo());
534
535        final ImageView icon = (ImageView) view.findViewById(R.id.icon);
536        icon.setVisibility(iconVisibility);
537        if (entry.getIcon() != null) {
538            icon.setImageDrawable(entry.getIcon());
539        }
540        final TextView header = (TextView) view.findViewById(R.id.header);
541        if (!TextUtils.isEmpty(entry.getHeader())) {
542            header.setText(entry.getHeader());
543        } else {
544            header.setVisibility(View.GONE);
545        }
546
547        final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
548        if (!TextUtils.isEmpty(entry.getSubHeader())) {
549            subHeader.setText(entry.getSubHeader());
550        } else {
551            subHeader.setVisibility(View.GONE);
552        }
553
554        final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
555        if (entry.getSubHeaderIcon() != null) {
556            subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
557        } else {
558            subHeaderIcon.setVisibility(View.GONE);
559        }
560
561        final TextView text = (TextView) view.findViewById(R.id.text);
562        if (!TextUtils.isEmpty(entry.getText())) {
563            text.setText(entry.getText());
564        } else {
565            text.setVisibility(View.GONE);
566        }
567
568        final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
569        if (entry.getTextIcon() != null) {
570            textIcon.setImageDrawable(entry.getTextIcon());
571        } else {
572            textIcon.setVisibility(View.GONE);
573        }
574
575        if (entry.getIntent() != null) {
576            view.setOnClickListener(mOnClickListener);
577            view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
578        }
579
580        // If only the header is visible, add a top margin to match icon's top margin.
581        // Also increase the space below the header for visual comfort.
582        if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
583                text.getVisibility() == View.GONE) {
584            RelativeLayout.LayoutParams headerLayoutParams =
585                    (RelativeLayout.LayoutParams) header.getLayoutParams();
586            headerLayoutParams.topMargin = (int) (getResources().getDimension(
587                    R.dimen.expanding_entry_card_item_header_only_margin_top));
588            headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
589                    R.dimen.expanding_entry_card_item_header_only_margin_bottom));
590            header.setLayoutParams(headerLayoutParams);
591        }
592
593        final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
594        if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
595            alternateIcon.setImageDrawable(entry.getAlternateIcon());
596            alternateIcon.setOnClickListener(mOnClickListener);
597            alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
598            alternateIcon.setVisibility(View.VISIBLE);
599            alternateIcon.setContentDescription(entry.getAlternateContentDescription());
600
601            // Expand the clickable area for alternate icon to be top to bottom and to end edge
602            // of the entry view
603            view.post(new Runnable() {
604                @Override
605                public void run() {
606                    final Rect alternateIconRect = new Rect();
607                    alternateIcon.getHitRect(alternateIconRect);
608
609                    alternateIconRect.bottom = view.getHeight();
610                    alternateIconRect.top = 0;
611                    if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
612                        alternateIconRect.left = 0;
613                    } else {
614                        alternateIconRect.right = view.getWidth();
615                    }
616                    final TouchDelegate touchDelegate =
617                            new TouchDelegate(alternateIconRect, alternateIcon);
618                    view.setTouchDelegate(touchDelegate);
619                }
620            });
621        }
622
623        // Decrease margin for entries that have an invisible icon
624        if (iconVisibility == View.INVISIBLE) {
625            view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
626                    view.getPaddingBottom());
627        }
628
629
630        view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
631
632        return view;
633    }
634
635    private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
636        if (mIsExpanded) {
637            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
638                    "rotation", 180);
639            animator.setDuration(duration);
640            animator.start();
641        } else {
642            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
643                    "rotation", 0);
644            animator.setDuration(duration);
645            animator.start();
646        }
647        updateBadges();
648
649        mExpandCollapseTextView.setText(buttonText);
650    }
651
652    private void updateBadges() {
653        if (mIsExpanded) {
654            mBadgeContainer.removeAllViews();
655        } else {
656            // Inflate badges if not yet created
657            if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) {
658                for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) {
659                    Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
660                    if (badgeDrawable != null) {
661                        ImageView badgeView = new ImageView(getContext());
662                        LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams(
663                                (int) getResources().getDimension(
664                                        R.dimen.expanding_entry_card_item_icon_width),
665                                (int) getResources().getDimension(
666                                        R.dimen.expanding_entry_card_item_icon_height));
667                        badgeViewParams.setMarginEnd((int) getResources().getDimension(
668                                R.dimen.expanding_entry_card_badge_separator_margin));
669                        badgeView.setLayoutParams(badgeViewParams);
670                        badgeView.setImageDrawable(badgeDrawable);
671                        mBadges.add(badgeView);
672                    }
673                }
674            }
675            mBadgeContainer.removeAllViews();
676            for (ImageView badge : mBadges) {
677                mBadgeContainer.addView(badge);
678            }
679        }
680    }
681
682    private void expand() {
683        ChangeBounds boundsTransition = new ChangeBounds();
684        boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
685
686        Fade fadeIn = new Fade(Fade.IN);
687        fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
688        fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
689
690        TransitionSet transitionSet = new TransitionSet();
691        transitionSet.addTransition(boundsTransition);
692        transitionSet.addTransition(fadeIn);
693
694        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
695                this : mAnimationViewGroup;
696
697        transitionSet.addListener(new TransitionListener() {
698            @Override
699            public void onTransitionStart(Transition transition) {
700                // The listener is used to turn off suppressing, the proper delta is not necessary
701                mListener.onExpand(0);
702            }
703
704            @Override
705            public void onTransitionEnd(Transition transition) {
706            }
707
708            @Override
709            public void onTransitionCancel(Transition transition) {
710            }
711
712            @Override
713            public void onTransitionPause(Transition transition) {
714            }
715
716            @Override
717            public void onTransitionResume(Transition transition) {
718            }
719        });
720
721        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
722
723        mIsExpanded = true;
724        // In order to insert new entries, we may need to inflate them for the first time
725        inflateAllEntries(LayoutInflater.from(getContext()));
726        insertEntriesIntoViewGroup();
727        updateExpandCollapseButton(getCollapseButtonText(),
728                DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
729    }
730
731    private void collapse() {
732        final int startingHeight = mEntriesViewGroup.getMeasuredHeight();
733        mIsExpanded = false;
734        updateExpandCollapseButton(getExpandButtonText(),
735                DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
736
737        final ChangeBounds boundsTransition = new ChangeBounds();
738        boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
739
740        final ChangeScroll scrollTransition = new ChangeScroll();
741        scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
742
743        TransitionSet transitionSet = new TransitionSet();
744        transitionSet.addTransition(boundsTransition);
745        transitionSet.addTransition(scrollTransition);
746
747        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
748                this : mAnimationViewGroup;
749
750        boundsTransition.addListener(new TransitionListener() {
751            @Override
752            public void onTransitionStart(Transition transition) {
753                /*
754                 * onTransitionStart is called after the view hierarchy has been changed but before
755                 * the animation begins.
756                 */
757                int finishingHeight = mEntriesViewGroup.getMeasuredHeight();
758                mListener.onCollapse(startingHeight - finishingHeight);
759            }
760
761            @Override
762            public void onTransitionEnd(Transition transition) {
763            }
764
765            @Override
766            public void onTransitionCancel(Transition transition) {
767            }
768
769            @Override
770            public void onTransitionPause(Transition transition) {
771            }
772
773            @Override
774            public void onTransitionResume(Transition transition) {
775            }
776        });
777
778        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
779
780        insertEntriesIntoViewGroup();
781    }
782
783    /**
784     * Returns whether the view is currently in its expanded state.
785     */
786    public boolean isExpanded() {
787        return mIsExpanded;
788    }
789
790    /**
791     * Sets the title text of this ExpandingEntryCardView.
792     * @param title The title to set. A null title will result in the title being removed.
793     */
794    public void setTitle(String title) {
795        if (mTitleTextView == null) {
796            Log.e(TAG, "mTitleTextView is null");
797        }
798        mTitleTextView.setText(title);
799        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
800        findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
801                View.GONE : View.VISIBLE);
802        // If the title is set after children have been added, reset the top entry's padding to
803        // the default. Else if the title is cleared after children have been added, set
804        // the extra top padding
805        if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
806            View firstEntry = mEntriesViewGroup.getChildAt(0);
807            firstEntry.setPadding(firstEntry.getPaddingLeft(),
808                    getResources().getDimensionPixelSize(
809                            R.dimen.expanding_entry_card_item_padding_top),
810                    firstEntry.getPaddingRight(),
811                    firstEntry.getPaddingBottom());
812        } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
813            View firstEntry = mEntriesViewGroup.getChildAt(0);
814            firstEntry.setPadding(firstEntry.getPaddingLeft(),
815                    getResources().getDimensionPixelSize(
816                            R.dimen.expanding_entry_card_item_padding_top) +
817                            getResources().getDimensionPixelSize(
818                                    R.dimen.expanding_entry_card_null_title_top_extra_padding),
819                    firstEntry.getPaddingRight(),
820                    firstEntry.getPaddingBottom());
821        }
822    }
823
824    public boolean shouldShow() {
825        return mEntries != null && mEntries.size() > 0;
826    }
827
828    public static final class EntryView extends RelativeLayout {
829        private EntryContextMenuInfo mEntryContextMenuInfo;
830
831        public EntryView(Context context) {
832            super(context);
833        }
834
835        public EntryView(Context context, AttributeSet attrs) {
836            super(context, attrs);
837        }
838
839        public void setContextMenuInfo(EntryContextMenuInfo info) {
840            mEntryContextMenuInfo = info;
841        }
842
843        @Override
844        protected ContextMenuInfo getContextMenuInfo() {
845            return mEntryContextMenuInfo;
846        }
847    }
848
849    public static final class EntryContextMenuInfo implements ContextMenuInfo {
850        private final String mCopyText;
851        private final String mCopyLabel;
852
853        public EntryContextMenuInfo(String copyText, String copyLabel) {
854            mCopyText = copyText;
855            mCopyLabel = copyLabel;
856        }
857
858        public String getCopyText() {
859            return mCopyText;
860        }
861
862        public String getCopyLabel() {
863            return mCopyLabel;
864        }
865    }
866
867    static final class EntryTag {
868        private final int mId;
869        private final Intent mIntent;
870
871        public EntryTag(int id, Intent intent) {
872            mId = id;
873            mIntent = intent;
874        }
875
876        public int getId() {
877            return mId;
878        }
879
880        public Intent getIntent() {
881            return mIntent;
882        }
883    }
884}
885