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