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