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