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