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.support.annotation.Nullable;
25 import android.util.AttributeSet;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.ViewOutlineProvider;
29 import android.widget.LinearLayout;
30 import android.widget.TextView;
31 
32 import com.android.tv.R;
33 
34 /**
35  * A base class to render a card.
36  */
37 public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> {
38     private static final String TAG = "BaseCardView";
39     private static final boolean DEBUG = false;
40 
41     private static final float SCALE_FACTOR_0F = 0f;
42     private static final float SCALE_FACTOR_1F = 1f;
43 
44     private ValueAnimator mFocusAnimator;
45     private final int mFocusAnimDuration;
46     private final float mFocusTranslationZ;
47     private final float mVerticalCardMargin;
48     private final float mCardCornerRadius;
49     private float mFocusAnimatedValue;
50     private boolean mExtendViewOnFocus;
51     private final float mExtendedCardHeight;
52     private final float mTextViewHeight;
53     private final float mExtendedTextViewHeight;
54     @Nullable
55     private TextView mTextView;
56     @Nullable
57     private TextView mTextViewFocused;
58     private final int mCardImageWidth;
59     private final float mCardHeight;
60 
BaseCardView(Context context)61     public BaseCardView(Context context) {
62         this(context, null);
63     }
64 
BaseCardView(Context context, AttributeSet attrs)65     public BaseCardView(Context context, AttributeSet attrs) {
66         this(context, attrs, 0);
67     }
68 
BaseCardView(Context context, AttributeSet attrs, int defStyle)69     public BaseCardView(Context context, AttributeSet attrs, int defStyle) {
70         super(context, attrs, defStyle);
71 
72         setClipToOutline(true);
73         mFocusAnimDuration = getResources().getInteger(R.integer.menu_focus_anim_duration);
74         mFocusTranslationZ = getResources().getDimension(R.dimen.channel_card_elevation_focused)
75                 - getResources().getDimension(R.dimen.card_elevation_normal);
76         mVerticalCardMargin = 2 * (
77                 getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top)
78                 + getResources().getDimensionPixelOffset(R.dimen.menu_list_margin_top));
79         // Ensure the same elevation and focus animation for all subclasses.
80         setElevation(getResources().getDimension(R.dimen.card_elevation_normal));
81         mCardCornerRadius = getResources().getDimensionPixelSize(R.dimen.channel_card_round_radius);
82         setOutlineProvider(new ViewOutlineProvider() {
83             @Override
84             public void getOutline(View view, Outline outline) {
85                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius);
86             }
87         });
88         mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width);
89         mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height);
90         mExtendedCardHeight = getResources().getDimensionPixelSize(
91                 R.dimen.card_layout_height_extended);
92         mTextViewHeight = getResources().getDimensionPixelSize(R.dimen.card_meta_layout_height);
93         mExtendedTextViewHeight = getResources().getDimensionPixelOffset(
94                 R.dimen.card_meta_layout_height_extended);
95     }
96 
97     @Override
onFinishInflate()98     protected void onFinishInflate() {
99         super.onFinishInflate();
100         mTextView = (TextView) findViewById(R.id.card_text);
101         mTextViewFocused = (TextView) findViewById(R.id.card_text_focused);
102     }
103 
104     /**
105      * Called when the view is displayed.
106      *
107      * Before onBind is called, this view's text should be set to determine if it'll be extended
108      * or not in focus state.
109      */
110     @Override
onBind(T item, boolean selected)111     public void onBind(T item, boolean selected) {
112         if (mTextView != null && mTextViewFocused != null) {
113             mTextViewFocused.measure(
114                     MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
115                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
116             mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1;
117             if (mExtendViewOnFocus) {
118                 setTextViewFocusedAlpha(selected ? 1f : 0f);
119             } else {
120                 setTextViewFocusedAlpha(1f);
121             }
122         }
123         setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
124     }
125 
126     @Override
onRecycled()127     public void onRecycled() { }
128 
129     @Override
onSelected()130     public void onSelected() {
131         if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
132             startFocusAnimation(SCALE_FACTOR_1F);
133         } else {
134             cancelFocusAnimationIfAny();
135             setFocusAnimatedValue(SCALE_FACTOR_1F);
136         }
137     }
138 
139     @Override
onDeselected()140     public void onDeselected() {
141         if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
142             startFocusAnimation(SCALE_FACTOR_0F);
143         } else {
144             cancelFocusAnimationIfAny();
145             setFocusAnimatedValue(SCALE_FACTOR_0F);
146         }
147     }
148 
149     /**
150      * Sets text of this card view.
151      */
setText(int resId)152     public void setText(int resId) {
153         if (mTextViewFocused != null) {
154             mTextViewFocused.setText(resId);
155         }
156         if (mTextView != null) {
157             mTextView.setText(resId);
158         }
159     }
160 
161     /**
162      * Sets text of this card view.
163      */
setText(String text)164     public void setText(String text) {
165         if (mTextViewFocused != null) {
166             mTextViewFocused.setText(text);
167         }
168         if (mTextView != null) {
169             mTextView.setText(text);
170         }
171     }
172 
173     /**
174      * Enables or disables text view of this card view.
175      */
setTextViewEnabled(boolean enabled)176     public void setTextViewEnabled(boolean enabled) {
177         if (mTextViewFocused != null) {
178             mTextViewFocused.setEnabled(enabled);
179         }
180         if (mTextView != null) {
181             mTextView.setEnabled(enabled);
182         }
183     }
184 
185     /**
186      * Called when the focus animation started.
187      */
onFocusAnimationStart(boolean selected)188     protected void onFocusAnimationStart(boolean selected) {
189         if (mExtendViewOnFocus) {
190             setTextViewFocusedAlpha(selected ? 1f : 0f);
191         }
192     }
193 
194     /**
195      * Called when the focus animation ended.
196      */
onFocusAnimationEnd(boolean selected)197     protected void onFocusAnimationEnd(boolean selected) {
198         // do nothing.
199     }
200 
201     /**
202      * Called when the view is bound, or while focus animation is running with a value
203      * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}.
204      */
onSetFocusAnimatedValue(float animatedValue)205     protected void onSetFocusAnimatedValue(float animatedValue) {
206         float cardViewHeight = (mExtendViewOnFocus && isFocused())
207                 ? mExtendedCardHeight : mCardHeight;
208         float scale = 1f + (mVerticalCardMargin / cardViewHeight) * animatedValue;
209         setScaleX(scale);
210         setScaleY(scale);
211         setTranslationZ(mFocusTranslationZ * animatedValue);
212         if (mExtendViewOnFocus) {
213             ViewGroup.LayoutParams params = mTextView.getLayoutParams();
214             params.height = Math.round(mTextViewHeight
215                     + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue);
216             setTextViewLayoutParams(params);
217             setTextViewFocusedAlpha(animatedValue);
218         }
219     }
220 
setFocusAnimatedValue(float animatedValue)221     private void setFocusAnimatedValue(float animatedValue) {
222         mFocusAnimatedValue = animatedValue;
223         onSetFocusAnimatedValue(animatedValue);
224     }
225 
startFocusAnimation(final float targetAnimatedValue)226     private void startFocusAnimation(final float targetAnimatedValue) {
227         cancelFocusAnimationIfAny();
228         final boolean selected = targetAnimatedValue == SCALE_FACTOR_1F;
229         mFocusAnimator = ValueAnimator.ofFloat(mFocusAnimatedValue, targetAnimatedValue);
230         mFocusAnimator.setDuration(mFocusAnimDuration);
231         mFocusAnimator.addListener(new AnimatorListenerAdapter() {
232             @Override
233             public void onAnimationStart(Animator animation) {
234                 setHasTransientState(true);
235                 onFocusAnimationStart(selected);
236             }
237 
238             @Override
239             public void onAnimationEnd(Animator animation) {
240                 setHasTransientState(false);
241                 onFocusAnimationEnd(selected);
242             }
243         });
244         mFocusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
245             @Override
246             public void onAnimationUpdate(ValueAnimator animation) {
247                 setFocusAnimatedValue((Float) animation.getAnimatedValue());
248             }
249         });
250         mFocusAnimator.start();
251     }
252 
cancelFocusAnimationIfAny()253     private void cancelFocusAnimationIfAny() {
254         if (mFocusAnimator != null) {
255             mFocusAnimator.cancel();
256             mFocusAnimator = null;
257         }
258     }
259 
setTextViewLayoutParams(ViewGroup.LayoutParams params)260     private void setTextViewLayoutParams(ViewGroup.LayoutParams params) {
261         mTextViewFocused.setLayoutParams(params);
262         mTextView.setLayoutParams(params);
263     }
264 
setTextViewFocusedAlpha(float focusedAlpha)265     private void setTextViewFocusedAlpha(float focusedAlpha) {
266         mTextViewFocused.setAlpha(focusedAlpha);
267         mTextView.setAlpha(1f - focusedAlpha);
268     }
269 }
270