• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BaseCardView(Context context)148     public BaseCardView(Context context) {
149         this(context, null);
150     }
151 
BaseCardView(Context context, AttributeSet attrs)152     public BaseCardView(Context context, AttributeSet attrs) {
153         this(context, attrs, R.attr.baseCardViewStyle);
154     }
155 
BaseCardView(Context context, AttributeSet attrs, int defStyleAttr)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      */
setSelectedAnimationDelayed(boolean delay)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      */
isSelectedAnimationDelayed()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      */
setCardType(int type)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      */
getCardType()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      */
setInfoVisibility(int visibility)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 
getFinalInfoVisFraction()292     final float getFinalInfoVisFraction() {
293         return mCardType == CARD_TYPE_INFO_UNDER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED
294                 && !isSelected() ? 0.0f : 1.0f;
295     }
296 
getFinalInfoAlpha()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      */
getInfoVisibility()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
setExtraVisibility(int visibility)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
getExtraVisibility()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
setActivated(boolean activated)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
setSelected(boolean selected)360     public void setSelected(boolean selected) {
361         if (selected != isSelected()) {
362             super.setSelected(selected);
363             applySelectedState(isSelected());
364         }
365     }
366 
367     @Override
shouldDelayChildPressedState()368     public boolean shouldDelayChildPressedState() {
369         return false;
370     }
371 
372     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
onDetachedFromWindow()510     protected void onDetachedFromWindow() {
511         super.onDetachedFromWindow();
512         removeCallbacks(mAnimationTrigger);
513         cancelAnimations();
514     }
515 
hasInfoRegion()516     private boolean hasInfoRegion() {
517         return mCardType != CARD_TYPE_MAIN_ONLY;
518     }
519 
hasExtraRegion()520     private boolean hasExtraRegion() {
521         return mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA;
522     }
523 
524     /**
525      * Returns target visibility of info region.
526      */
isRegionVisible(int regionVisibility)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      */
isCurrentRegionVisible(int regionVisibility)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 
findChildrenViews()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
onCreateDrawableState(int extraSpace)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 
applyActiveState(boolean active)623     private void applyActiveState(boolean active) {
624         if (hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_ACTIVATED) {
625             setInfoViewVisibility(isRegionVisible(mInfoVisibility));
626         }
627     }
628 
setInfoViewVisibility(boolean visible)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 
applySelectedState(boolean focused)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 
cancelAnimations()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.
animateInfoOffset(boolean shown)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.
animateInfoHeight(boolean shown)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.
animateInfoAlpha(boolean shown)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
generateLayoutParams(AttributeSet attrs)819     public LayoutParams generateLayoutParams(AttributeSet attrs) {
820         return new BaseCardView.LayoutParams(getContext(), attrs);
821     }
822 
823     @Override
generateDefaultLayoutParams()824     protected LayoutParams generateDefaultLayoutParams() {
825         return new BaseCardView.LayoutParams(
826                 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
827     }
828 
829     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)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
checkLayoutParams(ViewGroup.LayoutParams p)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          */
LayoutParams(Context c, AttributeSet attrs)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          */
LayoutParams(int width, int height)877         public LayoutParams(int width, int height) {
878             super(width, height);
879         }
880 
881         /**
882          * {@inheritDoc}
883          */
LayoutParams(ViewGroup.LayoutParams p)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          */
LayoutParams(LayoutParams source)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
mockStart()904         final void mockStart() {
905             getTransformation(0, null);
906         }
907 
908         @VisibleForTesting
mockEnd()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 
InfoOffsetAnimation(float start, float end)921         public InfoOffsetAnimation(float start, float end) {
922             mStartValue = start;
923             mDelta = end - start;
924         }
925 
926         @Override
applyTransformation(float interpolatedTime, Transformation t)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 
InfoHeightAnimation(float start, float end)939         public InfoHeightAnimation(float start, float end) {
940             mStartValue = start;
941             mDelta = end - start;
942         }
943 
944         @Override
applyTransformation(float interpolatedTime, Transformation t)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 
InfoAlphaAnimation(float start, float end)957         public InfoAlphaAnimation(float start, float end) {
958             mStartValue = start;
959             mDelta = end - start;
960         }
961 
962         @Override
applyTransformation(float interpolatedTime, Transformation t)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
toString()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