ExpandingEntryCardView.java revision 7ce5352a70e8aaf120bf4f7bd05d595f46abb080
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.expanding_entry_card_item_separator_color));
425        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
426                ViewGroup.LayoutParams.MATCH_PARENT,
427                res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_separator_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.setColorFilter(mThemeColorFilter);
563                            }
564                        }
565                        Drawable alternateIcon = entry.getAlternateIcon();
566                        if (alternateIcon != null) {
567                            alternateIcon.setColorFilter(mThemeColorFilter);
568                        }
569                        Drawable thirdIcon = entry.getThirdIcon();
570                        if (thirdIcon != null) {
571                            thirdIcon.setColorFilter(mThemeColorFilter);
572                        }
573                    }
574                }
575            }
576
577            // Expand/Collapse
578            mExpandCollapseTextView.setTextColor(mThemeColor);
579            mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
580        }
581    }
582
583    private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
584            int iconVisibility) {
585        final EntryView view = (EntryView) layoutInflater.inflate(
586                R.layout.expanding_entry_card_item, this, false);
587
588        view.setContextMenuInfo(entry.getEntryContextMenuInfo());
589        if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) {
590            view.setContentDescription(entry.getPrimaryContentDescription());
591        }
592
593        final ImageView icon = (ImageView) view.findViewById(R.id.icon);
594        icon.setVisibility(iconVisibility);
595        if (entry.getIcon() != null) {
596            icon.setImageDrawable(entry.getIcon());
597        }
598        final TextView header = (TextView) view.findViewById(R.id.header);
599        if (!TextUtils.isEmpty(entry.getHeader())) {
600            header.setText(entry.getHeader());
601        } else {
602            header.setVisibility(View.GONE);
603        }
604
605        final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
606        if (!TextUtils.isEmpty(entry.getSubHeader())) {
607            subHeader.setText(entry.getSubHeader());
608        } else {
609            subHeader.setVisibility(View.GONE);
610        }
611
612        final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
613        if (entry.getSubHeaderIcon() != null) {
614            subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
615        } else {
616            subHeaderIcon.setVisibility(View.GONE);
617        }
618
619        final TextView text = (TextView) view.findViewById(R.id.text);
620        if (!TextUtils.isEmpty(entry.getText())) {
621            text.setText(entry.getText());
622        } else {
623            text.setVisibility(View.GONE);
624        }
625
626        final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
627        if (entry.getTextIcon() != null) {
628            textIcon.setImageDrawable(entry.getTextIcon());
629        } else {
630            textIcon.setVisibility(View.GONE);
631        }
632
633        if (entry.getIntent() != null) {
634            view.setOnClickListener(mOnClickListener);
635            view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
636        }
637
638        if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
639            // Remove the click effect
640            view.setBackground(null);
641        }
642
643        // If only the header is visible, add a top margin to match icon's top margin.
644        // Also increase the space below the header for visual comfort.
645        if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
646                text.getVisibility() == View.GONE) {
647            RelativeLayout.LayoutParams headerLayoutParams =
648                    (RelativeLayout.LayoutParams) header.getLayoutParams();
649            headerLayoutParams.topMargin = (int) (getResources().getDimension(
650                    R.dimen.expanding_entry_card_item_header_only_margin_top));
651            headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
652                    R.dimen.expanding_entry_card_item_header_only_margin_bottom));
653            header.setLayoutParams(headerLayoutParams);
654        }
655
656        // Adjust the top padding size for entries with an invisible icon. The padding depends on
657        // if there is a sub header or text section
658        if (iconVisibility == View.INVISIBLE &&
659                (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
660            view.setPaddingRelative(view.getPaddingStart(),
661                    getResources().getDimensionPixelSize(
662                            R.dimen.expanding_entry_card_item_no_icon_margin_top),
663                    view.getPaddingEnd(),
664                    view.getPaddingBottom());
665        } else if (iconVisibility == View.INVISIBLE &&  TextUtils.isEmpty(entry.getSubHeader())
666                && TextUtils.isEmpty(entry.getText())) {
667            view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
668                    view.getPaddingBottom());
669        }
670
671        final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
672        final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
673
674        if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
675            alternateIcon.setImageDrawable(entry.getAlternateIcon());
676            alternateIcon.setOnClickListener(mOnClickListener);
677            alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
678            alternateIcon.setVisibility(View.VISIBLE);
679            alternateIcon.setContentDescription(entry.getAlternateContentDescription());
680        }
681
682        if (entry.getThirdIcon() != null && entry.getThirdIntent() != null) {
683            thirdIcon.setImageDrawable(entry.getThirdIcon());
684            thirdIcon.setOnClickListener(mOnClickListener);
685            thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
686            thirdIcon.setVisibility(View.VISIBLE);
687            thirdIcon.setContentDescription(entry.getThirdContentDescription());
688        }
689
690        // Set a custom touch listener for expanding the extra icon touch areas
691        view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
692        view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
693
694        return view;
695    }
696
697    private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
698        if (mIsExpanded) {
699            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
700                    "rotation", 180);
701            animator.setDuration(duration);
702            animator.start();
703        } else {
704            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
705                    "rotation", 0);
706            animator.setDuration(duration);
707            animator.start();
708        }
709        updateBadges();
710
711        mExpandCollapseTextView.setText(buttonText);
712    }
713
714    private void updateBadges() {
715        if (mIsExpanded) {
716            mBadgeContainer.removeAllViews();
717        } else {
718            // Inflate badges if not yet created
719            if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) {
720                for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) {
721                    Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
722                    int badgeResourceId = mEntries.get(i).get(0).getIconResourceId();
723                    // Do not add the same badge twice
724                    if (badgeResourceId != 0 && mBadgeIds.contains(badgeResourceId)) {
725                        continue;
726                    }
727                    if (badgeDrawable != null) {
728                        ImageView badgeView = new ImageView(getContext());
729                        LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams(
730                                (int) getResources().getDimension(
731                                        R.dimen.expanding_entry_card_item_icon_width),
732                                (int) getResources().getDimension(
733                                        R.dimen.expanding_entry_card_item_icon_height));
734                        badgeViewParams.setMarginEnd((int) getResources().getDimension(
735                                R.dimen.expanding_entry_card_badge_separator_margin));
736                        badgeView.setLayoutParams(badgeViewParams);
737                        badgeView.setImageDrawable(badgeDrawable);
738                        mBadges.add(badgeView);
739                        mBadgeIds.add(badgeResourceId);
740                    }
741                }
742            }
743            mBadgeContainer.removeAllViews();
744            for (ImageView badge : mBadges) {
745                mBadgeContainer.addView(badge);
746            }
747        }
748    }
749
750    private void expand() {
751        ChangeBounds boundsTransition = new ChangeBounds();
752        boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
753
754        Fade fadeIn = new Fade(Fade.IN);
755        fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
756        fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
757
758        TransitionSet transitionSet = new TransitionSet();
759        transitionSet.addTransition(boundsTransition);
760        transitionSet.addTransition(fadeIn);
761
762        transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
763
764        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
765                this : mAnimationViewGroup;
766
767        transitionSet.addListener(new TransitionListener() {
768            @Override
769            public void onTransitionStart(Transition transition) {
770                // The listener is used to turn off suppressing, the proper delta is not necessary
771                mListener.onExpand(0);
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        mIsExpanded = true;
794        // In order to insert new entries, we may need to inflate them for the first time
795        inflateAllEntries(LayoutInflater.from(getContext()));
796        insertEntriesIntoViewGroup();
797        updateExpandCollapseButton(getCollapseButtonText(),
798                DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
799    }
800
801    private void collapse() {
802        final int startingHeight = mEntriesViewGroup.getMeasuredHeight();
803        mIsExpanded = false;
804        updateExpandCollapseButton(getExpandButtonText(),
805                DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
806
807        final ChangeBounds boundsTransition = new ChangeBounds();
808        boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
809
810        final ChangeScroll scrollTransition = new ChangeScroll();
811        scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
812
813        TransitionSet transitionSet = new TransitionSet();
814        transitionSet.addTransition(boundsTransition);
815        transitionSet.addTransition(scrollTransition);
816
817        transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
818
819        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
820                this : mAnimationViewGroup;
821
822        boundsTransition.addListener(new TransitionListener() {
823            @Override
824            public void onTransitionStart(Transition transition) {
825                /*
826                 * onTransitionStart is called after the view hierarchy has been changed but before
827                 * the animation begins.
828                 */
829                int finishingHeight = mEntriesViewGroup.getMeasuredHeight();
830                mListener.onCollapse(startingHeight - finishingHeight);
831            }
832
833            @Override
834            public void onTransitionEnd(Transition transition) {
835            }
836
837            @Override
838            public void onTransitionCancel(Transition transition) {
839            }
840
841            @Override
842            public void onTransitionPause(Transition transition) {
843            }
844
845            @Override
846            public void onTransitionResume(Transition transition) {
847            }
848        });
849
850        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
851
852        insertEntriesIntoViewGroup();
853    }
854
855    /**
856     * Returns whether the view is currently in its expanded state.
857     */
858    public boolean isExpanded() {
859        return mIsExpanded;
860    }
861
862    /**
863     * Sets the title text of this ExpandingEntryCardView.
864     * @param title The title to set. A null title will result in the title being removed.
865     */
866    public void setTitle(String title) {
867        if (mTitleTextView == null) {
868            Log.e(TAG, "mTitleTextView is null");
869        }
870        mTitleTextView.setText(title);
871        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
872        findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
873                View.GONE : View.VISIBLE);
874        // If the title is set after children have been added, reset the top entry's padding to
875        // the default. Else if the title is cleared after children have been added, set
876        // the extra top padding
877        if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
878            View firstEntry = mEntriesViewGroup.getChildAt(0);
879            firstEntry.setPadding(firstEntry.getPaddingLeft(),
880                    getResources().getDimensionPixelSize(
881                            R.dimen.expanding_entry_card_item_padding_top),
882                    firstEntry.getPaddingRight(),
883                    firstEntry.getPaddingBottom());
884        } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
885            View firstEntry = mEntriesViewGroup.getChildAt(0);
886            firstEntry.setPadding(firstEntry.getPaddingLeft(),
887                    getResources().getDimensionPixelSize(
888                            R.dimen.expanding_entry_card_item_padding_top) +
889                            getResources().getDimensionPixelSize(
890                                    R.dimen.expanding_entry_card_null_title_top_extra_padding),
891                    firstEntry.getPaddingRight(),
892                    firstEntry.getPaddingBottom());
893        }
894    }
895
896    public boolean shouldShow() {
897        return mEntries != null && mEntries.size() > 0;
898    }
899
900    public static final class EntryView extends RelativeLayout {
901        private EntryContextMenuInfo mEntryContextMenuInfo;
902
903        public EntryView(Context context) {
904            super(context);
905        }
906
907        public EntryView(Context context, AttributeSet attrs) {
908            super(context, attrs);
909        }
910
911        public void setContextMenuInfo(EntryContextMenuInfo info) {
912            mEntryContextMenuInfo = info;
913        }
914
915        @Override
916        protected ContextMenuInfo getContextMenuInfo() {
917            return mEntryContextMenuInfo;
918        }
919    }
920
921    public static final class EntryContextMenuInfo implements ContextMenuInfo {
922        private final String mCopyText;
923        private final String mCopyLabel;
924        private final String mMimeType;
925        private final long mId;
926        private final boolean mIsSuperPrimary;
927
928        public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id,
929                boolean isSuperPrimary) {
930            mCopyText = copyText;
931            mCopyLabel = copyLabel;
932            mMimeType = mimeType;
933            mId = id;
934            mIsSuperPrimary = isSuperPrimary;
935        }
936
937        public String getCopyText() {
938            return mCopyText;
939        }
940
941        public String getCopyLabel() {
942            return mCopyLabel;
943        }
944
945        public String getMimeType() {
946            return mMimeType;
947        }
948
949        public long getId() {
950            return mId;
951        }
952
953        public boolean isSuperPrimary() {
954            return mIsSuperPrimary;
955        }
956    }
957
958    static final class EntryTag {
959        private final int mId;
960        private final Intent mIntent;
961
962        public EntryTag(int id, Intent intent) {
963            mId = id;
964            mIntent = intent;
965        }
966
967        public int getId() {
968            return mId;
969        }
970
971        public Intent getIntent() {
972            return mIntent;
973        }
974    }
975
976    /**
977     * This custom touch listener increases the touch area for the second and third icons, if
978     * they are present. This is necessary to maintain other properties on an entry view, like
979     * using a top padding on entry. Based off of {@link android.view.TouchDelegate}
980     */
981    private static final class EntryTouchListener implements View.OnTouchListener {
982        private final View mEntry;
983        private final ImageView mAlternateIcon;
984        private final ImageView mThirdIcon;
985        /** mTouchedView locks in a view on touch down */
986        private View mTouchedView;
987        /** mSlop adds some space to account for touches that are just outside the hit area */
988        private int mSlop;
989
990        public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) {
991            mEntry = entry;
992            mAlternateIcon = alternateIcon;
993            mThirdIcon = thirdIcon;
994            mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop();
995        }
996
997        @Override
998        public boolean onTouch(View v, MotionEvent event) {
999            View touchedView = mTouchedView;
1000            boolean sendToTouched = false;
1001            boolean hit = true;
1002            boolean handled = false;
1003
1004            switch (event.getAction()) {
1005                case MotionEvent.ACTION_DOWN:
1006                    if (hitThirdIcon(event)) {
1007                        mTouchedView = mThirdIcon;
1008                        sendToTouched = true;
1009                    } else if (hitAlternateIcon(event)) {
1010                        mTouchedView = mAlternateIcon;
1011                        sendToTouched = true;
1012                    } else {
1013                        mTouchedView = mEntry;
1014                        sendToTouched = false;
1015                    }
1016                    touchedView = mTouchedView;
1017                    break;
1018                case MotionEvent.ACTION_UP:
1019                case MotionEvent.ACTION_MOVE:
1020                    sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1021                    if (sendToTouched) {
1022                        final Rect slopBounds = new Rect();
1023                        touchedView.getHitRect(slopBounds);
1024                        slopBounds.inset(-mSlop, -mSlop);
1025                        if (!slopBounds.contains((int) event.getX(), (int) event.getY())) {
1026                            hit = false;
1027                        }
1028                    }
1029                    break;
1030                case MotionEvent.ACTION_CANCEL:
1031                    sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1032                    mTouchedView = null;
1033                    break;
1034            }
1035            if (sendToTouched) {
1036                if (hit) {
1037                    event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2);
1038                } else {
1039                    // Offset event coordinates to be outside the target view (in case it does
1040                    // something like tracking pressed state)
1041                    event.setLocation(-(mSlop * 2), -(mSlop * 2));
1042                }
1043                handled = touchedView.dispatchTouchEvent(event);
1044            }
1045            return handled;
1046        }
1047
1048        private boolean hitThirdIcon(MotionEvent event) {
1049            if (mEntry.isLayoutRtl()) {
1050                return mThirdIcon.getVisibility() == View.VISIBLE &&
1051                        event.getX() < mThirdIcon.getRight();
1052            } else {
1053                return mThirdIcon.getVisibility() == View.VISIBLE &&
1054                        event.getX() > mThirdIcon.getLeft();
1055            }
1056        }
1057
1058        /**
1059         * Should be used after checking if third icon was hit
1060         */
1061        private boolean hitAlternateIcon(MotionEvent event) {
1062            // LayoutParams used to add the start margin to the touch area
1063            final RelativeLayout.LayoutParams alternateIconParams =
1064                    (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams();
1065            if (mEntry.isLayoutRtl()) {
1066                return mAlternateIcon.getVisibility() == View.VISIBLE &&
1067                        event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin;
1068            } else {
1069                return mAlternateIcon.getVisibility() == View.VISIBLE &&
1070                        event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin;
1071            }
1072        }
1073    }
1074}
1075