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.util.AttributeSet; 25 import android.view.View; 26 import android.view.ViewOutlineProvider; 27 import android.widget.LinearLayout; 28 29 import com.android.tv.R; 30 31 /** 32 * A base class to render a card. 33 */ 34 public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> { 35 private static final String TAG = "BaseCardView"; 36 private static final boolean DEBUG = false; 37 38 private static final float SCALE_FACTOR_0F = 0f; 39 private static final float SCALE_FACTOR_1F = 1f; 40 41 private ValueAnimator mFocusAnimator; 42 private final int mFocusAnimDuration; 43 private final float mFocusTranslationZ; 44 private final float mVerticalCardMargin; 45 private final float mCardCornerRadius; 46 private float mFocusAnimatedValue; 47 48 public BaseCardView(Context context) { 49 this(context, null); 50 } 51 52 public BaseCardView(Context context, AttributeSet attrs) { 53 this(context, attrs, 0); 54 } 55 56 public BaseCardView(Context context, AttributeSet attrs, int defStyle) { 57 super(context, attrs, defStyle); 58 59 setClipToOutline(true); 60 mFocusAnimDuration = getResources().getInteger(R.integer.menu_focus_anim_duration); 61 mFocusTranslationZ = getResources().getDimension(R.dimen.channel_card_elevation_focused) 62 - getResources().getDimension(R.dimen.card_elevation_normal); 63 mVerticalCardMargin = 2 * ( 64 getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top) 65 + getResources().getDimensionPixelOffset(R.dimen.menu_list_margin_top)); 66 // Ensure the same elevation and focus animation for all subclasses. 67 setElevation(getResources().getDimension(R.dimen.card_elevation_normal)); 68 mCardCornerRadius = getResources().getDimensionPixelSize(R.dimen.channel_card_round_radius); 69 setOutlineProvider(new ViewOutlineProvider() { 70 @Override 71 public void getOutline(View view, Outline outline) { 72 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius); 73 } 74 }); 75 } 76 77 /** 78 * Called when the view is displayed. 79 */ 80 @Override 81 public void onBind(T item, boolean selected) { 82 // Note that getCardHeight() will be called by setFocusAnimatedValue(). 83 // Therefore, be sure that getCardHeight() has a proper value before this method is called. 84 setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); 85 } 86 87 @Override 88 public void onRecycled() { } 89 90 @Override 91 public void onSelected() { 92 if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { 93 startFocusAnimation(SCALE_FACTOR_1F); 94 } else { 95 cancelFocusAnimationIfAny(); 96 setFocusAnimatedValue(SCALE_FACTOR_1F); 97 } 98 } 99 100 @Override 101 public void onDeselected() { 102 if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { 103 startFocusAnimation(SCALE_FACTOR_0F); 104 } else { 105 cancelFocusAnimationIfAny(); 106 setFocusAnimatedValue(SCALE_FACTOR_0F); 107 } 108 } 109 110 /** 111 * Called when the focus animation started. 112 */ 113 protected void onFocusAnimationStart(boolean selected) { 114 // do nothing. 115 } 116 117 /** 118 * Called when the focus animation ended. 119 */ 120 protected void onFocusAnimationEnd(boolean selected) { 121 // do nothing. 122 } 123 124 /** 125 * Called when the view is bound, or while focus animation is running with a value 126 * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}. 127 */ 128 protected void onSetFocusAnimatedValue(float animatedValue) { 129 float scale = 1f + (mVerticalCardMargin / getCardHeight()) * animatedValue; 130 setScaleX(scale); 131 setScaleY(scale); 132 setTranslationZ(mFocusTranslationZ * animatedValue); 133 } 134 135 private void setFocusAnimatedValue(float animatedValue) { 136 mFocusAnimatedValue = animatedValue; 137 onSetFocusAnimatedValue(animatedValue); 138 } 139 140 private void startFocusAnimation(final float targetAnimatedValue) { 141 cancelFocusAnimationIfAny(); 142 final boolean selected = targetAnimatedValue == SCALE_FACTOR_1F; 143 mFocusAnimator = ValueAnimator.ofFloat(mFocusAnimatedValue, targetAnimatedValue); 144 mFocusAnimator.setDuration(mFocusAnimDuration); 145 mFocusAnimator.addListener(new AnimatorListenerAdapter() { 146 @Override 147 public void onAnimationStart(Animator animation) { 148 setHasTransientState(true); 149 onFocusAnimationStart(selected); 150 } 151 152 @Override 153 public void onAnimationEnd(Animator animation) { 154 setHasTransientState(false); 155 onFocusAnimationEnd(selected); 156 } 157 }); 158 mFocusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 159 @Override 160 public void onAnimationUpdate(ValueAnimator animation) { 161 setFocusAnimatedValue((Float) animation.getAnimatedValue()); 162 } 163 }); 164 mFocusAnimator.start(); 165 } 166 167 private void cancelFocusAnimationIfAny() { 168 if (mFocusAnimator != null) { 169 mFocusAnimator.cancel(); 170 mFocusAnimator = null; 171 } 172 } 173 174 /** 175 * The implementation should return the height of the card. 176 */ 177 protected abstract float getCardHeight(); 178 } 179