| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.menu; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.Outline; |
| import android.support.annotation.Nullable; |
| import android.util.AttributeSet; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewOutlineProvider; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import com.android.tv.R; |
| |
| /** |
| * A base class to render a card. |
| */ |
| public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> { |
| private static final String TAG = "BaseCardView"; |
| private static final boolean DEBUG = false; |
| |
| private static final float SCALE_FACTOR_0F = 0f; |
| private static final float SCALE_FACTOR_1F = 1f; |
| |
| private ValueAnimator mFocusAnimator; |
| private final int mFocusAnimDuration; |
| private final float mFocusTranslationZ; |
| private final float mVerticalCardMargin; |
| private final float mCardCornerRadius; |
| private float mFocusAnimatedValue; |
| private boolean mExtendViewOnFocus; |
| private final float mExtendedCardHeight; |
| private final float mTextViewHeight; |
| private final float mExtendedTextViewHeight; |
| @Nullable |
| private TextView mTextView; |
| @Nullable |
| private TextView mTextViewFocused; |
| private final int mCardImageWidth; |
| private final float mCardHeight; |
| |
| public BaseCardView(Context context) { |
| this(context, null); |
| } |
| |
| public BaseCardView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public BaseCardView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| |
| setClipToOutline(true); |
| mFocusAnimDuration = getResources().getInteger(R.integer.menu_focus_anim_duration); |
| mFocusTranslationZ = getResources().getDimension(R.dimen.channel_card_elevation_focused) |
| - getResources().getDimension(R.dimen.card_elevation_normal); |
| mVerticalCardMargin = 2 * ( |
| getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top) |
| + getResources().getDimensionPixelOffset(R.dimen.menu_list_margin_top)); |
| // Ensure the same elevation and focus animation for all subclasses. |
| setElevation(getResources().getDimension(R.dimen.card_elevation_normal)); |
| mCardCornerRadius = getResources().getDimensionPixelSize(R.dimen.channel_card_round_radius); |
| setOutlineProvider(new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius); |
| } |
| }); |
| mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); |
| mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height); |
| mExtendedCardHeight = getResources().getDimensionPixelSize( |
| R.dimen.card_layout_height_extended); |
| mTextViewHeight = getResources().getDimensionPixelSize(R.dimen.card_meta_layout_height); |
| mExtendedTextViewHeight = getResources().getDimensionPixelOffset( |
| R.dimen.card_meta_layout_height_extended); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mTextView = (TextView) findViewById(R.id.card_text); |
| mTextViewFocused = (TextView) findViewById(R.id.card_text_focused); |
| } |
| |
| /** |
| * Called when the view is displayed. |
| * |
| * Before onBind is called, this view's text should be set to determine if it'll be extended |
| * or not in focus state. |
| */ |
| @Override |
| public void onBind(T item, boolean selected) { |
| if (mTextView != null && mTextViewFocused != null) { |
| mTextViewFocused.measure( |
| MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
| mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; |
| if (mExtendViewOnFocus) { |
| setTextViewFocusedAlpha(selected ? 1f : 0f); |
| } else { |
| setTextViewFocusedAlpha(1f); |
| } |
| } |
| setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); |
| } |
| |
| @Override |
| public void onRecycled() { } |
| |
| @Override |
| public void onSelected() { |
| if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { |
| startFocusAnimation(SCALE_FACTOR_1F); |
| } else { |
| cancelFocusAnimationIfAny(); |
| setFocusAnimatedValue(SCALE_FACTOR_1F); |
| } |
| } |
| |
| @Override |
| public void onDeselected() { |
| if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { |
| startFocusAnimation(SCALE_FACTOR_0F); |
| } else { |
| cancelFocusAnimationIfAny(); |
| setFocusAnimatedValue(SCALE_FACTOR_0F); |
| } |
| } |
| |
| /** |
| * Sets text of this card view. |
| */ |
| public void setText(int resId) { |
| if (mTextViewFocused != null) { |
| mTextViewFocused.setText(resId); |
| } |
| if (mTextView != null) { |
| mTextView.setText(resId); |
| } |
| } |
| |
| /** |
| * Sets text of this card view. |
| */ |
| public void setText(String text) { |
| if (mTextViewFocused != null) { |
| mTextViewFocused.setText(text); |
| } |
| if (mTextView != null) { |
| mTextView.setText(text); |
| } |
| } |
| |
| /** |
| * Enables or disables text view of this card view. |
| */ |
| public void setTextViewEnabled(boolean enabled) { |
| if (mTextViewFocused != null) { |
| mTextViewFocused.setEnabled(enabled); |
| } |
| if (mTextView != null) { |
| mTextView.setEnabled(enabled); |
| } |
| } |
| |
| /** |
| * Called when the focus animation started. |
| */ |
| protected void onFocusAnimationStart(boolean selected) { |
| if (mExtendViewOnFocus) { |
| setTextViewFocusedAlpha(selected ? 1f : 0f); |
| } |
| } |
| |
| /** |
| * Called when the focus animation ended. |
| */ |
| protected void onFocusAnimationEnd(boolean selected) { |
| // do nothing. |
| } |
| |
| /** |
| * Called when the view is bound, or while focus animation is running with a value |
| * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}. |
| */ |
| protected void onSetFocusAnimatedValue(float animatedValue) { |
| float cardViewHeight = (mExtendViewOnFocus && isFocused()) |
| ? mExtendedCardHeight : mCardHeight; |
| float scale = 1f + (mVerticalCardMargin / cardViewHeight) * animatedValue; |
| setScaleX(scale); |
| setScaleY(scale); |
| setTranslationZ(mFocusTranslationZ * animatedValue); |
| if (mExtendViewOnFocus) { |
| ViewGroup.LayoutParams params = mTextView.getLayoutParams(); |
| params.height = Math.round(mTextViewHeight |
| + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue); |
| setTextViewLayoutParams(params); |
| setTextViewFocusedAlpha(animatedValue); |
| } |
| } |
| |
| private void setFocusAnimatedValue(float animatedValue) { |
| mFocusAnimatedValue = animatedValue; |
| onSetFocusAnimatedValue(animatedValue); |
| } |
| |
| private void startFocusAnimation(final float targetAnimatedValue) { |
| cancelFocusAnimationIfAny(); |
| final boolean selected = targetAnimatedValue == SCALE_FACTOR_1F; |
| mFocusAnimator = ValueAnimator.ofFloat(mFocusAnimatedValue, targetAnimatedValue); |
| mFocusAnimator.setDuration(mFocusAnimDuration); |
| mFocusAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| setHasTransientState(true); |
| onFocusAnimationStart(selected); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| setHasTransientState(false); |
| onFocusAnimationEnd(selected); |
| } |
| }); |
| mFocusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| setFocusAnimatedValue((Float) animation.getAnimatedValue()); |
| } |
| }); |
| mFocusAnimator.start(); |
| } |
| |
| private void cancelFocusAnimationIfAny() { |
| if (mFocusAnimator != null) { |
| mFocusAnimator.cancel(); |
| mFocusAnimator = null; |
| } |
| } |
| |
| private void setTextViewLayoutParams(ViewGroup.LayoutParams params) { |
| mTextViewFocused.setLayoutParams(params); |
| mTextView.setLayoutParams(params); |
| } |
| |
| private void setTextViewFocusedAlpha(float focusedAlpha) { |
| mTextViewFocused.setAlpha(focusedAlpha); |
| mTextView.setAlpha(1f - focusedAlpha); |
| } |
| } |