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