ExpandingEntryCardView.java revision 48ebbaafcf467c072e4477c98ef2faba1c65af7e
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 com.android.contacts.R;
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.graphics.ColorFilter;
27import android.graphics.drawable.Drawable;
28import android.text.TextUtils;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewTreeObserver;
35import android.view.ViewTreeObserver.OnPreDrawListener;
36import android.widget.ImageView;
37import android.widget.LinearLayout;
38import android.widget.TextView;
39
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * Display entries in a LinearLayout that can be expanded to show all entries.
45 */
46public class ExpandingEntryCardView extends LinearLayout {
47
48    private static final String TAG = "ExpandingEntryCardView";
49
50    /**
51     * Entry data.
52     */
53    public static final class Entry {
54
55        private final int mViewId;
56        private final Drawable mIcon;
57        private final String mHeader;
58        private final String mSubHeader;
59        private final Drawable mSubHeaderIcon;
60        private final String mText;
61        private final Drawable mTextIcon;
62        private final Intent mIntent;
63        private final boolean mShouldApplyColor;
64        private final boolean mIsEditable;
65
66        public Entry(int viewId, Drawable icon, String header, String subHeader, String text,
67                Intent intent, boolean shouldApplyColor, boolean isEditable) {
68            this(viewId, icon, header, subHeader, null, text, null, intent, shouldApplyColor,
69                    isEditable);
70        }
71
72        public Entry(int viewId, Drawable mainIcon, String header, String subHeader,
73                Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent,
74                boolean shouldApplyColor, boolean isEditable) {
75            mViewId = viewId;
76            mIcon = mainIcon;
77            mHeader = header;
78            mSubHeader = subHeader;
79            mSubHeaderIcon = subHeaderIcon;
80            mText = text;
81            mTextIcon = textIcon;
82            mIntent = intent;
83            mShouldApplyColor = shouldApplyColor;
84            mIsEditable = isEditable;
85        }
86
87        Drawable getIcon() {
88            return mIcon;
89        }
90
91        String getHeader() {
92            return mHeader;
93        }
94
95        String getSubHeader() {
96            return mSubHeader;
97        }
98
99        Drawable getSubHeaderIcon() {
100            return mSubHeaderIcon;
101        }
102
103        public String getText() {
104            return mText;
105        }
106
107        Drawable getTextIcon() {
108            return mTextIcon;
109        }
110
111        Intent getIntent() {
112            return mIntent;
113        }
114
115        boolean shouldApplyColor() {
116            return mShouldApplyColor;
117        }
118
119        boolean isEditable() {
120            return mIsEditable;
121        }
122
123        int getViewId() {
124            return mViewId;
125        }
126    }
127
128    public interface ExpandingEntryCardViewListener {
129        void onCollapse(int heightDelta);
130    }
131
132    private View mExpandCollapseButton;
133    private TextView mExpandCollapseTextView;
134    private TextView mTitleTextView;
135    private CharSequence mExpandButtonText;
136    private CharSequence mCollapseButtonText;
137    private OnClickListener mOnClickListener;
138    private boolean mIsExpanded = false;
139    private int mCollapsedEntriesCount;
140    private ExpandingEntryCardViewListener mListener;
141    private List<List<Entry>> mEntries;
142    private int mNumEntries = 0;
143    private boolean mAllEntriesInflated = false;
144    private List<List<View>> mEntryViews;
145    private LinearLayout mEntriesViewGroup;
146    private final Drawable mCollapseArrowDrawable;
147    private final Drawable mExpandArrowDrawable;
148    private int mThemeColor;
149    private ColorFilter mThemeColorFilter;
150
151    private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
152        @Override
153        public void onClick(View v) {
154            if (mIsExpanded) {
155                collapse();
156            } else {
157                expand();
158            }
159        }
160    };
161
162    public ExpandingEntryCardView(Context context) {
163        this(context, null);
164    }
165
166    public ExpandingEntryCardView(Context context, AttributeSet attrs) {
167        super(context, attrs);
168        LayoutInflater inflater = LayoutInflater.from(context);
169        View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
170        mEntriesViewGroup = (LinearLayout)
171                expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
172        mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
173        mCollapseArrowDrawable =
174                getResources().getDrawable(R.drawable.expanding_entry_card_collapse_white_24);
175        mExpandArrowDrawable =
176                getResources().getDrawable(R.drawable.expanding_entry_card_expand_white_24);
177
178        mExpandCollapseButton = inflater.inflate(
179                R.layout.quickcontact_expanding_entry_card_button, this, false);
180        mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
181        mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
182
183
184    }
185
186    /**
187     * Sets the Entry list to display.
188     *
189     * @param entries The Entry list to display.
190     */
191    public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
192            boolean isExpanded, ExpandingEntryCardViewListener listener) {
193        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
194        mIsExpanded = isExpanded;
195        mEntryViews = new ArrayList<List<View>>(entries.size());
196        mEntries = entries;
197        mNumEntries = 0;
198        mAllEntriesInflated = false;
199        for (List<Entry> entryList : mEntries) {
200            mNumEntries += entryList.size();
201            mEntryViews.add(new ArrayList<View>());
202        }
203        mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
204        // Only show the head of each entry list if the initial visible number falls between the
205        // number of lists and the total number of entries
206        if (mCollapsedEntriesCount > mEntries.size()) {
207            mCollapsedEntriesCount = mEntries.size();
208        }
209        mListener = listener;
210
211        if (mIsExpanded) {
212            updateExpandCollapseButton(getCollapseButtonText());
213            inflateAllEntries(layoutInflater);
214        } else {
215            updateExpandCollapseButton(getExpandButtonText());
216            inflateInitialEntries(layoutInflater);
217        }
218        insertEntriesIntoViewGroup();
219        applyColor();
220    }
221
222    /**
223     * Sets the text for the expand button.
224     *
225     * @param expandButtonText The expand button text.
226     */
227    public void setExpandButtonText(CharSequence expandButtonText) {
228        mExpandButtonText = expandButtonText;
229        if (mExpandCollapseTextView != null && !mIsExpanded) {
230            mExpandCollapseTextView.setText(expandButtonText);
231        }
232    }
233
234    /**
235     * Sets the text for the expand button.
236     *
237     * @param expandButtonText The expand button text.
238     */
239    public void setCollapseButtonText(CharSequence expandButtonText) {
240        mCollapseButtonText = expandButtonText;
241        if (mExpandCollapseTextView != null && mIsExpanded) {
242            mExpandCollapseTextView.setText(mCollapseButtonText);
243        }
244    }
245
246    @Override
247    public void setOnClickListener(OnClickListener listener) {
248        mOnClickListener = listener;
249    }
250
251    private void insertEntriesIntoViewGroup() {
252        mEntriesViewGroup.removeAllViews();
253
254        if (mIsExpanded) {
255            for (List<View> viewList : mEntryViews) {
256                for (View view : viewList) {
257                    addEntry(view);
258                }
259            }
260        } else {
261            for (int i = 0; i < mCollapsedEntriesCount; i++) {
262                addEntry(mEntryViews.get(i).get(0));
263            }
264        }
265
266        removeView(mExpandCollapseButton);
267        if (mCollapsedEntriesCount < mNumEntries
268                && mExpandCollapseButton.getParent() == null) {
269            addView(mExpandCollapseButton, -1);
270        }
271    }
272
273    private void addEntry(View entry) {
274        if (mEntriesViewGroup.getChildCount() > 0) {
275            View separator = new View(getContext());
276            separator.setBackgroundColor(getResources().getColor(
277                    R.color.expanding_entry_card_item_separator_color));
278            LayoutParams layoutParams = generateDefaultLayoutParams();
279            Resources resources = getResources();
280            layoutParams.height = resources.getDimensionPixelSize(
281                    R.dimen.expanding_entry_card_item_separator_height);
282            if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
283                layoutParams.rightMargin = resources.getDimensionPixelSize(
284                        R.dimen.expanding_entry_card_item_padding_start);
285                layoutParams.leftMargin = resources.getDimensionPixelSize(
286                        R.dimen.expanding_entry_card_item_padding_end);
287            } else {
288                layoutParams.leftMargin = resources.getDimensionPixelSize(
289                        R.dimen.expanding_entry_card_item_padding_start);
290                layoutParams.rightMargin = resources.getDimensionPixelSize(
291                        R.dimen.expanding_entry_card_item_padding_end);
292            }
293            separator.setLayoutParams(layoutParams);
294            mEntriesViewGroup.addView(separator);
295        }
296        mEntriesViewGroup.addView(entry);
297    }
298
299    private CharSequence getExpandButtonText() {
300        if (!TextUtils.isEmpty(mExpandButtonText)) {
301            return mExpandButtonText;
302        } else {
303            // Default to "See more".
304            return getResources().getText(R.string.expanding_entry_card_view_see_more);
305        }
306    }
307
308    private CharSequence getCollapseButtonText() {
309        if (!TextUtils.isEmpty(mCollapseButtonText)) {
310            return mCollapseButtonText;
311        } else {
312            // Default to "See less".
313            return getResources().getText(R.string.expanding_entry_card_view_see_less);
314        }
315    }
316
317    /**
318     * Inflates the initial entries to be shown.
319     */
320    private void inflateInitialEntries(LayoutInflater layoutInflater) {
321        // If the number of collapsed entries equals total entries, inflate all
322        if (mCollapsedEntriesCount == mNumEntries) {
323            inflateAllEntries(layoutInflater);
324        } else {
325            // Otherwise inflate the top entry from each list
326            for (int i = 0; i < mCollapsedEntriesCount; i++) {
327                mEntryViews.get(i).add(createEntryView(layoutInflater, mEntries.get(i).get(0)));
328            }
329        }
330    }
331
332    /**
333     * Inflates all entries.
334     */
335    private void inflateAllEntries(LayoutInflater layoutInflater) {
336        if (mAllEntriesInflated) {
337            return;
338        }
339        for (int i = 0; i < mEntries.size(); i++) {
340            List<Entry> entryList = mEntries.get(i);
341            List<View> viewList = mEntryViews.get(i);
342            for (int j = viewList.size(); j < entryList.size(); j++) {
343                viewList.add(createEntryView(layoutInflater, entryList.get(j)));
344            }
345        }
346        mAllEntriesInflated = true;
347    }
348
349    public void setColorAndFilter(int color, ColorFilter colorFilter) {
350        mThemeColor = color;
351        mThemeColorFilter = colorFilter;
352        applyColor();
353    }
354
355    public void setEntryHeaderColor(int color) {
356        if (mEntries != null) {
357            for (List<View> entryList : mEntryViews) {
358                for (View entryView : entryList) {
359                    TextView header = (TextView) entryView.findViewById(R.id.header);
360                    if (header != null) {
361                        header.setTextColor(color);
362                    }
363                }
364            }
365        }
366    }
367
368    /**
369     * The ColorFilter is passed in along with the color so that a new one only needs to be created
370     * once for the entire activity.
371     * 1. Title
372     * 2. Entry icons
373     * 3. Expand/Collapse Text
374     * 4. Expand/Collapse Button
375     */
376    public void applyColor() {
377        if (mThemeColor != 0 && mThemeColorFilter != null) {
378            // Title
379            if (mTitleTextView != null) {
380                mTitleTextView.setTextColor(mThemeColor);
381            }
382
383            // Entry icons
384            if (mEntries != null) {
385                for (List<Entry> entryList : mEntries) {
386                    for (Entry entry : entryList) {
387                        if (entry.shouldApplyColor()) {
388                            Drawable icon = entry.getIcon();
389                            if (icon != null) {
390                                icon.setColorFilter(mThemeColorFilter);
391                            }
392                        }
393                    }
394                }
395            }
396
397            // Expand/Collapse
398            mExpandCollapseTextView.setTextColor(mThemeColor);
399            mCollapseArrowDrawable.setColorFilter(mThemeColorFilter);
400            mExpandArrowDrawable.setColorFilter(mThemeColorFilter);
401        }
402    }
403
404    // TODO add accessibility content descriptions
405    private View createEntryView(LayoutInflater layoutInflater, Entry entry) {
406        View view = layoutInflater.inflate(
407                R.layout.expanding_entry_card_item, this, false);
408
409        view.setId(entry.getViewId());
410
411        ImageView icon = (ImageView) view.findViewById(R.id.icon);
412        if (entry.getIcon() != null) {
413            icon.setImageDrawable(entry.getIcon());
414        } else {
415            icon.setVisibility(View.GONE);
416        }
417
418        TextView header = (TextView) view.findViewById(R.id.header);
419        if (entry.getHeader() != null) {
420            header.setText(entry.getHeader());
421        } else {
422            header.setVisibility(View.GONE);
423        }
424
425        TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
426        if (entry.getSubHeader() != null) {
427            subHeader.setText(entry.getSubHeader());
428        } else {
429            subHeader.setVisibility(View.GONE);
430        }
431
432        ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
433        if (entry.getSubHeaderIcon() != null) {
434            subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
435        } else {
436            subHeaderIcon.setVisibility(View.GONE);
437        }
438
439        TextView text = (TextView) view.findViewById(R.id.text);
440        if (entry.getText() != null) {
441            text.setText(entry.getText());
442        } else {
443            text.setVisibility(View.GONE);
444        }
445
446        ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
447        if (entry.getTextIcon() != null) {
448            textIcon.setImageDrawable(entry.getTextIcon());
449        } else {
450            textIcon.setVisibility(View.GONE);
451        }
452
453        if (entry.getIntent() != null) {
454            view.setOnClickListener(mOnClickListener);
455            view.setTag(entry.getIntent());
456        }
457
458        return view;
459    }
460
461    private void updateExpandCollapseButton(CharSequence buttonText) {
462        final Drawable arrow = mIsExpanded ? mCollapseArrowDrawable : mExpandArrowDrawable;
463        if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
464            mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, arrow,
465                    null);
466        } else {
467            mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(arrow, null, null,
468                    null);
469        }
470        mExpandCollapseTextView.setText(buttonText);
471    }
472
473    private void expand() {
474        final int startingHeight = mEntriesViewGroup.getHeight();
475
476        mIsExpanded = true;
477        // In order to insert new entries, we may need to inflate them for the first time
478        inflateAllEntries(LayoutInflater.from(getContext()));
479        insertEntriesIntoViewGroup();
480        updateExpandCollapseButton(getCollapseButtonText());
481
482        // When expanding, all the TextViews haven't been laid out yet. Therefore,
483        // calling measure() would return an incorrect result. Therefore, we need a pre draw
484        // listener.
485        final ViewTreeObserver observer = mEntriesViewGroup.getViewTreeObserver();
486        observer.addOnPreDrawListener(new OnPreDrawListener() {
487            @Override
488            public boolean onPreDraw() {
489                if (observer.isAlive()) {
490                    mEntriesViewGroup.getViewTreeObserver().removeOnPreDrawListener(this);
491                }
492                createExpandAnimator(startingHeight, mEntriesViewGroup.getHeight()).start();
493                // Do not draw the final frame of the animation immediately.
494                return false;
495            }
496        });
497    }
498
499    private void collapse() {
500        int startingHeight = mEntriesViewGroup.getHeight();
501        int finishHeight = measureCollapsedViewGroupHeight();
502        mListener.onCollapse(startingHeight - finishHeight);
503
504        mIsExpanded = false;
505        updateExpandCollapseButton(getExpandButtonText());
506        createExpandAnimator(startingHeight, finishHeight).start();
507    }
508
509    private int measureCollapsedViewGroupHeight() {
510        if (mCollapsedEntriesCount == 0) {
511            return 0;
512        }
513        final View bottomCollapsedView = mEntryViews.get(mCollapsedEntriesCount - 1).get(0);
514        return bottomCollapsedView.getTop() + bottomCollapsedView.getHeight();
515    }
516
517    /**
518     * Create ValueAnimator that performs an expand animation on the content LinearLayout.
519     *
520     * The animation needs to be performed manually using a ValueAnimator, since LinearLayout
521     * doesn't have a single set-able height property (ie, no setHeight()).
522     */
523    private ValueAnimator createExpandAnimator(int start, int end) {
524        ValueAnimator animator = ValueAnimator.ofInt(start, end);
525        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
526            @Override
527            public void onAnimationUpdate(ValueAnimator valueAnimator) {
528                int value = (Integer) valueAnimator.getAnimatedValue();
529                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
530                layoutParams.height = value;
531                mEntriesViewGroup.setLayoutParams(layoutParams);
532            }
533        });
534        animator.addListener(new AnimatorListenerAdapter() {
535            @Override
536            public void onAnimationEnd(Animator animation) {
537                insertEntriesIntoViewGroup();
538                // Now that the animation is done, stop using a fixed height.
539                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
540                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
541                mEntriesViewGroup.setLayoutParams(layoutParams);
542            }
543        });
544        return animator;
545    }
546
547    /**
548     * Returns whether the view is currently in its expanded state.
549     */
550    public boolean isExpanded() {
551        return mIsExpanded;
552    }
553
554    /**
555     * Sets the title text of this ExpandingEntryCardView.
556     * @param title The title to set. A null title will result in an empty string being set.
557     */
558    public void setTitle(String title) {
559        if (mTitleTextView == null) {
560            Log.e(TAG, "mTitleTextView is null");
561        }
562        if (title == null) {
563            mTitleTextView.setText("");
564        }
565        mTitleTextView.setText(title);
566    }
567
568    public boolean shouldShow() {
569        return mEntries != null && mEntries.size() > 0;
570    }
571}
572