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