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 package androidx.leanback.transition; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorSet; 22 import android.animation.TimeInterpolator; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Rect; 26 import android.transition.Fade; 27 import android.transition.Transition; 28 import android.transition.TransitionValues; 29 import android.transition.Visibility; 30 import android.util.AttributeSet; 31 import android.view.Gravity; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.animation.DecelerateInterpolator; 35 36 import androidx.annotation.RequiresApi; 37 import androidx.annotation.RestrictTo; 38 import androidx.leanback.R; 39 40 /** 41 * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) 42 * @hide 43 */ 44 @RequiresApi(21) 45 @RestrictTo(LIBRARY_GROUP) 46 public class FadeAndShortSlide extends Visibility { 47 48 private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); 49 // private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); 50 private static final String PROPNAME_SCREEN_POSITION = 51 "android:fadeAndShortSlideTransition:screenPosition"; 52 53 private CalculateSlide mSlideCalculator; 54 private Visibility mFade = new Fade(); 55 private float mDistance = -1; 56 57 private static abstract class CalculateSlide { 58 CalculateSlide()59 CalculateSlide() { 60 } 61 62 /** Returns the translation X value for view when it goes out of the scene */ getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position)63 float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 64 return view.getTranslationX(); 65 } 66 67 /** Returns the translation Y value for view when it goes out of the scene */ getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position)68 float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 69 return view.getTranslationY(); 70 } 71 } 72 getHorizontalDistance(ViewGroup sceneRoot)73 float getHorizontalDistance(ViewGroup sceneRoot) { 74 return mDistance >= 0 ? mDistance : (sceneRoot.getWidth() / 4); 75 } 76 getVerticalDistance(ViewGroup sceneRoot)77 float getVerticalDistance(ViewGroup sceneRoot) { 78 return mDistance >= 0 ? mDistance : (sceneRoot.getHeight() / 4); 79 } 80 81 final static CalculateSlide sCalculateStart = new CalculateSlide() { 82 @Override 83 public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 84 final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 85 final float x; 86 if (isRtl) { 87 x = view.getTranslationX() + t.getHorizontalDistance(sceneRoot); 88 } else { 89 x = view.getTranslationX() - t.getHorizontalDistance(sceneRoot); 90 } 91 return x; 92 } 93 }; 94 95 final static CalculateSlide sCalculateEnd = new CalculateSlide() { 96 @Override 97 public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 98 final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 99 final float x; 100 if (isRtl) { 101 x = view.getTranslationX() - t.getHorizontalDistance(sceneRoot); 102 } else { 103 x = view.getTranslationX() + t.getHorizontalDistance(sceneRoot); 104 } 105 return x; 106 } 107 }; 108 109 final static CalculateSlide sCalculateStartEnd = new CalculateSlide() { 110 @Override 111 public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 112 final int viewCenter = position[0] + view.getWidth() / 2; 113 sceneRoot.getLocationOnScreen(position); 114 Rect center = t.getEpicenter(); 115 final int sceneRootCenter = center == null ? (position[0] + sceneRoot.getWidth() / 2) 116 : center.centerX(); 117 if (viewCenter < sceneRootCenter) { 118 return view.getTranslationX() - t.getHorizontalDistance(sceneRoot); 119 } else { 120 return view.getTranslationX() + t.getHorizontalDistance(sceneRoot); 121 } 122 } 123 }; 124 125 final static CalculateSlide sCalculateBottom = new CalculateSlide() { 126 @Override 127 public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 128 return view.getTranslationY() + t.getVerticalDistance(sceneRoot); 129 } 130 }; 131 132 final static CalculateSlide sCalculateTop = new CalculateSlide() { 133 @Override 134 public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 135 return view.getTranslationY() - t.getVerticalDistance(sceneRoot); 136 } 137 }; 138 139 final CalculateSlide sCalculateTopBottom = new CalculateSlide() { 140 @Override 141 public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) { 142 final int viewCenter = position[1] + view.getHeight() / 2; 143 sceneRoot.getLocationOnScreen(position); 144 Rect center = getEpicenter(); 145 final int sceneRootCenter = center == null ? (position[1] + sceneRoot.getHeight() / 2) 146 : center.centerY(); 147 if (viewCenter < sceneRootCenter) { 148 return view.getTranslationY() - t.getVerticalDistance(sceneRoot); 149 } else { 150 return view.getTranslationY() + t.getVerticalDistance(sceneRoot); 151 } 152 } 153 }; 154 FadeAndShortSlide()155 public FadeAndShortSlide() { 156 this(Gravity.START); 157 } 158 FadeAndShortSlide(int slideEdge)159 public FadeAndShortSlide(int slideEdge) { 160 setSlideEdge(slideEdge); 161 } 162 FadeAndShortSlide(Context context, AttributeSet attrs)163 public FadeAndShortSlide(Context context, AttributeSet attrs) { 164 super(context, attrs); 165 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSlide); 166 int edge = a.getInt(R.styleable.lbSlide_lb_slideEdge, Gravity.START); 167 setSlideEdge(edge); 168 a.recycle(); 169 } 170 171 @Override setEpicenterCallback(EpicenterCallback epicenterCallback)172 public void setEpicenterCallback(EpicenterCallback epicenterCallback) { 173 mFade.setEpicenterCallback(epicenterCallback); 174 super.setEpicenterCallback(epicenterCallback); 175 } 176 captureValues(TransitionValues transitionValues)177 private void captureValues(TransitionValues transitionValues) { 178 View view = transitionValues.view; 179 int[] position = new int[2]; 180 view.getLocationOnScreen(position); 181 transitionValues.values.put(PROPNAME_SCREEN_POSITION, position); 182 } 183 184 @Override captureStartValues(TransitionValues transitionValues)185 public void captureStartValues(TransitionValues transitionValues) { 186 mFade.captureStartValues(transitionValues); 187 super.captureStartValues(transitionValues); 188 captureValues(transitionValues); 189 } 190 191 @Override captureEndValues(TransitionValues transitionValues)192 public void captureEndValues(TransitionValues transitionValues) { 193 mFade.captureEndValues(transitionValues); 194 super.captureEndValues(transitionValues); 195 captureValues(transitionValues); 196 } 197 setSlideEdge(int slideEdge)198 public void setSlideEdge(int slideEdge) { 199 switch (slideEdge) { 200 case Gravity.START: 201 mSlideCalculator = sCalculateStart; 202 break; 203 case Gravity.END: 204 mSlideCalculator = sCalculateEnd; 205 break; 206 case Gravity.START | Gravity.END: 207 mSlideCalculator = sCalculateStartEnd; 208 break; 209 case Gravity.TOP: 210 mSlideCalculator = sCalculateTop; 211 break; 212 case Gravity.BOTTOM: 213 mSlideCalculator = sCalculateBottom; 214 break; 215 case Gravity.TOP | Gravity.BOTTOM: 216 mSlideCalculator = sCalculateTopBottom; 217 break; 218 default: 219 throw new IllegalArgumentException("Invalid slide direction"); 220 } 221 } 222 223 @Override onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)224 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 225 TransitionValues endValues) { 226 if (endValues == null) { 227 return null; 228 } 229 if (sceneRoot == view) { 230 // workaround b/25375640, avoid run animation on sceneRoot 231 return null; 232 } 233 int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION); 234 int left = position[0]; 235 int top = position[1]; 236 float endX = view.getTranslationX(); 237 float startX = mSlideCalculator.getGoneX(this, sceneRoot, view, position); 238 float endY = view.getTranslationY(); 239 float startY = mSlideCalculator.getGoneY(this, sceneRoot, view, position); 240 final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, 241 left, top, startX, startY, endX, endY, sDecelerate, this); 242 final Animator fadeAnimator = mFade.onAppear(sceneRoot, view, startValues, endValues); 243 if (slideAnimator == null) { 244 return fadeAnimator; 245 } else if (fadeAnimator == null) { 246 return slideAnimator; 247 } 248 final AnimatorSet set = new AnimatorSet(); 249 set.play(slideAnimator).with(fadeAnimator); 250 251 return set; 252 } 253 254 @Override onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)255 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 256 TransitionValues endValues) { 257 if (startValues == null) { 258 return null; 259 } 260 if (sceneRoot == view) { 261 // workaround b/25375640, avoid run animation on sceneRoot 262 return null; 263 } 264 int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION); 265 int left = position[0]; 266 int top = position[1]; 267 float startX = view.getTranslationX(); 268 float endX = mSlideCalculator.getGoneX(this, sceneRoot, view, position); 269 float startY = view.getTranslationY(); 270 float endY = mSlideCalculator.getGoneY(this, sceneRoot, view, position); 271 final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, 272 startValues, left, top, startX, startY, endX, endY, sDecelerate /* sAccelerate */, 273 this); 274 final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues); 275 if (slideAnimator == null) { 276 return fadeAnimator; 277 } else if (fadeAnimator == null) { 278 return slideAnimator; 279 } 280 final AnimatorSet set = new AnimatorSet(); 281 set.play(slideAnimator).with(fadeAnimator); 282 283 return set; 284 } 285 286 @Override addListener(TransitionListener listener)287 public Transition addListener(TransitionListener listener) { 288 mFade.addListener(listener); 289 return super.addListener(listener); 290 } 291 292 @Override removeListener(TransitionListener listener)293 public Transition removeListener(TransitionListener listener) { 294 mFade.removeListener(listener); 295 return super.removeListener(listener); 296 } 297 298 /** 299 * Returns distance to slide. When negative value is returned, it will use 1/4 of 300 * sceneRoot dimension. 301 */ getDistance()302 public float getDistance() { 303 return mDistance; 304 } 305 306 /** 307 * Set distance to slide, default value is -1. when negative value is set, it will use 1/4 of 308 * sceneRoot dimension. 309 * @param distance Pixels to slide. 310 */ setDistance(float distance)311 public void setDistance(float distance) { 312 mDistance = distance; 313 } 314 315 @Override clone()316 public Transition clone() { 317 FadeAndShortSlide clone = null; 318 clone = (FadeAndShortSlide) super.clone(); 319 clone.mFade = (Visibility) mFade.clone(); 320 return clone; 321 } 322 } 323