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