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