ExpandingEntryCardView.java revision 71032f3fb7038995297666602773ae023c1351c4
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.drawable.Drawable;
27import android.net.Uri;
28import android.provider.ContactsContract;
29import android.provider.ContactsContract.QuickContact;
30import android.support.v4.text.TextUtilsCompat;
31import android.support.v4.view.ViewCompat;
32import android.text.TextUtils;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.View.OnClickListener;
38import android.view.ViewGroup;
39import android.view.ViewTreeObserver;
40import android.view.ViewTreeObserver.OnPreDrawListener;
41import android.view.animation.AccelerateDecelerateInterpolator;
42import android.widget.FrameLayout;
43import android.widget.ImageView;
44import android.widget.LinearLayout;
45import android.widget.TextView;
46
47import java.util.ArrayList;
48import java.util.List;
49import java.util.Locale;
50
51/**
52 * Display entries in a LinearLayout that can be expanded to show all entries.
53 */
54public class ExpandingEntryCardView extends LinearLayout {
55
56    private static final String TAG = "ExpandingEntryCardView";
57
58    /**
59     * Entry data.
60     */
61    public static final class Entry {
62
63        private final Drawable mIcon;
64        private final String mHeader;
65        private final String mSubHeader;
66        private final Drawable mSubHeaderIcon;
67        private final String mText;
68        private final Drawable mTextIcon;
69        private final Intent mIntent;
70        private final boolean mIsEditable;
71
72        public Entry(Drawable icon, String header, String subHeader, String text,
73                Intent intent, boolean isEditable) {
74            this(icon, header, subHeader, null, text, null, intent, isEditable);
75        }
76
77        public Entry(Drawable mainIcon, String header, String subHeader,
78                Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent,
79                boolean isEditable) {
80            mIcon = mainIcon;
81            mHeader = header;
82            mSubHeader = subHeader;
83            mSubHeaderIcon = subHeaderIcon;
84            mText = text;
85            mTextIcon = textIcon;
86            mIntent = intent;
87            mIsEditable = isEditable;
88        }
89
90        Drawable getIcon() {
91            return mIcon;
92        }
93
94        String getHeader() {
95            return mHeader;
96        }
97
98        String getSubHeader() {
99            return mSubHeader;
100        }
101
102        Drawable getSubHeaderIcon() {
103            return mSubHeaderIcon;
104        }
105
106        public String getText() {
107            return mText;
108        }
109
110        Drawable getTextIcon() {
111            return mTextIcon;
112        }
113
114        Intent getIntent() {
115            return mIntent;
116        }
117
118        boolean isEditable() {
119            return mIsEditable;
120        }
121    }
122
123    private View mExpandCollapseButton;
124    private TextView mExpandCollapseTextView;
125    private TextView mTitleTextView;
126    private CharSequence mExpandButtonText;
127    private CharSequence mCollapseButtonText;
128    private OnClickListener mOnClickListener;
129    private boolean mIsExpanded = false;
130    private int mCollapsedEntriesCount;
131    private List<View> mEntryViews;
132    private LinearLayout mEntriesViewGroup;
133    private int mThemeColor;
134
135    private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
136        @Override
137        public void onClick(View v) {
138            if (mIsExpanded) {
139                collapse();
140            } else {
141                expand();
142            }
143        }
144    };
145
146    public ExpandingEntryCardView(Context context) {
147        super(context);
148        LayoutInflater inflater = LayoutInflater.from(context);
149        View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
150        mEntriesViewGroup = (LinearLayout)
151                expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
152        mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
153    }
154
155    public ExpandingEntryCardView(Context context, AttributeSet attrs) {
156        super(context, attrs);
157        LayoutInflater inflater = LayoutInflater.from(context);
158        View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
159        mEntriesViewGroup = (LinearLayout)
160                expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
161        mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
162    }
163
164    /**
165     * Sets the Entry list to display.
166     *
167     * @param entries The Entry list to display.
168     */
169    public void initialize(List<Entry> entries, int numInitialVisibleEntries,
170            boolean isExpanded, int themeColor) {
171        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
172        mIsExpanded = isExpanded;
173        mEntryViews = createEntryViews(layoutInflater, entries);
174        mThemeColor = themeColor;
175        mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, entries.size());
176        if (mExpandCollapseButton == null) {
177            createExpandButton(layoutInflater);
178        }
179        insertEntriesIntoViewGroup();
180    }
181
182    /**
183     * Sets the text for the expand button.
184     *
185     * @param expandButtonText The expand button text.
186     */
187    public void setExpandButtonText(CharSequence expandButtonText) {
188        mExpandButtonText = expandButtonText;
189        if (mExpandCollapseTextView != null && !mIsExpanded) {
190            mExpandCollapseTextView.setText(expandButtonText);
191        }
192    }
193
194    /**
195     * Sets the text for the expand button.
196     *
197     * @param expandButtonText The expand button text.
198     */
199    public void setCollapseButtonText(CharSequence expandButtonText) {
200        mCollapseButtonText = expandButtonText;
201        if (mExpandCollapseTextView != null && mIsExpanded) {
202            mExpandCollapseTextView.setText(mCollapseButtonText);
203        }
204    }
205
206    @Override
207    public void setOnClickListener(OnClickListener listener) {
208        mOnClickListener = listener;
209    }
210
211    private void insertEntriesIntoViewGroup() {
212        mEntriesViewGroup.removeAllViews();
213        for (int i = 0; i < mCollapsedEntriesCount; ++i) {
214            addEntry(mEntryViews.get(i));
215        }
216        if (mIsExpanded) {
217            for (int i = mCollapsedEntriesCount; i < mEntryViews.size(); ++i) {
218                addEntry(mEntryViews.get(i));
219            }
220        }
221
222        removeView(mExpandCollapseButton);
223        if (mCollapsedEntriesCount < mEntryViews.size()
224                && mExpandCollapseButton.getParent() == null) {
225            addView(mExpandCollapseButton, -1);
226        }
227    }
228
229    private void addEntry(View entry) {
230        if (mEntriesViewGroup.getChildCount() > 0) {
231            View separator = new View(getContext());
232            separator.setBackgroundColor(getResources().getColor(
233                    R.color.expanding_entry_card_item_separator_color));
234            LayoutParams layoutParams = generateDefaultLayoutParams();
235            Resources resources = getResources();
236            layoutParams.height = resources.getDimensionPixelSize(
237                    R.dimen.expanding_entry_card_item_separator_height);
238            if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
239                layoutParams.rightMargin = resources.getDimensionPixelSize(
240                        R.dimen.expanding_entry_card_item_padding_start);
241                layoutParams.leftMargin = resources.getDimensionPixelSize(
242                        R.dimen.expanding_entry_card_item_padding_end);
243            } else {
244                layoutParams.leftMargin = resources.getDimensionPixelSize(
245                        R.dimen.expanding_entry_card_item_padding_start);
246                layoutParams.rightMargin = resources.getDimensionPixelSize(
247                        R.dimen.expanding_entry_card_item_padding_end);
248            }
249            separator.setLayoutParams(layoutParams);
250            mEntriesViewGroup.addView(separator);
251        }
252        mEntriesViewGroup.addView(entry);
253    }
254
255    private CharSequence getExpandButtonText() {
256        if (!TextUtils.isEmpty(mExpandButtonText)) {
257            return mExpandButtonText;
258        } else {
259            // Default to "See more".
260            return getResources().getText(R.string.expanding_entry_card_view_see_more);
261        }
262    }
263
264    private CharSequence getCollapseButtonText() {
265        if (!TextUtils.isEmpty(mCollapseButtonText)) {
266            return mCollapseButtonText;
267        } else {
268            // Default to "See less".
269            return getResources().getText(R.string.expanding_entry_card_view_see_less);
270        }
271    }
272
273    private void createExpandButton(LayoutInflater layoutInflater) {
274        mExpandCollapseButton = layoutInflater.inflate(
275                R.layout.quickcontact_expanding_entry_card_button, this, false);
276        mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
277        if (mIsExpanded) {
278            updateExpandCollapseButton(getCollapseButtonText());
279        } else {
280            updateExpandCollapseButton(getExpandButtonText());
281        }
282        mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
283    }
284
285    private List<View> createEntryViews(LayoutInflater layoutInflater, List<Entry> entries) {
286        ArrayList<View> views = new ArrayList<View>(entries.size());
287        for (Entry entry : entries) {
288            views.add(createEntryView(layoutInflater, entry));
289        }
290        return views;
291    }
292
293    private View createEntryView(LayoutInflater layoutInflater, Entry entry) {
294        View view = layoutInflater.inflate(
295                R.layout.expanding_entry_card_item, this, false);
296
297        ImageView icon = (ImageView) view.findViewById(R.id.icon);
298        icon.setImageDrawable(entry.getIcon());
299
300        TextView header = (TextView) view.findViewById(R.id.header);
301        if (entry.getHeader() != null) {
302            header.setText(entry.getHeader());
303        } else {
304            header.setVisibility(View.GONE);
305        }
306
307        TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
308        if (entry.getSubHeader() != null) {
309            subHeader.setText(entry.getSubHeader());
310        } else {
311            subHeader.setVisibility(View.GONE);
312        }
313
314        ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
315        if (entry.getSubHeaderIcon() != null) {
316            subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
317        } else {
318            subHeaderIcon.setVisibility(View.GONE);
319        }
320
321        TextView text = (TextView) view.findViewById(R.id.text);
322        if (entry.getText() != null) {
323            text.setText(entry.getText());
324        } else {
325            text.setVisibility(View.GONE);
326        }
327
328        ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
329        if (entry.getTextIcon() != null) {
330            textIcon.setImageDrawable(entry.getTextIcon());
331        } else {
332            textIcon.setVisibility(View.GONE);
333        }
334
335        if (entry.getIntent() != null) {
336            View entryLayout = view.findViewById(R.id.entry_layout);
337            entryLayout.setOnClickListener(mOnClickListener);
338            entryLayout.setTag(entry.getIntent());
339        }
340
341        return view;
342    }
343
344    private void updateExpandCollapseButton(CharSequence buttonText) {
345        int resId = mIsExpanded ? R.drawable.expanding_entry_card_collapse_white_24
346                : R.drawable.expanding_entry_card_expand_white_24;
347        // TODO: apply color theme to the drawable
348        Drawable drawable = getResources().getDrawable(resId);
349        if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
350            mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable,
351                    null);
352        } else {
353            mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null,
354                    null);
355        }
356        mExpandCollapseTextView.setText(buttonText);
357    }
358
359    private void expand() {
360        final int startingHeight = mEntriesViewGroup.getHeight();
361
362        mIsExpanded = true;
363        insertEntriesIntoViewGroup();
364        updateExpandCollapseButton(getCollapseButtonText());
365
366        // When expanding, all the TextViews haven't been laid out yet. Therefore,
367        // calling measure() would return an incorrect result. Therefore, we need a pre draw
368        // listener.
369        final ViewTreeObserver observer = mEntriesViewGroup.getViewTreeObserver();
370        observer.addOnPreDrawListener(new OnPreDrawListener() {
371            @Override
372            public boolean onPreDraw() {
373                if (observer.isAlive()) {
374                    mEntriesViewGroup.getViewTreeObserver().removeOnPreDrawListener(this);
375                }
376                createExpandAnimator(startingHeight, mEntriesViewGroup.getHeight()).start();
377                // Do not draw the final frame of the animation immediately.
378                return false;
379            }
380        });
381    }
382
383    private void collapse() {
384        int startingHeight = mEntriesViewGroup.getHeight();
385        int finishHeight = measureCollapsedViewGroupHeight();
386
387        mIsExpanded = false;
388        updateExpandCollapseButton(getExpandButtonText());
389        createExpandAnimator(startingHeight, finishHeight).start();
390    }
391
392    private int measureCollapsedViewGroupHeight() {
393        if (mCollapsedEntriesCount == 0) {
394            return 0;
395        }
396        final View bottomCollapsedView = mEntryViews.get(mCollapsedEntriesCount - 1);
397        return bottomCollapsedView.getTop() + bottomCollapsedView.getHeight();
398    }
399
400    /**
401     * Create ValueAnimator that performs an expand animation on the content LinearLayout.
402     *
403     * The animation needs to be performed manually using a ValueAnimator, since LinearLayout
404     * doesn't have a single set-able height property (ie, no setHeight()).
405     */
406    private ValueAnimator createExpandAnimator(int start, int end) {
407        ValueAnimator animator = ValueAnimator.ofInt(start, end);
408        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
409            @Override
410            public void onAnimationUpdate(ValueAnimator valueAnimator) {
411                int value = (Integer) valueAnimator.getAnimatedValue();
412                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
413                layoutParams.height = value;
414                mEntriesViewGroup.setLayoutParams(layoutParams);
415            }
416        });
417        animator.addListener(new AnimatorListenerAdapter() {
418            @Override
419            public void onAnimationEnd(Animator animation) {
420                insertEntriesIntoViewGroup();
421                // Now that the animation is done, stop using a fixed height.
422                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
423                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
424                mEntriesViewGroup.setLayoutParams(layoutParams);
425            }
426        });
427        return animator;
428    }
429
430    /**
431     * Returns whether the view is currently in its expanded state.
432     */
433    public boolean isExpanded() {
434        return mIsExpanded;
435    }
436
437    /**
438     * Sets the title text of this ExpandingEntryCardView.
439     * @param title The title to set. A null title will result in an empty string being set.
440     */
441    public void setTitle(String title) {
442        if (mTitleTextView == null) {
443            Log.e(TAG, "mTitleTextView is null");
444        }
445        if (title == null) {
446            mTitleTextView.setText("");
447        }
448        mTitleTextView.setText(title);
449    }
450}
451