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