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