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 com.android.tv.common.ui.setup.animation; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.AnimatorSet; 21 import android.animation.TimeInterpolator; 22 import android.transition.Fade; 23 import android.transition.Transition; 24 import android.transition.TransitionValues; 25 import android.transition.Visibility; 26 import android.view.Gravity; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.ViewParent; 30 import android.view.animation.AccelerateInterpolator; 31 import android.view.animation.DecelerateInterpolator; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.Comparator; 36 import java.util.List; 37 38 /** 39 * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) 40 */ 41 public class FadeAndShortSlide extends Visibility { 42 private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator(); 43 private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator(); 44 45 private static final String PROPNAME_SCREEN_POSITION = 46 "android_fadeAndShortSlideTransition_screenPosition"; 47 private static final String PROPNAME_DELAY = "propname_delay"; 48 49 private static final int DEFAULT_DISTANCE = 200; 50 51 private static abstract class CalculateSlide { 52 /** Returns the translation value for view when it goes out of the scene */ getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance)53 public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position, 54 int distance); 55 } 56 57 private static final CalculateSlide sCalculateStart = new CalculateSlide() { 58 @Override 59 public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { 60 final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 61 final float x; 62 if (isRtl) { 63 x = view.getTranslationX() + distance; 64 } else { 65 x = view.getTranslationX() - distance; 66 } 67 return x; 68 } 69 }; 70 71 private static final CalculateSlide sCalculateEnd = new CalculateSlide() { 72 @Override 73 public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { 74 final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 75 final float x; 76 if (isRtl) { 77 x = view.getTranslationX() - distance; 78 } else { 79 x = view.getTranslationX() + distance; 80 } 81 return x; 82 } 83 }; 84 85 private static final ViewPositionComparator sViewPositionComparator = 86 new ViewPositionComparator(); 87 88 private int mSlideEdge; 89 private CalculateSlide mSlideCalculator = sCalculateEnd; 90 private Visibility mFade = new Fade(); 91 92 // TODO: Consider using TransitionPropagation. 93 private int[] mParentIdsForDelay; 94 private int mDistance = DEFAULT_DISTANCE; 95 FadeAndShortSlide()96 public FadeAndShortSlide() { 97 this(Gravity.START); 98 } 99 FadeAndShortSlide(int slideEdge)100 public FadeAndShortSlide(int slideEdge) { 101 this(slideEdge, null); 102 } 103 FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay)104 public FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay) { 105 setSlideEdge(slideEdge); 106 mParentIdsForDelay = parentIdsForDelay; 107 } 108 109 @Override setEpicenterCallback(EpicenterCallback epicenterCallback)110 public void setEpicenterCallback(EpicenterCallback epicenterCallback) { 111 super.setEpicenterCallback(epicenterCallback); 112 mFade.setEpicenterCallback(epicenterCallback); 113 } 114 captureValues(TransitionValues transitionValues)115 private void captureValues(TransitionValues transitionValues) { 116 View view = transitionValues.view; 117 int[] position = new int[2]; 118 view.getLocationOnScreen(position); 119 transitionValues.values.put(PROPNAME_SCREEN_POSITION, position); 120 } 121 getDelayOrder(View view, boolean appear)122 private int getDelayOrder(View view, boolean appear) { 123 if (mParentIdsForDelay == null) { 124 return -1; 125 } 126 final View parentForDelay = findParentForDelay(view); 127 if (parentForDelay == null || !(parentForDelay instanceof ViewGroup)) { 128 return -1; 129 } 130 List<View> transitionTargets = new ArrayList<>(); 131 getTransitionTargets((ViewGroup) parentForDelay, transitionTargets); 132 sViewPositionComparator.mParentForDelay = parentForDelay; 133 sViewPositionComparator.mIsLtr = view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 134 sViewPositionComparator.mToLeft = sViewPositionComparator.mIsLtr 135 ? mSlideEdge == (appear ? Gravity.END : Gravity.START) 136 : mSlideEdge == (appear ? Gravity.START : Gravity.END); 137 Collections.sort(transitionTargets, sViewPositionComparator); 138 return transitionTargets.indexOf(view); 139 } 140 findParentForDelay(View view)141 private View findParentForDelay(View view) { 142 if (isParentForDelay(view.getId())) { 143 return view; 144 } 145 View parent = view; 146 while (parent.getParent() instanceof View) { 147 parent = (View) parent.getParent(); 148 if (isParentForDelay(parent.getId())) { 149 return parent; 150 } 151 } 152 return null; 153 } 154 isParentForDelay(int viewId)155 private boolean isParentForDelay(int viewId) { 156 for (int id : mParentIdsForDelay) { 157 if (id == viewId) { 158 return true; 159 } 160 } 161 return false; 162 } 163 getTransitionTargets(ViewGroup parent, List<View> transitionTargets)164 private void getTransitionTargets(ViewGroup parent, List<View> transitionTargets) { 165 int count = parent.getChildCount(); 166 for (int i = 0; i < count; ++i) { 167 View child = parent.getChildAt(i); 168 if (child instanceof ViewGroup && !((ViewGroup) child).isTransitionGroup()) { 169 getTransitionTargets((ViewGroup) child, transitionTargets); 170 } else { 171 transitionTargets.add(child); 172 } 173 } 174 } 175 176 @Override captureStartValues(TransitionValues transitionValues)177 public void captureStartValues(TransitionValues transitionValues) { 178 super.captureStartValues(transitionValues); 179 mFade.captureStartValues(transitionValues); 180 captureValues(transitionValues); 181 int delayIndex = getDelayOrder(transitionValues.view, false); 182 if (delayIndex > 0) { 183 transitionValues.values.put(PROPNAME_DELAY, 184 delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); 185 } 186 } 187 188 @Override captureEndValues(TransitionValues transitionValues)189 public void captureEndValues(TransitionValues transitionValues) { 190 super.captureEndValues(transitionValues); 191 mFade.captureEndValues(transitionValues); 192 captureValues(transitionValues); 193 int delayIndex = getDelayOrder(transitionValues.view, true); 194 if (delayIndex > 0) { 195 transitionValues.values.put(PROPNAME_DELAY, 196 delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); 197 } 198 } 199 setSlideEdge(int slideEdge)200 public void setSlideEdge(int slideEdge) { 201 mSlideEdge = slideEdge; 202 switch (slideEdge) { 203 case Gravity.START: 204 mSlideCalculator = sCalculateStart; 205 break; 206 case Gravity.END: 207 mSlideCalculator = sCalculateEnd; 208 break; 209 default: 210 throw new IllegalArgumentException("Invalid slide direction"); 211 } 212 } 213 214 @Override onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)215 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 216 TransitionValues endValues) { 217 if (endValues == null) { 218 return null; 219 } 220 int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION); 221 int left = position[0]; 222 float endX = view.getTranslationX(); 223 float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); 224 final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, 225 left, startX, endX, APPEAR_INTERPOLATOR, this); 226 if (slideAnimator == null) { 227 return null; 228 } 229 mFade.setInterpolator(APPEAR_INTERPOLATOR); 230 final AnimatorSet set = new AnimatorSet(); 231 set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues)); 232 Long delay = (Long ) endValues.values.get(PROPNAME_DELAY); 233 if (delay != null) { 234 set.setStartDelay(delay); 235 } 236 return set; 237 } 238 239 @Override onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues)240 public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, 241 TransitionValues endValues) { 242 if (startValues == null) { 243 return null; 244 } 245 int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION); 246 int left = position[0]; 247 float startX = view.getTranslationX(); 248 float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); 249 final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, 250 startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); 251 if (slideAnimator == null) { // slideAnimator is null if startX == endX 252 return null; 253 } 254 255 mFade.setInterpolator(DISAPPEAR_INTERPOLATOR); 256 final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues); 257 if (fadeAnimator == null) { 258 return null; 259 } 260 fadeAnimator.addListener(new AnimatorListenerAdapter() { 261 @Override 262 public void onAnimationEnd(Animator animator) { 263 fadeAnimator.removeListener(this); 264 view.setAlpha(0.0f); 265 } 266 }); 267 268 final AnimatorSet set = new AnimatorSet(); 269 set.play(slideAnimator).with(fadeAnimator); 270 Long delay = (Long) startValues.values.get(PROPNAME_DELAY); 271 if (delay != null) { 272 set.setStartDelay(delay); 273 } 274 return set; 275 } 276 277 @Override addListener(TransitionListener listener)278 public Transition addListener(TransitionListener listener) { 279 mFade.addListener(listener); 280 return super.addListener(listener); 281 } 282 283 @Override removeListener(TransitionListener listener)284 public Transition removeListener(TransitionListener listener) { 285 mFade.removeListener(listener); 286 return super.removeListener(listener); 287 } 288 289 @Override clone()290 public Transition clone() { 291 FadeAndShortSlide clone = (FadeAndShortSlide) super.clone(); 292 clone.mFade = (Visibility) mFade.clone(); 293 return clone; 294 } 295 296 @Override setDuration(long duration)297 public Transition setDuration(long duration) { 298 long scaledDuration = SetupAnimationHelper.applyAnimationTimeScale(duration); 299 mFade.setDuration(scaledDuration); 300 return super.setDuration(scaledDuration); 301 } 302 303 /** 304 * Sets the moving distance in pixel. 305 */ setDistance(int distance)306 public void setDistance(int distance) { 307 mDistance = distance; 308 } 309 310 private static class ViewPositionComparator implements Comparator<View> { 311 View mParentForDelay; 312 boolean mIsLtr; 313 boolean mToLeft; 314 315 @Override compare(View lhs, View rhs)316 public int compare(View lhs, View rhs) { 317 int start1; 318 int start2; 319 if (mIsLtr) { 320 start1 = getRelativeLeft(lhs, mParentForDelay); 321 start2 = getRelativeLeft(rhs, mParentForDelay); 322 } else { 323 start1 = getRelativeRight(lhs, mParentForDelay); 324 start2 = getRelativeRight(rhs, mParentForDelay); 325 } 326 if (mToLeft) { 327 if (start1 > start2) { 328 return 1; 329 } else if (start1 < start2) { 330 return -1; 331 } 332 } else { 333 if (start1 > start2) { 334 return -1; 335 } else if (start1 < start2) { 336 return 1; 337 } 338 } 339 int top1 = getRelativeTop(lhs, mParentForDelay); 340 int top2 = getRelativeTop(rhs, mParentForDelay); 341 return Integer.compare(top1, top2); 342 } 343 getRelativeLeft(View child, View ancestor)344 private int getRelativeLeft(View child, View ancestor) { 345 ViewParent parent = child.getParent(); 346 int left = child.getLeft(); 347 while (parent instanceof View) { 348 if (parent == ancestor) { 349 break; 350 } 351 left += ((View) parent).getLeft(); 352 parent = parent.getParent(); 353 } 354 return left; 355 } 356 getRelativeRight(View child, View ancestor)357 private int getRelativeRight(View child, View ancestor) { 358 ViewParent parent = child.getParent(); 359 int right = child.getRight(); 360 while (parent instanceof View) { 361 if (parent == ancestor) { 362 break; 363 } 364 right += ((View) parent).getLeft(); 365 parent = parent.getParent(); 366 } 367 return right; 368 } 369 getRelativeTop(View child, View ancestor)370 private int getRelativeTop(View child, View ancestor) { 371 ViewParent parent = child.getParent(); 372 int top = child.getTop(); 373 while (parent instanceof View) { 374 if (parent == ancestor) { 375 break; 376 } 377 top += ((View) parent).getTop(); 378 parent = parent.getParent(); 379 } 380 return top; 381 } 382 } 383 } 384