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