1 /*
2  * Copyright (C) 2015 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 com.android.tv.menu;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.Outline;
24 import android.util.AttributeSet;
25 import android.view.View;
26 import android.view.ViewOutlineProvider;
27 import android.widget.LinearLayout;
28 
29 import com.android.tv.R;
30 
31 /**
32  * A base class to render a card.
33  */
34 public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> {
35     private static final String TAG = "BaseCardView";
36     private static final boolean DEBUG = false;
37 
38     private static final float SCALE_FACTOR_0F = 0f;
39     private static final float SCALE_FACTOR_1F = 1f;
40 
41     private ValueAnimator mFocusAnimator;
42     private final int mFocusAnimDuration;
43     private final float mFocusTranslationZ;
44     private final float mVerticalCardMargin;
45     private final float mCardCornerRadius;
46     private float mFocusAnimatedValue;
47 
BaseCardView(Context context)48     public BaseCardView(Context context) {
49         this(context, null);
50     }
51 
BaseCardView(Context context, AttributeSet attrs)52     public BaseCardView(Context context, AttributeSet attrs) {
53         this(context, attrs, 0);
54     }
55 
BaseCardView(Context context, AttributeSet attrs, int defStyle)56     public BaseCardView(Context context, AttributeSet attrs, int defStyle) {
57         super(context, attrs, defStyle);
58 
59         setClipToOutline(true);
60         mFocusAnimDuration = getResources().getInteger(R.integer.menu_focus_anim_duration);
61         mFocusTranslationZ = getResources().getDimension(R.dimen.channel_card_elevation_focused)
62                 - getResources().getDimension(R.dimen.card_elevation_normal);
63         mVerticalCardMargin = 2 * (
64                 getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top)
65                 + getResources().getDimensionPixelOffset(R.dimen.menu_list_margin_top));
66         // Ensure the same elevation and focus animation for all subclasses.
67         setElevation(getResources().getDimension(R.dimen.card_elevation_normal));
68         mCardCornerRadius = getResources().getDimensionPixelSize(R.dimen.channel_card_round_radius);
69         setOutlineProvider(new ViewOutlineProvider() {
70             @Override
71             public void getOutline(View view, Outline outline) {
72                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius);
73             }
74         });
75     }
76 
77     /**
78      * Called when the view is displayed.
79      */
80     @Override
onBind(T item, boolean selected)81     public void onBind(T item, boolean selected) {
82         // Note that getCardHeight() will be called by setFocusAnimatedValue().
83         // Therefore, be sure that getCardHeight() has a proper value before this method is called.
84        setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
85     }
86 
87     @Override
onRecycled()88     public void onRecycled() { }
89 
90     @Override
onSelected()91     public void onSelected() {
92         if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
93             startFocusAnimation(SCALE_FACTOR_1F);
94         } else {
95             cancelFocusAnimationIfAny();
96             setFocusAnimatedValue(SCALE_FACTOR_1F);
97         }
98     }
99 
100     @Override
onDeselected()101     public void onDeselected() {
102         if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
103             startFocusAnimation(SCALE_FACTOR_0F);
104         } else {
105             cancelFocusAnimationIfAny();
106             setFocusAnimatedValue(SCALE_FACTOR_0F);
107         }
108     }
109 
110     /**
111      * Called when the focus animation started.
112      */
onFocusAnimationStart(boolean selected)113     protected void onFocusAnimationStart(boolean selected) {
114         // do nothing.
115     }
116 
117     /**
118      * Called when the focus animation ended.
119      */
onFocusAnimationEnd(boolean selected)120     protected void onFocusAnimationEnd(boolean selected) {
121         // do nothing.
122     }
123 
124     /**
125      * Called when the view is bound, or while focus animation is running with a value
126      * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}.
127      */
onSetFocusAnimatedValue(float animatedValue)128     protected void onSetFocusAnimatedValue(float animatedValue) {
129         float scale = 1f + (mVerticalCardMargin / getCardHeight()) * animatedValue;
130         setScaleX(scale);
131         setScaleY(scale);
132         setTranslationZ(mFocusTranslationZ * animatedValue);
133     }
134 
setFocusAnimatedValue(float animatedValue)135     private void setFocusAnimatedValue(float animatedValue) {
136         mFocusAnimatedValue = animatedValue;
137         onSetFocusAnimatedValue(animatedValue);
138     }
139 
startFocusAnimation(final float targetAnimatedValue)140     private void startFocusAnimation(final float targetAnimatedValue) {
141         cancelFocusAnimationIfAny();
142         final boolean selected = targetAnimatedValue == SCALE_FACTOR_1F;
143         mFocusAnimator = ValueAnimator.ofFloat(mFocusAnimatedValue, targetAnimatedValue);
144         mFocusAnimator.setDuration(mFocusAnimDuration);
145         mFocusAnimator.addListener(new AnimatorListenerAdapter() {
146             @Override
147             public void onAnimationStart(Animator animation) {
148                 setHasTransientState(true);
149                 onFocusAnimationStart(selected);
150             }
151 
152             @Override
153             public void onAnimationEnd(Animator animation) {
154                 setHasTransientState(false);
155                 onFocusAnimationEnd(selected);
156             }
157         });
158         mFocusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
159             @Override
160             public void onAnimationUpdate(ValueAnimator animation) {
161                 setFocusAnimatedValue((Float) animation.getAnimatedValue());
162             }
163         });
164         mFocusAnimator.start();
165     }
166 
cancelFocusAnimationIfAny()167     private void cancelFocusAnimationIfAny() {
168         if (mFocusAnimator != null) {
169             mFocusAnimator.cancel();
170             mFocusAnimator = null;
171         }
172     }
173 
174     /**
175      * The implementation should return the height of the card.
176      */
getCardHeight()177     protected abstract float getCardHeight();
178 }
179