ExpandingEntryCardView.java revision edd44f03e0e7aa57d4eb721147d4a3f3d2551c1a
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        // Adjust the top padding size for entries with an invisible icon. The padding depends on
624        // if there is a sub header or text section
625        if (iconVisibility == View.INVISIBLE &&
626                (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
627            view.setPaddingRelative(view.getPaddingStart(),
628                    getResources().getDimensionPixelSize(
629                            R.dimen.expanding_entry_card_item_no_icon_margin_top),
630                    view.getPaddingEnd(),
631                    view.getPaddingBottom());
632        } else if (iconVisibility == View.INVISIBLE &&  TextUtils.isEmpty(entry.getSubHeader())
633                && TextUtils.isEmpty(entry.getText())) {
634            view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
635                    view.getPaddingBottom());
636        }
637
638        view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
639
640        return view;
641    }
642
643    private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
644        if (mIsExpanded) {
645            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
646                    "rotation", 180);
647            animator.setDuration(duration);
648            animator.start();
649        } else {
650            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
651                    "rotation", 0);
652            animator.setDuration(duration);
653            animator.start();
654        }
655        updateBadges();
656
657        mExpandCollapseTextView.setText(buttonText);
658    }
659
660    private void updateBadges() {
661        if (mIsExpanded) {
662            mBadgeContainer.removeAllViews();
663        } else {
664            // Inflate badges if not yet created
665            if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) {
666                for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) {
667                    Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
668                    if (badgeDrawable != null) {
669                        ImageView badgeView = new ImageView(getContext());
670                        LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams(
671                                (int) getResources().getDimension(
672                                        R.dimen.expanding_entry_card_item_icon_width),
673                                (int) getResources().getDimension(
674                                        R.dimen.expanding_entry_card_item_icon_height));
675                        badgeViewParams.setMarginEnd((int) getResources().getDimension(
676                                R.dimen.expanding_entry_card_badge_separator_margin));
677                        badgeView.setLayoutParams(badgeViewParams);
678                        badgeView.setImageDrawable(badgeDrawable);
679                        mBadges.add(badgeView);
680                    }
681                }
682            }
683            mBadgeContainer.removeAllViews();
684            for (ImageView badge : mBadges) {
685                mBadgeContainer.addView(badge);
686            }
687        }
688    }
689
690    private void expand() {
691        ChangeBounds boundsTransition = new ChangeBounds();
692        boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
693
694        Fade fadeIn = new Fade(Fade.IN);
695        fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
696        fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
697
698        TransitionSet transitionSet = new TransitionSet();
699        transitionSet.addTransition(boundsTransition);
700        transitionSet.addTransition(fadeIn);
701
702        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
703                this : mAnimationViewGroup;
704
705        transitionSet.addListener(new TransitionListener() {
706            @Override
707            public void onTransitionStart(Transition transition) {
708                // The listener is used to turn off suppressing, the proper delta is not necessary
709                mListener.onExpand(0);
710            }
711
712            @Override
713            public void onTransitionEnd(Transition transition) {
714            }
715
716            @Override
717            public void onTransitionCancel(Transition transition) {
718            }
719
720            @Override
721            public void onTransitionPause(Transition transition) {
722            }
723
724            @Override
725            public void onTransitionResume(Transition transition) {
726            }
727        });
728
729        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
730
731        mIsExpanded = true;
732        // In order to insert new entries, we may need to inflate them for the first time
733        inflateAllEntries(LayoutInflater.from(getContext()));
734        insertEntriesIntoViewGroup();
735        updateExpandCollapseButton(getCollapseButtonText(),
736                DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
737    }
738
739    private void collapse() {
740        final int startingHeight = mEntriesViewGroup.getMeasuredHeight();
741        mIsExpanded = false;
742        updateExpandCollapseButton(getExpandButtonText(),
743                DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
744
745        final ChangeBounds boundsTransition = new ChangeBounds();
746        boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
747
748        final ChangeScroll scrollTransition = new ChangeScroll();
749        scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
750
751        TransitionSet transitionSet = new TransitionSet();
752        transitionSet.addTransition(boundsTransition);
753        transitionSet.addTransition(scrollTransition);
754
755        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
756                this : mAnimationViewGroup;
757
758        boundsTransition.addListener(new TransitionListener() {
759            @Override
760            public void onTransitionStart(Transition transition) {
761                /*
762                 * onTransitionStart is called after the view hierarchy has been changed but before
763                 * the animation begins.
764                 */
765                int finishingHeight = mEntriesViewGroup.getMeasuredHeight();
766                mListener.onCollapse(startingHeight - finishingHeight);
767            }
768
769            @Override
770            public void onTransitionEnd(Transition transition) {
771            }
772
773            @Override
774            public void onTransitionCancel(Transition transition) {
775            }
776
777            @Override
778            public void onTransitionPause(Transition transition) {
779            }
780
781            @Override
782            public void onTransitionResume(Transition transition) {
783            }
784        });
785
786        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
787
788        insertEntriesIntoViewGroup();
789    }
790
791    /**
792     * Returns whether the view is currently in its expanded state.
793     */
794    public boolean isExpanded() {
795        return mIsExpanded;
796    }
797
798    /**
799     * Sets the title text of this ExpandingEntryCardView.
800     * @param title The title to set. A null title will result in the title being removed.
801     */
802    public void setTitle(String title) {
803        if (mTitleTextView == null) {
804            Log.e(TAG, "mTitleTextView is null");
805        }
806        mTitleTextView.setText(title);
807        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
808        findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
809                View.GONE : View.VISIBLE);
810        // If the title is set after children have been added, reset the top entry's padding to
811        // the default. Else if the title is cleared after children have been added, set
812        // the extra top padding
813        if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
814            View firstEntry = mEntriesViewGroup.getChildAt(0);
815            firstEntry.setPadding(firstEntry.getPaddingLeft(),
816                    getResources().getDimensionPixelSize(
817                            R.dimen.expanding_entry_card_item_padding_top),
818                    firstEntry.getPaddingRight(),
819                    firstEntry.getPaddingBottom());
820        } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
821            View firstEntry = mEntriesViewGroup.getChildAt(0);
822            firstEntry.setPadding(firstEntry.getPaddingLeft(),
823                    getResources().getDimensionPixelSize(
824                            R.dimen.expanding_entry_card_item_padding_top) +
825                            getResources().getDimensionPixelSize(
826                                    R.dimen.expanding_entry_card_null_title_top_extra_padding),
827                    firstEntry.getPaddingRight(),
828                    firstEntry.getPaddingBottom());
829        }
830    }
831
832    public boolean shouldShow() {
833        return mEntries != null && mEntries.size() > 0;
834    }
835
836    public static final class EntryView extends RelativeLayout {
837        private EntryContextMenuInfo mEntryContextMenuInfo;
838
839        public EntryView(Context context) {
840            super(context);
841        }
842
843        public EntryView(Context context, AttributeSet attrs) {
844            super(context, attrs);
845        }
846
847        public void setContextMenuInfo(EntryContextMenuInfo info) {
848            mEntryContextMenuInfo = info;
849        }
850
851        @Override
852        protected ContextMenuInfo getContextMenuInfo() {
853            return mEntryContextMenuInfo;
854        }
855    }
856
857    public static final class EntryContextMenuInfo implements ContextMenuInfo {
858        private final String mCopyText;
859        private final String mCopyLabel;
860
861        public EntryContextMenuInfo(String copyText, String copyLabel) {
862            mCopyText = copyText;
863            mCopyLabel = copyLabel;
864        }
865
866        public String getCopyText() {
867            return mCopyText;
868        }
869
870        public String getCopyLabel() {
871            return mCopyLabel;
872        }
873    }
874
875    static final class EntryTag {
876        private final int mId;
877        private final Intent mIntent;
878
879        public EntryTag(int id, Intent intent) {
880            mId = id;
881            mIntent = intent;
882        }
883
884        public int getId() {
885            return mId;
886        }
887
888        public Intent getIntent() {
889            return mIntent;
890        }
891    }
892}
893