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