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