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