public class

BaseCardView

extends FrameLayout

 java.lang.Object

↳FrameLayout

↳androidx.leanback.widget.BaseCardView

Subclasses:

ImageCardView

Gradle dependencies

compile group: 'androidx.leanback', name: 'leanback', version: '1.2.0-alpha02'

  • groupId: androidx.leanback
  • artifactId: leanback
  • version: 1.2.0-alpha02

Artifact androidx.leanback:leanback:1.2.0-alpha02 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.leanback:leanback com.android.support:leanback-v17

Androidx class mapping:

androidx.leanback.widget.BaseCardView android.support.v17.leanback.widget.BaseCardView

Overview

A card style layout that responds to certain state changes. It arranges its children in a vertical column, with different regions becoming visible at different times.

A BaseCardView will draw its children based on its type, the region visibilities of the child types, and the state of the widget. A child may be marked as belonging to one of three regions: main, info, or extra. The main region is always visible, while the info and extra regions can be set to display based on the activated or selected state of the View. The card states are set by calling setActivated and setSelected.

See BaseCardView.LayoutParams for layout attributes.

Summary

Fields
public static final intCARD_REGION_VISIBLE_ACTIVATED

Indicates that a card region is visible when the card is activated.

public static final intCARD_REGION_VISIBLE_ALWAYS

Indicates that a card region is always visible.

public static final intCARD_REGION_VISIBLE_SELECTED

Indicates that a card region is visible when the card is selected.

public static final intCARD_TYPE_INFO_OVER

A Card type with 2 layout areas: A main area which is always visible, and an info area that fades in over the main area when it is visible.

public static final intCARD_TYPE_INFO_UNDER

A Card type with 2 layout areas: A main area which is always visible, and an info area that appears below the main area.

public static final intCARD_TYPE_INFO_UNDER_WITH_EXTRA

A Card type with 3 layout areas: A main area which is always visible; an info area which will appear below the main area, and an extra area that only appears after a short delay.

public static final intCARD_TYPE_MAIN_ONLY

A simple card type with a single layout area.

Constructors
publicBaseCardView(Context context)

publicBaseCardView(Context context, AttributeSet attrs)

publicBaseCardView(Context context, AttributeSet attrs, int defStyleAttr)

Methods
protected booleancheckLayoutParams(ViewGroup.LayoutParams p)

protected BaseCardView.LayoutParamsgenerateDefaultLayoutParams()

public BaseCardView.LayoutParamsgenerateLayoutParams(AttributeSet attrs)

protected BaseCardView.LayoutParamsgenerateLayoutParams(ViewGroup.LayoutParams lp)

public intgetCardType()

Returns the type of this Card.

public intgetExtraVisibility()

Returns the visibility of the extra region of the card.

public intgetInfoVisibility()

Returns the visibility of the info region of the card.

public booleanisSelectedAnimationDelayed()

Returns a boolean indicating if the selected animation will run immediately or be delayed the next time the card is Selected.

protected int[]onCreateDrawableState(int extraSpace)

protected voidonDetachedFromWindow()

protected voidonLayout(boolean changed, int left, int top, int right, int bottom)

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public voidsetActivated(boolean activated)

Sets the Activated state of this Card.

public voidsetCardType(int type)

Sets the type of this Card.

public voidsetExtraVisibility(int visibility)

Sets the visibility of the extra region of the card.

public voidsetInfoVisibility(int visibility)

Sets the visibility of the info region of the card.

public voidsetSelected(boolean selected)

Sets the Selected state of this Card.

public voidsetSelectedAnimationDelayed(boolean delay)

Sets a flag indicating if the Selected animation (if the selected card type implements one) should run immediately after the card is selected, or if it should be delayed.

public booleanshouldDelayChildPressedState()

public java.lang.StringtoString()

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait

Fields

public static final int CARD_TYPE_MAIN_ONLY

A simple card type with a single layout area. This card type does not change its layout or size as it transitions between Activated/Not-Activated or Selected/Unselected states.

See also: BaseCardView.getCardType()

public static final int CARD_TYPE_INFO_OVER

A Card type with 2 layout areas: A main area which is always visible, and an info area that fades in over the main area when it is visible. The card height will not change.

See also: BaseCardView.getCardType()

public static final int CARD_TYPE_INFO_UNDER

A Card type with 2 layout areas: A main area which is always visible, and an info area that appears below the main area. When the info area is visible the total card height will change.

See also: BaseCardView.getCardType()

public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA

A Card type with 3 layout areas: A main area which is always visible; an info area which will appear below the main area, and an extra area that only appears after a short delay. The info area appears below the main area, causing the total card height to change. The extra area animates in at the bottom of the card, shifting up the info view without affecting the card height.

See also: BaseCardView.getCardType()

public static final int CARD_REGION_VISIBLE_ALWAYS

Indicates that a card region is always visible.

public static final int CARD_REGION_VISIBLE_ACTIVATED

Indicates that a card region is visible when the card is activated.

public static final int CARD_REGION_VISIBLE_SELECTED

Indicates that a card region is visible when the card is selected.

Constructors

public BaseCardView(Context context)

public BaseCardView(Context context, AttributeSet attrs)

public BaseCardView(Context context, AttributeSet attrs, int defStyleAttr)

Methods

public void setSelectedAnimationDelayed(boolean delay)

Sets a flag indicating if the Selected animation (if the selected card type implements one) should run immediately after the card is selected, or if it should be delayed. The default behavior is to delay this animation. This is a one-shot override. If set to false, after the card is selected and the selected animation is triggered, this flag is automatically reset to true. This is useful when you want to change the default behavior, and have the selected animation run immediately. One such case could be when focus moves from one row to the other, when instead of delaying the selected animation until the user pauses on a card, it may be desirable to trigger the animation for that card immediately.

Parameters:

delay: True (default) if the selected animation should be delayed after the card is selected, or false if the animation should run immediately the next time the card is Selected.

public boolean isSelectedAnimationDelayed()

Returns a boolean indicating if the selected animation will run immediately or be delayed the next time the card is Selected.

Returns:

true if this card is set to delay the selected animation the next time it is selected, or false if the selected animation will run immediately the next time the card is selected.

public void setCardType(int type)

Sets the type of this Card.

Parameters:

type: The desired card type.

public int getCardType()

Returns the type of this Card.

Returns:

The type of this card.

public void setInfoVisibility(int visibility)

Sets the visibility of the info region of the card.

Parameters:

visibility: The region visibility to use for the info region. Must be one of BaseCardView.CARD_REGION_VISIBLE_ALWAYS, BaseCardView.CARD_REGION_VISIBLE_SELECTED, or BaseCardView.CARD_REGION_VISIBLE_ACTIVATED.

public int getInfoVisibility()

Returns the visibility of the info region of the card.

public void setExtraVisibility(int visibility)

Deprecated: Extra view's visibility is controlled by BaseCardView.setInfoVisibility(int)

Sets the visibility of the extra region of the card.

Parameters:

visibility: The region visibility to use for the extra region. Must be one of BaseCardView.CARD_REGION_VISIBLE_ALWAYS, BaseCardView.CARD_REGION_VISIBLE_SELECTED, or BaseCardView.CARD_REGION_VISIBLE_ACTIVATED.

public int getExtraVisibility()

Deprecated: Extra view's visibility is controlled by BaseCardView.getInfoVisibility()

Returns the visibility of the extra region of the card.

public void setActivated(boolean activated)

Sets the Activated state of this Card. This can trigger changes in the card layout, resulting in views to become visible or hidden. A card is normally set to Activated state when its parent container (like a Row) receives focus, and then activates all of its children.

Parameters:

activated: True if the card is ACTIVE, or false if INACTIVE.

See also: BaseCardView

public void setSelected(boolean selected)

Sets the Selected state of this Card. This can trigger changes in the card layout, resulting in views to become visible or hidden. A card is normally set to Selected state when it receives input focus.

Parameters:

selected: True if the card is Selected, or false otherwise.

See also: BaseCardView

public boolean shouldDelayChildPressedState()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

protected void onLayout(boolean changed, int left, int top, int right, int bottom)

protected void onDetachedFromWindow()

protected int[] onCreateDrawableState(int extraSpace)

public BaseCardView.LayoutParams generateLayoutParams(AttributeSet attrs)

protected BaseCardView.LayoutParams generateDefaultLayoutParams()

protected BaseCardView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp)

protected boolean checkLayoutParams(ViewGroup.LayoutParams p)

public java.lang.String toString()

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.leanback.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.FrameLayout;

import androidx.annotation.VisibleForTesting;
import androidx.leanback.R;

import java.util.ArrayList;

/**
 * A card style layout that responds to certain state changes. It arranges its
 * children in a vertical column, with different regions becoming visible at
 * different times.
 *
 * <p>
 * A BaseCardView will draw its children based on its type, the region
 * visibilities of the child types, and the state of the widget. A child may be
 * marked as belonging to one of three regions: main, info, or extra. The main
 * region is always visible, while the info and extra regions can be set to
 * display based on the activated or selected state of the View. The card states
 * are set by calling {@link #setActivated(boolean) setActivated} and
 * {@link #setSelected(boolean) setSelected}.
 * <p>
 * See {@link BaseCardView.LayoutParams} for layout attributes.
 * </p>
 */
public class BaseCardView extends FrameLayout {
    private static final String TAG = "BaseCardView";
    private static final boolean DEBUG = false;

    /**
     * A simple card type with a single layout area. This card type does not
     * change its layout or size as it transitions between
     * Activated/Not-Activated or Selected/Unselected states.
     *
     * @see #getCardType()
     */
    public static final int CARD_TYPE_MAIN_ONLY = 0;

    /**
     * A Card type with 2 layout areas: A main area which is always visible, and
     * an info area that fades in over the main area when it is visible.
     * The card height will not change.
     *
     * @see #getCardType()
     */
    public static final int CARD_TYPE_INFO_OVER = 1;

    /**
     * A Card type with 2 layout areas: A main area which is always visible, and
     * an info area that appears below the main area. When the info area is visible
     * the total card height will change.
     *
     * @see #getCardType()
     */
    public static final int CARD_TYPE_INFO_UNDER = 2;

    /**
     * A Card type with 3 layout areas: A main area which is always visible; an
     * info area which will appear below the main area, and an extra area that
     * only appears after a short delay. The info area appears below the main
     * area, causing the total card height to change. The extra area animates in
     * at the bottom of the card, shifting up the info view without affecting
     * the card height.
     *
     * @see #getCardType()
     */
    public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3;

    /**
     * Indicates that a card region is always visible.
     */
    public static final int CARD_REGION_VISIBLE_ALWAYS = 0;

    /**
     * Indicates that a card region is visible when the card is activated.
     */
    public static final int CARD_REGION_VISIBLE_ACTIVATED = 1;

    /**
     * Indicates that a card region is visible when the card is selected.
     */
    public static final int CARD_REGION_VISIBLE_SELECTED = 2;

    private static final int CARD_TYPE_INVALID = 4;

    private int mCardType;
    private int mInfoVisibility;
    private int mExtraVisibility;

    private ArrayList<View> mMainViewList;
    ArrayList<View> mInfoViewList;
    ArrayList<View> mExtraViewList;

    private int mMeasuredWidth;
    private int mMeasuredHeight;
    private boolean mDelaySelectedAnim;
    private int mSelectedAnimationDelay;
    private final int mActivatedAnimDuration;
    private final int mSelectedAnimDuration;

    /**
     * Distance of top of info view to bottom of MainView, it will shift up when extra view appears.
     */
    float mInfoOffset;
    float mInfoVisFraction;
    float mInfoAlpha;
    private Animation mAnim;

    private final static int[] LB_PRESSED_STATE_SET = new int[]{
        android.R.attr.state_pressed};

    private final Runnable mAnimationTrigger = new Runnable() {
        @Override
        public void run() {
            animateInfoOffset(true);
        }
    };

    public BaseCardView(Context context) {
        this(context, null);
    }

    public BaseCardView(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.baseCardViewStyle);
    }

    @SuppressLint("CustomViewStyleable")
    public BaseCardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView,
                defStyleAttr, 0);

        try {
            mCardType = a.getInteger(R.styleable.lbBaseCardView_cardType, CARD_TYPE_MAIN_ONLY);
            Drawable cardForeground = a.getDrawable(R.styleable.lbBaseCardView_cardForeground);
            if (cardForeground != null) {
                setForeground(cardForeground);
            }
            Drawable cardBackground = a.getDrawable(R.styleable.lbBaseCardView_cardBackground);
            if (cardBackground != null) {
                setBackground(cardBackground);
            }
            mInfoVisibility = a.getInteger(R.styleable.lbBaseCardView_infoVisibility,
                    CARD_REGION_VISIBLE_ACTIVATED);
            mExtraVisibility = a.getInteger(R.styleable.lbBaseCardView_extraVisibility,
                    CARD_REGION_VISIBLE_SELECTED);
            // Extra region should never show before info region.
            if (mExtraVisibility < mInfoVisibility) {
                mExtraVisibility = mInfoVisibility;
            }

            mSelectedAnimationDelay = a.getInteger(
                    R.styleable.lbBaseCardView_selectedAnimationDelay,
                    getResources().getInteger(R.integer.lb_card_selected_animation_delay));

            mSelectedAnimDuration = a.getInteger(
                    R.styleable.lbBaseCardView_selectedAnimationDuration,
                    getResources().getInteger(R.integer.lb_card_selected_animation_duration));

            mActivatedAnimDuration =
                    a.getInteger(R.styleable.lbBaseCardView_activatedAnimationDuration,
                    getResources().getInteger(R.integer.lb_card_activated_animation_duration));
        } finally {
            a.recycle();
        }

        mDelaySelectedAnim = true;

        mMainViewList = new ArrayList<View>();
        mInfoViewList = new ArrayList<View>();
        mExtraViewList = new ArrayList<View>();

        mInfoOffset = 0.0f;
        mInfoVisFraction = getFinalInfoVisFraction();
        mInfoAlpha = getFinalInfoAlpha();
    }

    /**
     * Sets a flag indicating if the Selected animation (if the selected card
     * type implements one) should run immediately after the card is selected,
     * or if it should be delayed. The default behavior is to delay this
     * animation. This is a one-shot override. If set to false, after the card
     * is selected and the selected animation is triggered, this flag is
     * automatically reset to true. This is useful when you want to change the
     * default behavior, and have the selected animation run immediately. One
     * such case could be when focus moves from one row to the other, when
     * instead of delaying the selected animation until the user pauses on a
     * card, it may be desirable to trigger the animation for that card
     * immediately.
     *
     * @param delay True (default) if the selected animation should be delayed
     *            after the card is selected, or false if the animation should
     *            run immediately the next time the card is Selected.
     */
    public void setSelectedAnimationDelayed(boolean delay) {
        mDelaySelectedAnim = delay;
    }

    /**
     * Returns a boolean indicating if the selected animation will run
     * immediately or be delayed the next time the card is Selected.
     *
     * @return true if this card is set to delay the selected animation the next
     *         time it is selected, or false if the selected animation will run
     *         immediately the next time the card is selected.
     */
    public boolean isSelectedAnimationDelayed() {
        return mDelaySelectedAnim;
    }

    /**
     * Sets the type of this Card.
     *
     * @param type The desired card type.
     */
    public void setCardType(int type) {
        if (mCardType != type) {
            if (type >= CARD_TYPE_MAIN_ONLY && type < CARD_TYPE_INVALID) {
                // Valid card type
                mCardType = type;
            } else {
                Log.e(TAG, "Invalid card type specified: " + type
                        + ". Defaulting to type CARD_TYPE_MAIN_ONLY.");
                mCardType = CARD_TYPE_MAIN_ONLY;
            }
            requestLayout();
        }
    }

    /**
     * Returns the type of this Card.
     *
     * @return The type of this card.
     */
    public int getCardType() {
        return mCardType;
    }

    /**
     * Sets the visibility of the info region of the card.
     *
     * @param visibility The region visibility to use for the info region. Must
     *     be one of {@link #CARD_REGION_VISIBLE_ALWAYS},
     *     {@link #CARD_REGION_VISIBLE_SELECTED}, or
     *     {@link #CARD_REGION_VISIBLE_ACTIVATED}.
     */
    public void setInfoVisibility(int visibility) {
        if (mInfoVisibility != visibility) {
            cancelAnimations();
            mInfoVisibility = visibility;
            mInfoVisFraction = getFinalInfoVisFraction();
            requestLayout();
            float newInfoAlpha = getFinalInfoAlpha();
            if (newInfoAlpha != mInfoAlpha) {
                mInfoAlpha = newInfoAlpha;
                for (int i = 0; i < mInfoViewList.size(); i++) {
                    mInfoViewList.get(i).setAlpha(mInfoAlpha);
                }
            }
        }
    }

    final float getFinalInfoVisFraction() {
        return mCardType == CARD_TYPE_INFO_UNDER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED
                && !isSelected() ? 0.0f : 1.0f;
    }

    final float getFinalInfoAlpha() {
        return mCardType == CARD_TYPE_INFO_OVER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED
                && !isSelected() ? 0.0f : 1.0f;
    }

    /**
     * Returns the visibility of the info region of the card.
     */
    public int getInfoVisibility() {
        return mInfoVisibility;
    }

    /**
     * Sets the visibility of the extra region of the card.
     *
     * @param visibility The region visibility to use for the extra region. Must
     *     be one of {@link #CARD_REGION_VISIBLE_ALWAYS},
     *     {@link #CARD_REGION_VISIBLE_SELECTED}, or
     *     {@link #CARD_REGION_VISIBLE_ACTIVATED}.
     * @deprecated Extra view's visibility is controlled by {@link #setInfoVisibility(int)}
     */
    @Deprecated
    public void setExtraVisibility(int visibility) {
        if (mExtraVisibility != visibility) {
            mExtraVisibility = visibility;
        }
    }

    /**
     * Returns the visibility of the extra region of the card.
     * @deprecated Extra view's visibility is controlled by {@link #getInfoVisibility()}
     */
    @Deprecated
    public int getExtraVisibility() {
        return mExtraVisibility;
    }

    /**
     * Sets the Activated state of this Card. This can trigger changes in the
     * card layout, resulting in views to become visible or hidden. A card is
     * normally set to Activated state when its parent container (like a Row)
     * receives focus, and then activates all of its children.
     *
     * @param activated True if the card is ACTIVE, or false if INACTIVE.
     * @see #isActivated()
     */
    @Override
    public void setActivated(boolean activated) {
        if (activated != isActivated()) {
            super.setActivated(activated);
            applyActiveState();
        }
    }

    /**
     * Sets the Selected state of this Card. This can trigger changes in the
     * card layout, resulting in views to become visible or hidden. A card is
     * normally set to Selected state when it receives input focus.
     *
     * @param selected True if the card is Selected, or false otherwise.
     * @see #isSelected()
     */
    @Override
    public void setSelected(boolean selected) {
        if (selected != isSelected()) {
            super.setSelected(selected);
            applySelectedState(isSelected());
        }
    }

    @Override
    public boolean shouldDelayChildPressedState() {
        return false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mMeasuredWidth = 0;
        mMeasuredHeight = 0;
        int state = 0;
        int mainHeight = 0;
        int infoHeight = 0;
        int extraHeight = 0;

        findChildrenViews();

        final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        // MAIN is always present
        for (int i = 0; i < mMainViewList.size(); i++) {
            View mainView = mMainViewList.get(i);
            if (mainView.getVisibility() != View.GONE) {
                measureChild(mainView, unspecifiedSpec, unspecifiedSpec);
                mMeasuredWidth = Math.max(mMeasuredWidth, mainView.getMeasuredWidth());
                mainHeight += mainView.getMeasuredHeight();
                state = View.combineMeasuredStates(state, mainView.getMeasuredState());
            }
        }
        setPivotX(mMeasuredWidth / 2);
        setPivotY(mainHeight / 2);


        // The MAIN area determines the card width
        int cardWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);

        if (hasInfoRegion()) {
            for (int i = 0; i < mInfoViewList.size(); i++) {
                View infoView = mInfoViewList.get(i);
                if (infoView.getVisibility() != View.GONE) {
                    measureChild(infoView, cardWidthMeasureSpec, unspecifiedSpec);
                    if (mCardType != CARD_TYPE_INFO_OVER) {
                        infoHeight += infoView.getMeasuredHeight();
                    }
                    state = View.combineMeasuredStates(state, infoView.getMeasuredState());
                }
            }

            if (hasExtraRegion()) {
                for (int i = 0; i < mExtraViewList.size(); i++) {
                    View extraView = mExtraViewList.get(i);
                    if (extraView.getVisibility() != View.GONE) {
                        measureChild(extraView, cardWidthMeasureSpec, unspecifiedSpec);
                        extraHeight += extraView.getMeasuredHeight();
                        state = View.combineMeasuredStates(state, extraView.getMeasuredState());
                    }
                }
            }
        }

        boolean infoAnimating = hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED;
        mMeasuredHeight = (int) (mainHeight
                + (infoAnimating ? (infoHeight * mInfoVisFraction) : infoHeight)
                + extraHeight - (infoAnimating ? 0 : mInfoOffset));

        // Report our final dimensions.
        setMeasuredDimension(View.resolveSizeAndState(mMeasuredWidth + getPaddingLeft()
                + getPaddingRight(), widthMeasureSpec, state),
                View.resolveSizeAndState(mMeasuredHeight + getPaddingTop() + getPaddingBottom(),
                        heightMeasureSpec, state << View.MEASURED_HEIGHT_STATE_SHIFT));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        float currBottom = getPaddingTop();

        // MAIN is always present
        for (int i = 0; i < mMainViewList.size(); i++) {
            View mainView = mMainViewList.get(i);
            if (mainView.getVisibility() != View.GONE) {
                mainView.layout(getPaddingLeft(),
                        (int) currBottom,
                                mMeasuredWidth + getPaddingLeft(),
                        (int) (currBottom + mainView.getMeasuredHeight()));
                currBottom += mainView.getMeasuredHeight();
            }
        }

        if (hasInfoRegion()) {
            float infoHeight = 0f;
            for (int i = 0; i < mInfoViewList.size(); i++) {
                infoHeight += mInfoViewList.get(i).getMeasuredHeight();
            }

            if (mCardType == CARD_TYPE_INFO_OVER) {
                // retract currBottom to overlap the info views on top of main
                currBottom -= infoHeight;
                if (currBottom < 0) {
                    currBottom = 0;
                }
            } else if (mCardType == CARD_TYPE_INFO_UNDER) {
                if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
                    infoHeight = infoHeight * mInfoVisFraction;
                }
            } else {
                currBottom -= mInfoOffset;
            }

            for (int i = 0; i < mInfoViewList.size(); i++) {
                View infoView = mInfoViewList.get(i);
                if (infoView.getVisibility() != View.GONE) {
                    int viewHeight = infoView.getMeasuredHeight();
                    if (viewHeight > infoHeight) {
                        viewHeight = (int) infoHeight;
                    }
                    infoView.layout(getPaddingLeft(),
                            (int) currBottom,
                                    mMeasuredWidth + getPaddingLeft(),
                            (int) (currBottom + viewHeight));
                    currBottom += viewHeight;
                    infoHeight -= viewHeight;
                    if (infoHeight <= 0) {
                        break;
                    }
                }
            }

            if (hasExtraRegion()) {
                for (int i = 0; i < mExtraViewList.size(); i++) {
                    View extraView = mExtraViewList.get(i);
                    if (extraView.getVisibility() != View.GONE) {
                        extraView.layout(getPaddingLeft(),
                                (int) currBottom,
                                        mMeasuredWidth + getPaddingLeft(),
                                (int) (currBottom + extraView.getMeasuredHeight()));
                        currBottom += extraView.getMeasuredHeight();
                    }
                }
            }
        }
        // Force update drawable bounds.
        onSizeChanged(0, 0, right - left, bottom - top);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(mAnimationTrigger);
        cancelAnimations();
    }

    private boolean hasInfoRegion() {
        return mCardType != CARD_TYPE_MAIN_ONLY;
    }

    private boolean hasExtraRegion() {
        return mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA;
    }

    /**
     * Returns target visibility of info region.
     */
    private boolean isRegionVisible(int regionVisibility) {
        switch (regionVisibility) {
            case CARD_REGION_VISIBLE_ALWAYS:
                return true;
            case CARD_REGION_VISIBLE_ACTIVATED:
                return isActivated();
            case CARD_REGION_VISIBLE_SELECTED:
                return isSelected();
            default:
                if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility);
                return false;
        }
    }

    /**
     * Unlike isRegionVisible(), this method returns true when it is fading out when unselected.
     */
    private boolean isCurrentRegionVisible(int regionVisibility) {
        switch (regionVisibility) {
            case CARD_REGION_VISIBLE_ALWAYS:
                return true;
            case CARD_REGION_VISIBLE_ACTIVATED:
                return isActivated();
            case CARD_REGION_VISIBLE_SELECTED:
                if (mCardType == CARD_TYPE_INFO_UNDER) {
                    return mInfoVisFraction > 0f;
                } else {
                    return isSelected();
                }
            default:
                if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility);
                return false;
        }
    }

    private void findChildrenViews() {
        mMainViewList.clear();
        mInfoViewList.clear();
        mExtraViewList.clear();

        final int count = getChildCount();

        boolean infoVisible = hasInfoRegion() && isCurrentRegionVisible(mInfoVisibility);
        boolean extraVisible = hasExtraRegion() && mInfoOffset > 0f;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            if (child == null) {
                continue;
            }

            BaseCardView.LayoutParams lp = (BaseCardView.LayoutParams) child
                    .getLayoutParams();
            if (lp.viewType == LayoutParams.VIEW_TYPE_INFO) {
                child.setAlpha(mInfoAlpha);
                mInfoViewList.add(child);
                child.setVisibility(infoVisible ? View.VISIBLE : View.GONE);
            } else if (lp.viewType == LayoutParams.VIEW_TYPE_EXTRA) {
                mExtraViewList.add(child);
                child.setVisibility(extraVisible ? View.VISIBLE : View.GONE);
            } else {
                // Default to MAIN
                mMainViewList.add(child);
                child.setVisibility(View.VISIBLE);
            }
        }

    }

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        // filter out focus states,  since leanback does not fade foreground on focus.
        final int[] s = super.onCreateDrawableState(extraSpace);
        final int N = s.length;
        boolean pressed = false;
        boolean enabled = false;
        for (int i = 0; i < N; i++) {
            if (s[i] == android.R.attr.state_pressed) {
                pressed = true;
            }
            if (s[i] == android.R.attr.state_enabled) {
                enabled = true;
            }
        }
        if (pressed && enabled) {
            return View.PRESSED_ENABLED_STATE_SET;
        } else if (pressed) {
            return LB_PRESSED_STATE_SET;
        } else if (enabled) {
            return View.ENABLED_STATE_SET;
        } else {
            return View.EMPTY_STATE_SET;
        }
    }

    private void applyActiveState() {
        if (hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_ACTIVATED) {
            setInfoViewVisibility(isRegionVisible(mInfoVisibility));
        }
    }

    private void setInfoViewVisibility(boolean visible) {
        if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
            // Active state changes for card type
            // CARD_TYPE_INFO_UNDER_WITH_EXTRA
            if (visible) {
                for (int i = 0; i < mInfoViewList.size(); i++) {
                    mInfoViewList.get(i).setVisibility(View.VISIBLE);
                }
            } else {
                for (int i = 0; i < mInfoViewList.size(); i++) {
                    mInfoViewList.get(i).setVisibility(View.GONE);
                }
                for (int i = 0; i < mExtraViewList.size(); i++) {
                    mExtraViewList.get(i).setVisibility(View.GONE);
                }
                mInfoOffset = 0.0f;
            }
        } else if (mCardType == CARD_TYPE_INFO_UNDER) {
            // Active state changes for card type CARD_TYPE_INFO_UNDER
            if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
                animateInfoHeight(visible);
            } else {
                for (int i = 0; i < mInfoViewList.size(); i++) {
                    mInfoViewList.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
                }
            }
        } else if (mCardType == CARD_TYPE_INFO_OVER) {
            // Active state changes for card type CARD_TYPE_INFO_OVER
            animateInfoAlpha(visible);
        }
    }

    private void applySelectedState(boolean focused) {
        removeCallbacks(mAnimationTrigger);

        if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
            // Focus changes for card type CARD_TYPE_INFO_UNDER_WITH_EXTRA
            if (focused) {
                if (!mDelaySelectedAnim) {
                    post(mAnimationTrigger);
                    mDelaySelectedAnim = true;
                } else {
                    postDelayed(mAnimationTrigger, mSelectedAnimationDelay);
                }
            } else {
                animateInfoOffset(false);
            }
        } else if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
            setInfoViewVisibility(focused);
        }
    }

    void cancelAnimations() {
        if (mAnim != null) {
            mAnim.cancel();
            mAnim = null;
            // force-clear the animation, as Animation#cancel() doesn't work prior to N,
            // and will instead cause the animation to infinitely loop
            clearAnimation();
        }
    }

    // This animation changes the Y offset of the info and extra views,
    // so that they animate UP to make the extra info area visible when a
    // card is selected.
    void animateInfoOffset(boolean shown) {
        cancelAnimations();

        int extraHeight = 0;
        if (shown) {
            int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
            int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

            for (int i = 0; i < mExtraViewList.size(); i++) {
                View extraView = mExtraViewList.get(i);
                extraView.setVisibility(View.VISIBLE);
                extraView.measure(widthSpec, heightSpec);
                extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight());
            }
        }

        mAnim = new InfoOffsetAnimation(mInfoOffset, shown ? extraHeight : 0);
        mAnim.setDuration(mSelectedAnimDuration);
        mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
        mAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                if (mInfoOffset == 0f) {
                    for (int i = 0; i < mExtraViewList.size(); i++) {
                        mExtraViewList.get(i).setVisibility(View.GONE);
                    }
                }
            }

                @Override
            public void onAnimationRepeat(Animation animation) {
            }

        });
        startAnimation(mAnim);
    }

    // This animation changes the visible height of the info views,
    // so that they animate in and out of view.
    private void animateInfoHeight(boolean shown) {
        cancelAnimations();

        if (shown) {
            for (int i = 0; i < mInfoViewList.size(); i++) {
                View extraView = mInfoViewList.get(i);
                extraView.setVisibility(View.VISIBLE);
            }
        }

        float targetFraction = shown ? 1.0f : 0f;
        if (mInfoVisFraction == targetFraction) {
            return;
        }
        mAnim = new InfoHeightAnimation(mInfoVisFraction, targetFraction);
        mAnim.setDuration(mSelectedAnimDuration);
        mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
        mAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                if (mInfoVisFraction == 0f) {
                    for (int i = 0; i < mInfoViewList.size(); i++) {
                        mInfoViewList.get(i).setVisibility(View.GONE);
                    }
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

        });
        startAnimation(mAnim);
    }

    // This animation changes the alpha of the info views, so they animate in
    // and out. It's meant to be used when the info views are overlaid on top of
    // the main view area. It gets triggered by a change in the Active state of
    // the card.
    private void animateInfoAlpha(boolean shown) {
        cancelAnimations();

        if (shown) {
            for (int i = 0; i < mInfoViewList.size(); i++) {
                mInfoViewList.get(i).setVisibility(View.VISIBLE);
            }
        }
        float targetAlpha = shown ? 1.0f : 0.0f;
        if (targetAlpha == mInfoAlpha) {
            return;
        }

        mAnim = new InfoAlphaAnimation(mInfoAlpha, shown ? 1.0f : 0.0f);
        mAnim.setDuration(mActivatedAnimDuration);
        mAnim.setInterpolator(new DecelerateInterpolator());
        mAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                if (mInfoAlpha == 0.0) {
                    for (int i = 0; i < mInfoViewList.size(); i++) {
                        mInfoViewList.get(i).setVisibility(View.GONE);
                    }
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }

        });
        startAnimation(mAnim);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new BaseCardView.LayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new BaseCardView.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        if (lp instanceof LayoutParams) {
            return new LayoutParams((LayoutParams) lp);
        } else {
            return new LayoutParams(lp);
        }
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof BaseCardView.LayoutParams;
    }

    /**
     * Per-child layout information associated with BaseCardView.
     */
    public static class LayoutParams extends FrameLayout.LayoutParams {
        public static final int VIEW_TYPE_MAIN = 0;
        public static final int VIEW_TYPE_INFO = 1;
        public static final int VIEW_TYPE_EXTRA = 2;

        /**
         * Card component type for the view associated with these LayoutParams.
         */
        @ViewDebug.ExportedProperty(category = "layout", mapping = {
                @ViewDebug.IntToString(from = VIEW_TYPE_MAIN, to = "MAIN"),
                @ViewDebug.IntToString(from = VIEW_TYPE_INFO, to = "INFO"),
                @ViewDebug.IntToString(from = VIEW_TYPE_EXTRA, to = "EXTRA")
        })
        public int viewType = VIEW_TYPE_MAIN;

        /**
         * {@inheritDoc}
         */
        @SuppressLint("CustomViewStyleable")
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView_Layout);

            viewType = a.getInt(
                    R.styleable.lbBaseCardView_Layout_layout_viewType, VIEW_TYPE_MAIN);

            a.recycle();
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(int width, int height) {
            super(width, height);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }

        /**
         * Copy constructor. Clones the width, height, and View Type of the
         * source.
         *
         * @param source The layout params to copy from.
         */
        public LayoutParams(LayoutParams source) {
            super((ViewGroup.MarginLayoutParams) source);

            this.viewType = source.viewType;
        }
    }

    class AnimationBase extends Animation {

        @VisibleForTesting
        final void mockStart() {
            getTransformation(0, null);
        }

        @VisibleForTesting
        final void mockEnd() {
            applyTransformation(1f, null);
            cancelAnimations();
        }
    }

    // Helper animation class used in the animation of the info and extra
    // fields vertically within the card
    final class InfoOffsetAnimation extends AnimationBase {
        private float mStartValue;
        private float mDelta;

        public InfoOffsetAnimation(float start, float end) {
            mStartValue = start;
            mDelta = end - start;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            mInfoOffset = mStartValue + (interpolatedTime * mDelta);
            requestLayout();
        }
    }

    // Helper animation class used in the animation of the visible height
    // for the info fields.
    final class InfoHeightAnimation extends AnimationBase {
        private float mStartValue;
        private float mDelta;

        public InfoHeightAnimation(float start, float end) {
            mStartValue = start;
            mDelta = end - start;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            mInfoVisFraction = mStartValue + (interpolatedTime * mDelta);
            requestLayout();
        }
    }

    // Helper animation class used to animate the alpha for the info views
    // when they are fading in or out of view.
    final class InfoAlphaAnimation extends AnimationBase {
        private float mStartValue;
        private float mDelta;

        public InfoAlphaAnimation(float start, float end) {
            mStartValue = start;
            mDelta = end - start;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            mInfoAlpha = mStartValue + (interpolatedTime * mDelta);
            for (int i = 0; i < mInfoViewList.size(); i++) {
                mInfoViewList.get(i).setAlpha(mInfoAlpha);
            }
        }
    }

    @Override
    public String toString() {
        if (DEBUG) {
            StringBuilder sb = new StringBuilder();
            sb.append(this.getClass().getSimpleName()).append(" : ");
            sb.append("cardType=");
            switch(mCardType) {
                case CARD_TYPE_MAIN_ONLY:
                    sb.append("MAIN_ONLY");
                    break;
                case CARD_TYPE_INFO_OVER:
                    sb.append("INFO_OVER");
                    break;
                case CARD_TYPE_INFO_UNDER:
                    sb.append("INFO_UNDER");
                    break;
                case CARD_TYPE_INFO_UNDER_WITH_EXTRA:
                    sb.append("INFO_UNDER_WITH_EXTRA");
                    break;
                default:
                    sb.append("INVALID");
                    break;
            }
            sb.append(" : ");
            sb.append(mMainViewList.size()).append(" main views, ");
            sb.append(mInfoViewList.size()).append(" info views, ");
            sb.append(mExtraViewList.size()).append(" extra views : ");
            sb.append("infoVisibility=").append(mInfoVisibility).append(" ");
            sb.append("extraVisibility=").append(mExtraVisibility).append(" ");
            sb.append("isActivated=").append(isActivated());
            sb.append(" : ");
            sb.append("isSelected=").append(isSelected());
            return sb.toString();
        } else {
            return super.toString();
        }
    }
}