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