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