1 /* 2 * Copyright (C) 2014 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 package androidx.leanback.transition; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ObjectAnimator; 21 import android.animation.TimeInterpolator; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.transition.TransitionValues; 25 import android.transition.Visibility; 26 import android.util.AttributeSet; 27 import android.util.Property; 28 import android.view.Gravity; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.animation.AccelerateInterpolator; 32 import android.view.animation.AnimationUtils; 33 import android.view.animation.DecelerateInterpolator; 34 35 import androidx.annotation.RequiresApi; 36 import androidx.leanback.R; 37 38 /** 39 * Slide distance toward/from a edge. 40 * This is a limited Slide implementation for KitKat without propagation support. 41 */ 42 @RequiresApi(19) 43 class SlideKitkat extends Visibility { 44 private static final String TAG = "SlideKitkat"; 45 46 private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); 47 private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); 48 49 private int mSlideEdge; 50 private CalculateSlide mSlideCalculator; 51 52 private interface CalculateSlide { 53 /** Returns the translation value for view when it out of the scene */ 54 float getGone(View view); 55 56 /** Returns the translation value for view when it is in the scene */ 57 float getHere(View view); 58 59 /** Returns the property to animate translation */ 60 Property<View, Float> getProperty(); 61 } 62 63 private static abstract class CalculateSlideHorizontal implements CalculateSlide { 64 CalculateSlideHorizontal() { 65 } 66 67 @Override 68 public float getHere(View view) { 69 return view.getTranslationX(); 70 } 71 72 @Override 73 public Property<View, Float> getProperty() { 74 return View.TRANSLATION_X; 75 } 76 } 77 78 private static abstract class CalculateSlideVertical implements CalculateSlide { 79 CalculateSlideVertical() { 80 } 81 82 @Override 83 public float getHere(View view) { 84 return view.getTranslationY(); 85 } 86 87 @Override 88 public Property<View, Float> getProperty() { 89 return View.TRANSLATION_Y; 90 } 91 } 92 93 private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() { 94 @Override 95 public float getGone(View view) { 96 return view.getTranslationX() - view.getWidth(); 97 } 98 }; 99 100 private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() { 101 @Override 102 public float getGone(View view) { 103 return view.getTranslationY() - view.getHeight(); 104 } 105 }; 106 107 private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() { 108 @Override 109 public float getGone(View view) { 110 return view.getTranslationX() + view.getWidth(); 111 } 112 }; 113 114 private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() { 115 @Override 116 public float getGone(View view) { 117 return view.getTranslationY() + view.getHeight(); 118 } 119 }; 120 121 private static final CalculateSlide sCalculateStart = new CalculateSlideHorizontal() { 122 @Override 123 public float getGone(View view) { 124 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 125 return view.getTranslationX() + view.getWidth(); 126 } else { 127 return view.getTranslationX() - view.getWidth(); 128 } 129 } 130 }; 131 132 private static final CalculateSlide sCalculateEnd = new CalculateSlideHorizontal() { 133 @Override 134 public float getGone(View view) { 135 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 136 return view.getTranslationX() - view.getWidth(); 137 } else { 138 return view.getTranslationX() + view.getWidth(); 139 } 140 } 141 }; 142 143 public SlideKitkat() { 144 setSlideEdge(Gravity.BOTTOM); 145 } 146 147 public SlideKitkat(Context context, AttributeSet attrs) { 148 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSlide); 149 int edge = a.getInt(R.styleable.lbSlide_lb_slideEdge, Gravity.BOTTOM); 150 setSlideEdge(edge); 151 long duration = a.getInt(R.styleable.lbSlide_android_duration, -1); 152 if (duration >= 0) { 153 setDuration(duration); 154 } 155 long startDelay = a.getInt(R.styleable.lbSlide_android_startDelay, -1); 156 if (startDelay > 0) { 157 setStartDelay(startDelay); 158 } 159 final int resID = a.getResourceId(R.styleable.lbSlide_android_interpolator, 0); 160 if (resID > 0) { 161 setInterpolator(AnimationUtils.loadInterpolator(context, resID)); 162 } 163 a.recycle(); 164 } 165 166 /** 167 * Change the edge that Views appear and disappear from. 168 * 169 * @param slideEdge The edge of the scene to use for Views appearing and disappearing. One of 170 * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP}, 171 * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM}, 172 * {@link android.view.Gravity#START}, {@link android.view.Gravity#END}. 173 */ 174 public void setSlideEdge(int slideEdge) { 175 switch (slideEdge) { 176 case Gravity.LEFT: 177 mSlideCalculator = sCalculateLeft; 178 break; 179 case Gravity.TOP: 180 mSlideCalculator = sCalculateTop; 181 break; 182 case Gravity.RIGHT: 183 mSlideCalculator = sCalculateRight; 184 break; 185 case Gravity.BOTTOM: 186 mSlideCalculator = sCalculateBottom; 187 break; 188 case Gravity.START: 189 mSlideCalculator = sCalculateStart; 190 break; 191 case Gravity.END: 192 mSlideCalculator = sCalculateEnd; 193 break; 194 default: 195 throw new IllegalArgumentException("Invalid slide direction"); 196 } 197 mSlideEdge = slideEdge; 198 } 199 200 /** 201 * Returns the edge that Views appear and disappear from. 202 * @return the edge of the scene to use for Views appearing and disappearing. One of 203 * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP}, 204 * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM}, 205 * {@link android.view.Gravity#START}, {@link android.view.Gravity#END}. 206 */ 207 public int getSlideEdge() { 208 return mSlideEdge; 209 } 210 211 private Animator createAnimation(final View view, Property<View, Float> property, 212 float start, float end, float terminalValue, TimeInterpolator interpolator, 213 int finalVisibility) { 214 float[] startPosition = (float[]) view.getTag(R.id.lb_slide_transition_value); 215 if (startPosition != null) { 216 start = View.TRANSLATION_Y == property ? startPosition[1] : startPosition[0]; 217 view.setTag(R.id.lb_slide_transition_value, null); 218 } 219 final ObjectAnimator anim = ObjectAnimator.ofFloat(view, property, start, end); 220 221 SlideAnimatorListener listener = new SlideAnimatorListener(view, property, terminalValue, end, 222 finalVisibility); 223 anim.addListener(listener); 224 anim.addPauseListener(listener); 225 anim.setInterpolator(interpolator); 226 return anim; 227 } 228 229 @Override 230 public Animator onAppear(ViewGroup sceneRoot, 231 TransitionValues startValues, int startVisibility, 232 TransitionValues endValues, int endVisibility) { 233 View view = (endValues != null) ? endValues.view : null; 234 if (view == null) { 235 return null; 236 } 237 float end = mSlideCalculator.getHere(view); 238 float start = mSlideCalculator.getGone(view); 239 return createAnimation(view, mSlideCalculator.getProperty(), start, end, end, sDecelerate, 240 View.VISIBLE); 241 } 242 243 @Override 244 public Animator onDisappear(ViewGroup sceneRoot, 245 TransitionValues startValues, int startVisibility, 246 TransitionValues endValues, int endVisibility) { 247 View view = (startValues != null) ? startValues.view : null; 248 if (view == null) { 249 return null; 250 } 251 float start = mSlideCalculator.getHere(view); 252 float end = mSlideCalculator.getGone(view); 253 254 return createAnimation(view, mSlideCalculator.getProperty(), start, end, start, 255 sAccelerate, View.INVISIBLE); 256 } 257 258 private static class SlideAnimatorListener extends AnimatorListenerAdapter { 259 private boolean mCanceled = false; 260 private float mPausedValue; 261 private final View mView; 262 private final float mEndValue; 263 private final float mTerminalValue; 264 private final int mFinalVisibility; 265 private final Property<View, Float> mProp; 266 267 public SlideAnimatorListener(View view, Property<View, Float> prop, 268 float terminalValue, float endValue, int finalVisibility) { 269 mProp = prop; 270 mView = view; 271 mTerminalValue = terminalValue; 272 mEndValue = endValue; 273 mFinalVisibility = finalVisibility; 274 view.setVisibility(View.VISIBLE); 275 } 276 277 @Override 278 public void onAnimationCancel(Animator animator) { 279 float[] transitionPosition = new float[2]; 280 transitionPosition[0] = mView.getTranslationX(); 281 transitionPosition[1] = mView.getTranslationY(); 282 mView.setTag(R.id.lb_slide_transition_value, transitionPosition); 283 mProp.set(mView, mTerminalValue); 284 mCanceled = true; 285 } 286 287 @Override 288 public void onAnimationEnd(Animator animator) { 289 if (!mCanceled) { 290 mProp.set(mView, mTerminalValue); 291 } 292 mView.setVisibility(mFinalVisibility); 293 } 294 295 @Override 296 public void onAnimationPause(Animator animator) { 297 mPausedValue = mProp.get(mView); 298 mProp.set(mView, mEndValue); 299 mView.setVisibility(mFinalVisibility); 300 } 301 302 @Override 303 public void onAnimationResume(Animator animator) { 304 mProp.set(mView, mPausedValue); 305 mView.setVisibility(View.VISIBLE); 306 } 307 } 308 }