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