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