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