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 */
16
17package android.support.v17.leanback.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.drawable.Drawable;
22import android.support.v17.leanback.R;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.View;
26import android.view.ViewDebug;
27import android.view.ViewGroup;
28import android.view.animation.AccelerateDecelerateInterpolator;
29import android.view.animation.Animation;
30import android.view.animation.DecelerateInterpolator;
31import android.view.animation.Transformation;
32import android.widget.FrameLayout;
33
34import java.util.ArrayList;
35
36/**
37 * A card style layout that responds to certain state changes. It arranges its
38 * children in a vertical column, with different regions becoming visible at
39 * different times.
40 *
41 * <p>
42 * A BaseCardView will draw its children based on its type, the region
43 * visibilities of the child types, and the state of the widget. A child may be
44 * marked as belonging to one of three regions: main, info, or extra. The main
45 * region is always visible, while the info and extra regions can be set to
46 * display based on the activated or selected state of the View. The card states
47 * are set by calling {@link #setActivated(boolean) setActivated} and
48 * {@link #setSelected(boolean) setSelected}.
49 * <p>
50 * See {@link BaseCardView.LayoutParams} for layout attributes.
51 * </p>
52 */
53public class BaseCardView extends FrameLayout {
54    private static final String TAG = "BaseCardView";
55    private static final boolean DEBUG = false;
56
57    /**
58     * A simple card type with a single layout area. This card type does not
59     * change its layout or size as it transitions between
60     * Activated/Not-Activated or Selected/Unselected states.
61     *
62     * @see #getCardType()
63     */
64    public static final int CARD_TYPE_MAIN_ONLY = 0;
65
66    /**
67     * A Card type with 2 layout areas: A main area which is always visible, and
68     * an info area that fades in over the main area when it is visible.
69     * The card height will not change.
70     *
71     * @see #getCardType()
72     */
73    public static final int CARD_TYPE_INFO_OVER = 1;
74
75    /**
76     * A Card type with 2 layout areas: A main area which is always visible, and
77     * an info area that appears below the main area. When the info area is visible
78     * the total card height will change.
79     *
80     * @see #getCardType()
81     */
82    public static final int CARD_TYPE_INFO_UNDER = 2;
83
84    /**
85     * A Card type with 3 layout areas: A main area which is always visible; an
86     * info area which will appear below the main area, and an extra area that
87     * only appears after a short delay. The info area appears below the main
88     * area, causing the total card height to change. The extra area animates in
89     * at the bottom of the card, shifting up the info view without affecting
90     * the card height.
91     *
92     * @see #getCardType()
93     */
94    public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3;
95
96    /**
97     * Indicates that a card region is always visible.
98     */
99    public static final int CARD_REGION_VISIBLE_ALWAYS = 0;
100
101    /**
102     * Indicates that a card region is visible when the card is activated.
103     */
104    public static final int CARD_REGION_VISIBLE_ACTIVATED = 1;
105
106    /**
107     * Indicates that a card region is visible when the card is selected.
108     */
109    public static final int CARD_REGION_VISIBLE_SELECTED = 2;
110
111    private static final int CARD_TYPE_INVALID = 4;
112
113    private int mCardType;
114    private int mInfoVisibility;
115    private int mExtraVisibility;
116
117    private ArrayList<View> mMainViewList;
118    ArrayList<View> mInfoViewList;
119    ArrayList<View> mExtraViewList;
120
121    private int mMeasuredWidth;
122    private int mMeasuredHeight;
123    private boolean mDelaySelectedAnim;
124    private int mSelectedAnimationDelay;
125    private final int mActivatedAnimDuration;
126    private final int mSelectedAnimDuration;
127
128    float mInfoOffset;
129    float mInfoVisFraction;
130    float mInfoAlpha = 1.0f;
131    private Animation mAnim;
132
133    private final static int[] LB_PRESSED_STATE_SET = new int[]{
134        android.R.attr.state_pressed};
135
136    private final Runnable mAnimationTrigger = new Runnable() {
137        @Override
138        public void run() {
139            animateInfoOffset(true);
140        }
141    };
142
143    public BaseCardView(Context context) {
144        this(context, null);
145    }
146
147    public BaseCardView(Context context, AttributeSet attrs) {
148        this(context, attrs, R.attr.baseCardViewStyle);
149    }
150
151    public BaseCardView(Context context, AttributeSet attrs, int defStyleAttr) {
152        super(context, attrs, defStyleAttr);
153
154        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView,
155                defStyleAttr, 0);
156
157        try {
158            mCardType = a.getInteger(R.styleable.lbBaseCardView_cardType, CARD_TYPE_MAIN_ONLY);
159            Drawable cardForeground = a.getDrawable(R.styleable.lbBaseCardView_cardForeground);
160            if (cardForeground != null) {
161                setForeground(cardForeground);
162            }
163            Drawable cardBackground = a.getDrawable(R.styleable.lbBaseCardView_cardBackground);
164            if (cardBackground != null) {
165                setBackground(cardBackground);
166            }
167            mInfoVisibility = a.getInteger(R.styleable.lbBaseCardView_infoVisibility,
168                    CARD_REGION_VISIBLE_ACTIVATED);
169            mExtraVisibility = a.getInteger(R.styleable.lbBaseCardView_extraVisibility,
170                    CARD_REGION_VISIBLE_SELECTED);
171            // Extra region should never show before info region.
172            if (mExtraVisibility < mInfoVisibility) {
173                mExtraVisibility = mInfoVisibility;
174            }
175
176            mSelectedAnimationDelay = a.getInteger(
177                    R.styleable.lbBaseCardView_selectedAnimationDelay,
178                    getResources().getInteger(R.integer.lb_card_selected_animation_delay));
179
180            mSelectedAnimDuration = a.getInteger(
181                    R.styleable.lbBaseCardView_selectedAnimationDuration,
182                    getResources().getInteger(R.integer.lb_card_selected_animation_duration));
183
184            mActivatedAnimDuration =
185                    a.getInteger(R.styleable.lbBaseCardView_activatedAnimationDuration,
186                    getResources().getInteger(R.integer.lb_card_activated_animation_duration));
187        } finally {
188            a.recycle();
189        }
190
191        mDelaySelectedAnim = true;
192
193        mMainViewList = new ArrayList<View>();
194        mInfoViewList = new ArrayList<View>();
195        mExtraViewList = new ArrayList<View>();
196
197        mInfoOffset = 0.0f;
198        mInfoVisFraction = 0.0f;
199    }
200
201    /**
202     * Sets a flag indicating if the Selected animation (if the selected card
203     * type implements one) should run immediately after the card is selected,
204     * or if it should be delayed. The default behavior is to delay this
205     * animation. This is a one-shot override. If set to false, after the card
206     * is selected and the selected animation is triggered, this flag is
207     * automatically reset to true. This is useful when you want to change the
208     * default behavior, and have the selected animation run immediately. One
209     * such case could be when focus moves from one row to the other, when
210     * instead of delaying the selected animation until the user pauses on a
211     * card, it may be desirable to trigger the animation for that card
212     * immediately.
213     *
214     * @param delay True (default) if the selected animation should be delayed
215     *            after the card is selected, or false if the animation should
216     *            run immediately the next time the card is Selected.
217     */
218    public void setSelectedAnimationDelayed(boolean delay) {
219        mDelaySelectedAnim = delay;
220    }
221
222    /**
223     * Returns a boolean indicating if the selected animation will run
224     * immediately or be delayed the next time the card is Selected.
225     *
226     * @return true if this card is set to delay the selected animation the next
227     *         time it is selected, or false if the selected animation will run
228     *         immediately the next time the card is selected.
229     */
230    public boolean isSelectedAnimationDelayed() {
231        return mDelaySelectedAnim;
232    }
233
234    /**
235     * Sets the type of this Card.
236     *
237     * @param type The desired card type.
238     */
239    public void setCardType(int type) {
240        if (mCardType != type) {
241            if (type >= CARD_TYPE_MAIN_ONLY && type < CARD_TYPE_INVALID) {
242                // Valid card type
243                mCardType = type;
244            } else {
245                Log.e(TAG, "Invalid card type specified: " + type +
246                        ". Defaulting to type CARD_TYPE_MAIN_ONLY.");
247                mCardType = CARD_TYPE_MAIN_ONLY;
248            }
249            requestLayout();
250        }
251    }
252
253    /**
254     * Returns the type of this Card.
255     *
256     * @return The type of this card.
257     */
258    public int getCardType() {
259        return mCardType;
260    }
261
262    /**
263     * Sets the visibility of the info region of the card.
264     *
265     * @param visibility The region visibility to use for the info region. Must
266     *     be one of {@link #CARD_REGION_VISIBLE_ALWAYS},
267     *     {@link #CARD_REGION_VISIBLE_SELECTED}, or
268     *     {@link #CARD_REGION_VISIBLE_ACTIVATED}.
269     */
270    public void setInfoVisibility(int visibility) {
271        if (mInfoVisibility != visibility) {
272            mInfoVisibility = visibility;
273            if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED && isSelected()) {
274                mInfoVisFraction = 1.0f;
275            } else {
276                mInfoVisFraction = 0.0f;
277            }
278            requestLayout();
279        }
280    }
281
282    /**
283     * Returns the visibility of the info region of the card.
284     */
285    public int getInfoVisibility() {
286        return mInfoVisibility;
287    }
288
289    /**
290     * Sets the visibility of the extra region of the card.
291     *
292     * @param visibility The region visibility to use for the extra region. Must
293     *     be one of {@link #CARD_REGION_VISIBLE_ALWAYS},
294     *     {@link #CARD_REGION_VISIBLE_SELECTED}, or
295     *     {@link #CARD_REGION_VISIBLE_ACTIVATED}.
296     */
297    public void setExtraVisibility(int visibility) {
298        if (mExtraVisibility != visibility) {
299            mExtraVisibility = visibility;
300            requestLayout();
301        }
302    }
303
304    /**
305     * Returns the visibility of the extra region of the card.
306     */
307    public int getExtraVisibility() {
308        return mExtraVisibility;
309    }
310
311    /**
312     * Sets the Activated state of this Card. This can trigger changes in the
313     * card layout, resulting in views to become visible or hidden. A card is
314     * normally set to Activated state when its parent container (like a Row)
315     * receives focus, and then activates all of its children.
316     *
317     * @param activated True if the card is ACTIVE, or false if INACTIVE.
318     * @see #isActivated()
319     */
320    @Override
321    public void setActivated(boolean activated) {
322        if (activated != isActivated()) {
323            super.setActivated(activated);
324            applyActiveState(isActivated());
325        }
326    }
327
328    /**
329     * Sets the Selected state of this Card. This can trigger changes in the
330     * card layout, resulting in views to become visible or hidden. A card is
331     * normally set to Selected state when it receives input focus.
332     *
333     * @param selected True if the card is Selected, or false otherwise.
334     * @see #isSelected()
335     */
336    @Override
337    public void setSelected(boolean selected) {
338        if (selected != isSelected()) {
339            super.setSelected(selected);
340            applySelectedState(isSelected());
341        }
342    }
343
344    @Override
345    public boolean shouldDelayChildPressedState() {
346        return false;
347    }
348
349    @Override
350    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
351        mMeasuredWidth = 0;
352        mMeasuredHeight = 0;
353        int state = 0;
354        int mainHeight = 0;
355        int infoHeight = 0;
356        int extraHeight = 0;
357
358        findChildrenViews();
359
360        final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
361        // MAIN is always present
362        for (int i = 0; i < mMainViewList.size(); i++) {
363            View mainView = mMainViewList.get(i);
364            if (mainView.getVisibility() != View.GONE) {
365                measureChild(mainView, unspecifiedSpec, unspecifiedSpec);
366                mMeasuredWidth = Math.max(mMeasuredWidth, mainView.getMeasuredWidth());
367                mainHeight += mainView.getMeasuredHeight();
368                state = View.combineMeasuredStates(state, mainView.getMeasuredState());
369            }
370        }
371        setPivotX(mMeasuredWidth / 2);
372        setPivotY(mainHeight / 2);
373
374
375        // The MAIN area determines the card width
376        int cardWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
377
378        if (hasInfoRegion()) {
379            for (int i = 0; i < mInfoViewList.size(); i++) {
380                View infoView = mInfoViewList.get(i);
381                if (infoView.getVisibility() != View.GONE) {
382                    measureChild(infoView, cardWidthMeasureSpec, unspecifiedSpec);
383                    if (mCardType != CARD_TYPE_INFO_OVER) {
384                        infoHeight += infoView.getMeasuredHeight();
385                    }
386                    state = View.combineMeasuredStates(state, infoView.getMeasuredState());
387                }
388            }
389
390            if (hasExtraRegion()) {
391                for (int i = 0; i < mExtraViewList.size(); i++) {
392                    View extraView = mExtraViewList.get(i);
393                    if (extraView.getVisibility() != View.GONE) {
394                        measureChild(extraView, cardWidthMeasureSpec, unspecifiedSpec);
395                        extraHeight += extraView.getMeasuredHeight();
396                        state = View.combineMeasuredStates(state, extraView.getMeasuredState());
397                    }
398                }
399            }
400        }
401
402        boolean infoAnimating = hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED;
403        mMeasuredHeight = (int) (mainHeight +
404                (infoAnimating ? (infoHeight * mInfoVisFraction) : infoHeight)
405                + extraHeight - (infoAnimating ? 0 : mInfoOffset));
406
407        // Report our final dimensions.
408        setMeasuredDimension(View.resolveSizeAndState(mMeasuredWidth + getPaddingLeft() +
409                getPaddingRight(), widthMeasureSpec, state),
410                View.resolveSizeAndState(mMeasuredHeight + getPaddingTop() + getPaddingBottom(),
411                        heightMeasureSpec, state << View.MEASURED_HEIGHT_STATE_SHIFT));
412    }
413
414    @Override
415    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
416        float currBottom = getPaddingTop();
417
418        // MAIN is always present
419        for (int i = 0; i < mMainViewList.size(); i++) {
420            View mainView = mMainViewList.get(i);
421            if (mainView.getVisibility() != View.GONE) {
422                mainView.layout(getPaddingLeft(),
423                        (int) currBottom,
424                                mMeasuredWidth + getPaddingLeft(),
425                        (int) (currBottom + mainView.getMeasuredHeight()));
426                currBottom += mainView.getMeasuredHeight();
427            }
428        }
429
430        if (hasInfoRegion()) {
431            float infoHeight = 0f;
432            for (int i = 0; i < mInfoViewList.size(); i++) {
433                infoHeight += mInfoViewList.get(i).getMeasuredHeight();
434            }
435
436            if (mCardType == CARD_TYPE_INFO_OVER) {
437                // retract currBottom to overlap the info views on top of main
438                currBottom -= infoHeight;
439                if (currBottom < 0) {
440                    currBottom = 0;
441                }
442            } else if (mCardType == CARD_TYPE_INFO_UNDER) {
443                if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
444                    infoHeight = infoHeight * mInfoVisFraction;
445                }
446            } else {
447                currBottom -= mInfoOffset;
448            }
449
450            for (int i = 0; i < mInfoViewList.size(); i++) {
451                View infoView = mInfoViewList.get(i);
452                if (infoView.getVisibility() != View.GONE) {
453                    int viewHeight = infoView.getMeasuredHeight();
454                    if (viewHeight > infoHeight) {
455                        viewHeight = (int) infoHeight;
456                    }
457                    infoView.layout(getPaddingLeft(),
458                            (int) currBottom,
459                                    mMeasuredWidth + getPaddingLeft(),
460                            (int) (currBottom + viewHeight));
461                    currBottom += viewHeight;
462                    infoHeight -= viewHeight;
463                    if (infoHeight <= 0) {
464                        break;
465                    }
466                }
467            }
468
469            if (hasExtraRegion()) {
470                for (int i = 0; i < mExtraViewList.size(); i++) {
471                    View extraView = mExtraViewList.get(i);
472                    if (extraView.getVisibility() != View.GONE) {
473                        extraView.layout(getPaddingLeft(),
474                                (int) currBottom,
475                                        mMeasuredWidth + getPaddingLeft(),
476                                (int) (currBottom + extraView.getMeasuredHeight()));
477                        currBottom += extraView.getMeasuredHeight();
478                    }
479                }
480            }
481        }
482        // Force update drawable bounds.
483        onSizeChanged(0, 0, right - left, bottom - top);
484    }
485
486    @Override
487    protected void onDetachedFromWindow() {
488        super.onDetachedFromWindow();
489        removeCallbacks(mAnimationTrigger);
490        cancelAnimations();
491        mInfoOffset = 0.0f;
492        mInfoVisFraction = 0.0f;
493    }
494
495    private boolean hasInfoRegion() {
496        return mCardType != CARD_TYPE_MAIN_ONLY;
497    }
498
499    private boolean hasExtraRegion() {
500        return mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA;
501    }
502
503    private boolean isRegionVisible(int regionVisibility) {
504        switch (regionVisibility) {
505            case CARD_REGION_VISIBLE_ALWAYS:
506                return true;
507            case CARD_REGION_VISIBLE_ACTIVATED:
508                return isActivated();
509            case CARD_REGION_VISIBLE_SELECTED:
510                return isActivated() && isSelected();
511            default:
512                if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility);
513                return false;
514        }
515    }
516
517    private void findChildrenViews() {
518        mMainViewList.clear();
519        mInfoViewList.clear();
520        mExtraViewList.clear();
521
522        final int count = getChildCount();
523
524        boolean infoVisible = isRegionVisible(mInfoVisibility);
525        boolean extraVisible = hasExtraRegion() && mInfoOffset > 0f;
526
527        if (mCardType == CARD_TYPE_INFO_UNDER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
528            infoVisible = infoVisible && mInfoVisFraction > 0f;
529        }
530
531        for (int i = 0; i < count; i++) {
532            final View child = getChildAt(i);
533
534            if (child == null) {
535                continue;
536            }
537
538            BaseCardView.LayoutParams lp = (BaseCardView.LayoutParams) child
539                    .getLayoutParams();
540            if (lp.viewType == LayoutParams.VIEW_TYPE_INFO) {
541                mInfoViewList.add(child);
542                child.setVisibility(infoVisible ? View.VISIBLE : View.GONE);
543            } else if (lp.viewType == LayoutParams.VIEW_TYPE_EXTRA) {
544                mExtraViewList.add(child);
545                child.setVisibility(extraVisible ? View.VISIBLE : View.GONE);
546            } else {
547                // Default to MAIN
548                mMainViewList.add(child);
549                child.setVisibility(View.VISIBLE);
550            }
551        }
552
553    }
554
555    @Override
556    protected int[] onCreateDrawableState(int extraSpace) {
557        // filter out focus states,  since leanback does not fade foreground on focus.
558        final int[] s = super.onCreateDrawableState(extraSpace);
559        final int N = s.length;
560        boolean pressed = false;
561        boolean enabled = false;
562        for (int i = 0; i < N; i++) {
563            if (s[i] == android.R.attr.state_pressed) {
564                pressed = true;
565            }
566            if (s[i] == android.R.attr.state_enabled) {
567                enabled = true;
568            }
569        }
570        if (pressed && enabled) {
571            return View.PRESSED_ENABLED_STATE_SET;
572        } else if (pressed) {
573            return LB_PRESSED_STATE_SET;
574        } else if (enabled) {
575            return View.ENABLED_STATE_SET;
576        } else {
577            return View.EMPTY_STATE_SET;
578        }
579    }
580
581    private void applyActiveState(boolean active) {
582        if (hasInfoRegion() && mInfoVisibility <= CARD_REGION_VISIBLE_ACTIVATED) {
583            setInfoViewVisibility(active);
584        }
585        if (hasExtraRegion() && mExtraVisibility <= CARD_REGION_VISIBLE_ACTIVATED) {
586            //setExtraVisibility(active);
587        }
588    }
589
590    private void setInfoViewVisibility(boolean visible) {
591        if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
592            // Active state changes for card type
593            // CARD_TYPE_INFO_UNDER_WITH_EXTRA
594            if (visible) {
595                for (int i = 0; i < mInfoViewList.size(); i++) {
596                    mInfoViewList.get(i).setVisibility(View.VISIBLE);
597                }
598            } else {
599                for (int i = 0; i < mInfoViewList.size(); i++) {
600                    mInfoViewList.get(i).setVisibility(View.GONE);
601                }
602                for (int i = 0; i < mExtraViewList.size(); i++) {
603                    mExtraViewList.get(i).setVisibility(View.GONE);
604                }
605                mInfoOffset = 0.0f;
606            }
607        } else if (mCardType == CARD_TYPE_INFO_UNDER) {
608            // Active state changes for card type CARD_TYPE_INFO_UNDER
609            if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
610                animateInfoHeight(visible);
611            } else {
612                for (int i = 0; i < mInfoViewList.size(); i++) {
613                    mInfoViewList.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
614                }
615            }
616        } else if (mCardType == CARD_TYPE_INFO_OVER) {
617            // Active state changes for card type CARD_TYPE_INFO_OVER
618            animateInfoAlpha(visible);
619        }
620    }
621
622    private void applySelectedState(boolean focused) {
623        removeCallbacks(mAnimationTrigger);
624
625        if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
626            // Focus changes for card type CARD_TYPE_INFO_UNDER_WITH_EXTRA
627            if (focused) {
628                if (!mDelaySelectedAnim) {
629                    post(mAnimationTrigger);
630                    mDelaySelectedAnim = true;
631                } else {
632                    postDelayed(mAnimationTrigger, mSelectedAnimationDelay);
633                }
634            } else {
635                animateInfoOffset(false);
636            }
637        } else if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
638            setInfoViewVisibility(focused);
639        }
640    }
641
642    private void cancelAnimations() {
643        if (mAnim != null) {
644            mAnim.cancel();
645            mAnim = null;
646        }
647    }
648
649    // This animation changes the Y offset of the info and extra views,
650    // so that they animate UP to make the extra info area visible when a
651    // card is selected.
652    void animateInfoOffset(boolean shown) {
653        cancelAnimations();
654
655        int extraHeight = 0;
656        if (shown) {
657            int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
658            int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
659
660            for (int i = 0; i < mExtraViewList.size(); i++) {
661                View extraView = mExtraViewList.get(i);
662                extraView.setVisibility(View.VISIBLE);
663                extraView.measure(widthSpec, heightSpec);
664                extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight());
665            }
666        }
667
668        mAnim = new InfoOffsetAnimation(mInfoOffset, shown ? extraHeight : 0);
669        mAnim.setDuration(mSelectedAnimDuration);
670        mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
671        mAnim.setAnimationListener(new Animation.AnimationListener() {
672            @Override
673            public void onAnimationStart(Animation animation) {
674            }
675
676            @Override
677            public void onAnimationEnd(Animation animation) {
678                if (mInfoOffset == 0f) {
679                    for (int i = 0; i < mExtraViewList.size(); i++) {
680                        mExtraViewList.get(i).setVisibility(View.GONE);
681                    }
682                }
683            }
684
685                @Override
686            public void onAnimationRepeat(Animation animation) {
687            }
688
689        });
690        startAnimation(mAnim);
691    }
692
693    // This animation changes the visible height of the info views,
694    // so that they animate in and out of view.
695    private void animateInfoHeight(boolean shown) {
696        cancelAnimations();
697
698        int extraHeight = 0;
699        if (shown) {
700            int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
701            int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
702
703            for (int i = 0; i < mExtraViewList.size(); i++) {
704                View extraView = mExtraViewList.get(i);
705                extraView.setVisibility(View.VISIBLE);
706                extraView.measure(widthSpec, heightSpec);
707                extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight());
708            }
709        }
710
711        mAnim = new InfoHeightAnimation(mInfoVisFraction, shown ? 1.0f : 0f);
712        mAnim.setDuration(mSelectedAnimDuration);
713        mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
714        mAnim.setAnimationListener(new Animation.AnimationListener() {
715                @Override
716            public void onAnimationStart(Animation animation) {
717            }
718
719                @Override
720            public void onAnimationEnd(Animation animation) {
721                if (mInfoOffset == 0f) {
722                    for (int i = 0; i < mExtraViewList.size(); i++) {
723                        mExtraViewList.get(i).setVisibility(View.GONE);
724                    }
725                }
726            }
727
728            @Override
729            public void onAnimationRepeat(Animation animation) {
730            }
731
732        });
733        startAnimation(mAnim);
734    }
735
736    // This animation changes the alpha of the info views, so they animate in
737    // and out. It's meant to be used when the info views are overlaid on top of
738    // the main view area. It gets triggered by a change in the Active state of
739    // the card.
740    private void animateInfoAlpha(boolean shown) {
741        cancelAnimations();
742
743        if (shown) {
744            for (int i = 0; i < mInfoViewList.size(); i++) {
745                mInfoViewList.get(i).setVisibility(View.VISIBLE);
746            }
747        }
748
749        mAnim = new InfoAlphaAnimation(mInfoAlpha, shown ? 1.0f : 0.0f);
750        mAnim.setDuration(mActivatedAnimDuration);
751        mAnim.setInterpolator(new DecelerateInterpolator());
752        mAnim.setAnimationListener(new Animation.AnimationListener() {
753            @Override
754            public void onAnimationStart(Animation animation) {
755            }
756
757            @Override
758            public void onAnimationEnd(Animation animation) {
759                if (mInfoAlpha == 0.0) {
760                    for (int i = 0; i < mInfoViewList.size(); i++) {
761                        mInfoViewList.get(i).setVisibility(View.GONE);
762                    }
763                }
764            }
765
766            @Override
767            public void onAnimationRepeat(Animation animation) {
768            }
769
770        });
771        startAnimation(mAnim);
772    }
773
774    @Override
775    public LayoutParams generateLayoutParams(AttributeSet attrs) {
776        return new BaseCardView.LayoutParams(getContext(), attrs);
777    }
778
779    @Override
780    protected LayoutParams generateDefaultLayoutParams() {
781        return new BaseCardView.LayoutParams(
782                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
783    }
784
785    @Override
786    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
787        if (lp instanceof LayoutParams) {
788            return new LayoutParams((LayoutParams) lp);
789        } else {
790            return new LayoutParams(lp);
791        }
792    }
793
794    @Override
795    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
796        return p instanceof BaseCardView.LayoutParams;
797    }
798
799    /**
800     * Per-child layout information associated with BaseCardView.
801     */
802    public static class LayoutParams extends FrameLayout.LayoutParams {
803        public static final int VIEW_TYPE_MAIN = 0;
804        public static final int VIEW_TYPE_INFO = 1;
805        public static final int VIEW_TYPE_EXTRA = 2;
806
807        /**
808         * Card component type for the view associated with these LayoutParams.
809         */
810        @ViewDebug.ExportedProperty(category = "layout", mapping = {
811                @ViewDebug.IntToString(from = VIEW_TYPE_MAIN, to = "MAIN"),
812                @ViewDebug.IntToString(from = VIEW_TYPE_INFO, to = "INFO"),
813                @ViewDebug.IntToString(from = VIEW_TYPE_EXTRA, to = "EXTRA")
814        })
815        public int viewType = VIEW_TYPE_MAIN;
816
817        /**
818         * {@inheritDoc}
819         */
820        public LayoutParams(Context c, AttributeSet attrs) {
821            super(c, attrs);
822            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView_Layout);
823
824            viewType = a.getInt(
825                    R.styleable.lbBaseCardView_Layout_layout_viewType, VIEW_TYPE_MAIN);
826
827            a.recycle();
828        }
829
830        /**
831         * {@inheritDoc}
832         */
833        public LayoutParams(int width, int height) {
834            super(width, height);
835        }
836
837        /**
838         * {@inheritDoc}
839         */
840        public LayoutParams(ViewGroup.LayoutParams p) {
841            super(p);
842        }
843
844        /**
845         * Copy constructor. Clones the width, height, and View Type of the
846         * source.
847         *
848         * @param source The layout params to copy from.
849         */
850        public LayoutParams(LayoutParams source) {
851            super(source);
852
853            this.viewType = source.viewType;
854        }
855    }
856
857    // Helper animation class used in the animation of the info and extra
858    // fields vertically within the card
859    private class InfoOffsetAnimation extends Animation {
860        private float mStartValue;
861        private float mDelta;
862
863        public InfoOffsetAnimation(float start, float end) {
864            mStartValue = start;
865            mDelta = end - start;
866        }
867
868        @Override
869        protected void applyTransformation(float interpolatedTime, Transformation t) {
870            mInfoOffset = mStartValue + (interpolatedTime * mDelta);
871            requestLayout();
872        }
873    }
874
875    // Helper animation class used in the animation of the visible height
876    // for the info fields.
877    private class InfoHeightAnimation extends Animation {
878        private float mStartValue;
879        private float mDelta;
880
881        public InfoHeightAnimation(float start, float end) {
882            mStartValue = start;
883            mDelta = end - start;
884        }
885
886        @Override
887        protected void applyTransformation(float interpolatedTime, Transformation t) {
888            mInfoVisFraction = mStartValue + (interpolatedTime * mDelta);
889            requestLayout();
890        }
891    }
892
893    // Helper animation class used to animate the alpha for the info views
894    // when they are fading in or out of view.
895    private class InfoAlphaAnimation extends Animation {
896        private float mStartValue;
897        private float mDelta;
898
899        public InfoAlphaAnimation(float start, float end) {
900            mStartValue = start;
901            mDelta = end - start;
902        }
903
904        @Override
905        protected void applyTransformation(float interpolatedTime, Transformation t) {
906            mInfoAlpha = mStartValue + (interpolatedTime * mDelta);
907            for (int i = 0; i < mInfoViewList.size(); i++) {
908                mInfoViewList.get(i).setAlpha(mInfoAlpha);
909            }
910        }
911    }
912
913    @Override
914    public String toString() {
915        if (DEBUG) {
916            StringBuilder sb = new StringBuilder();
917            sb.append(this.getClass().getSimpleName()).append(" : ");
918            sb.append("cardType=");
919            switch(mCardType) {
920                case CARD_TYPE_MAIN_ONLY:
921                    sb.append("MAIN_ONLY");
922                    break;
923                case CARD_TYPE_INFO_OVER:
924                    sb.append("INFO_OVER");
925                    break;
926                case CARD_TYPE_INFO_UNDER:
927                    sb.append("INFO_UNDER");
928                    break;
929                case CARD_TYPE_INFO_UNDER_WITH_EXTRA:
930                    sb.append("INFO_UNDER_WITH_EXTRA");
931                    break;
932                default:
933                    sb.append("INVALID");
934                    break;
935            }
936            sb.append(" : ");
937            sb.append(mMainViewList.size()).append(" main views, ");
938            sb.append(mInfoViewList.size()).append(" info views, ");
939            sb.append(mExtraViewList.size()).append(" extra views : ");
940            sb.append("infoVisibility=").append(mInfoVisibility).append(" ");
941            sb.append("extraVisibility=").append(mExtraVisibility).append(" ");
942            sb.append("isActivated=").append(isActivated());
943            sb.append(" : ");
944            sb.append("isSelected=").append(isSelected());
945            return sb.toString();
946        } else {
947            return super.toString();
948        }
949    }
950}
951