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