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