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