1 /* 2 * Copyright 2020 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.core.view; 17 18 import static androidx.core.view.WindowInsetsCompat.toWindowInsetsCompat; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ValueAnimator; 23 import android.annotation.SuppressLint; 24 import android.os.Build; 25 import android.util.Log; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.view.WindowInsets; 29 import android.view.WindowInsetsAnimation; 30 import android.view.animation.AccelerateInterpolator; 31 import android.view.animation.DecelerateInterpolator; 32 import android.view.animation.Interpolator; 33 import android.view.animation.PathInterpolator; 34 35 import androidx.annotation.FloatRange; 36 import androidx.annotation.IntDef; 37 import androidx.annotation.RequiresApi; 38 import androidx.annotation.RestrictTo; 39 import androidx.core.R; 40 import androidx.core.graphics.Insets; 41 import androidx.core.view.WindowInsetsCompat.Type; 42 import androidx.core.view.WindowInsetsCompat.Type.InsetsType; 43 import androidx.interpolator.view.animation.FastOutLinearInInterpolator; 44 45 import org.jspecify.annotations.NonNull; 46 import org.jspecify.annotations.Nullable; 47 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.Objects; 55 56 /** 57 * Class representing an animation of a set of windows that cause insets. 58 */ 59 public final class WindowInsetsAnimationCompat { 60 private static final boolean DEBUG = false; 61 private static final String TAG = "WindowInsetsAnimCompat"; 62 private Impl mImpl; 63 64 /** 65 * Creates a new {@link WindowInsetsAnimationCompat} object. 66 * <p> 67 * This should only be used for testing, as usually the system creates this object for the 68 * application to listen to with {@link WindowInsetsAnimationCompat.Callback}. 69 * </p> 70 * 71 * @param typeMask The bitmask of {@link WindowInsetsCompat.Type}s that are animating. 72 * @param interpolator The interpolator of the animation. 73 * @param durationMillis The duration of the animation in 74 * {@link java.util.concurrent.TimeUnit#MILLISECONDS}. 75 */ WindowInsetsAnimationCompat( @nsetsType int typeMask, @Nullable Interpolator interpolator, long durationMillis)76 public WindowInsetsAnimationCompat( 77 @InsetsType int typeMask, @Nullable Interpolator interpolator, 78 long durationMillis) { 79 if (Build.VERSION.SDK_INT >= 30) { 80 mImpl = new Impl30(typeMask, interpolator, durationMillis); 81 } else if (Build.VERSION.SDK_INT >= 21) { 82 mImpl = new Impl21(typeMask, interpolator, durationMillis); 83 } else { 84 mImpl = new Impl(0, interpolator, durationMillis); 85 } 86 } 87 88 @RequiresApi(30) WindowInsetsAnimationCompat(@onNull WindowInsetsAnimation animation)89 private WindowInsetsAnimationCompat(@NonNull WindowInsetsAnimation animation) { 90 this(0, null, 0); 91 if (Build.VERSION.SDK_INT >= 30) { 92 mImpl = new Impl30(animation); 93 } 94 } 95 96 /** 97 * @return The bitmask of {@link Type} that are animating. 98 */ 99 @InsetsType getTypeMask()100 public int getTypeMask() { 101 return mImpl.getTypeMask(); 102 } 103 104 /** 105 * Returns the raw fractional progress of this animation between 106 * start state of the animation and the end state of the animation. Note 107 * that this progress is the global progress of the animation, whereas 108 * {@link WindowInsetsAnimationCompat.Callback#onProgress} will only dispatch the insets that 109 * may be inset with {@link WindowInsetsCompat#inset} by parents of views in the hierarchy. 110 * Progress per insets animation is global for the entire animation. One animation animates 111 * all things together (in, out, ...). If they don't animate together, we'd have 112 * multiple animations. 113 * <p> 114 * Note: In case the application is controlling the animation, the valued returned here will 115 * be the same as the application passed into 116 * 117 * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha( 118 * androidx.core.graphics.Insets, float, float)}. 119 * </p> 120 * 121 * @return The current progress of this animation. 122 */ 123 @FloatRange(from = 0f, to = 1f) getFraction()124 public float getFraction() { 125 return mImpl.getFraction(); 126 } 127 128 /** 129 * Returns the interpolated fractional progress of this animation between 130 * start state of the animation and the end state of the animation. Note 131 * that this progress is the global progress of the animation, whereas 132 * {@link WindowInsetsAnimationCompat.Callback#onProgress} will only dispatch the 133 * insets that may 134 * be inset with {@link WindowInsetsCompat#inset} by parents of views in the hierarchy. 135 * Progress per insets animation is global for the entire animation. One animation animates 136 * all things together (in, out, ...). If they don't animate together, we'd have 137 * multiple animations. 138 * <p> 139 * Note: In case the application is controlling the animation, the valued returned here will 140 * be the same as the application passed into 141 * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}, 142 * interpolated with the interpolator passed into 143 * {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}. 144 * <p> 145 * Note: For system-initiated animations, this will always return a valid value between 0 146 * and 1. 147 * 148 * @return The current interpolated progress of this animation. 149 * @see #getFraction() for raw fraction. 150 */ getInterpolatedFraction()151 public float getInterpolatedFraction() { 152 return mImpl.getInterpolatedFraction(); 153 } 154 155 /** 156 * Retrieves the interpolator used for this animation, or {@code null} if this animation 157 * doesn't follow an interpolation curved. For system-initiated animations, this will never 158 * return {@code null}. 159 * 160 * @return The interpolator used for this animation. 161 */ getInterpolator()162 public @Nullable Interpolator getInterpolator() { 163 return mImpl.getInterpolator(); 164 } 165 166 /** 167 * @return duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or 168 * -1 if the animation doesn't have a fixed duration. 169 */ getDurationMillis()170 public long getDurationMillis() { 171 return mImpl.getDurationMillis(); 172 } 173 174 /** 175 * Set fraction of the progress if {@link Type} animation is controlled by the app. 176 * <p> 177 * Note: This should only be used for testing, as the system fills in the fraction for the 178 * application or the fraction that was passed into 179 * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)} is 180 * being used. 181 * 182 * @param fraction fractional progress between 0 and 1 where 0 represents hidden and 183 * zero progress and 1 represent fully shown final state. 184 * @see #getFraction() 185 */ setFraction(@loatRangefrom = 0f, to = 1f) float fraction)186 public void setFraction(@FloatRange(from = 0f, to = 1f) float fraction) { 187 mImpl.setFraction(fraction); 188 } 189 190 /** 191 * Retrieves the translucency of the windows that are animating. 192 * 193 * @return Alpha of windows that cause insets of type {@link Type}. 194 */ 195 @FloatRange(from = 0f, to = 1f) getAlpha()196 public float getAlpha() { 197 return mImpl.getAlpha(); 198 } 199 200 /** 201 * Sets the translucency of the windows that are animating. 202 * <p> 203 * Note: This should only be used for testing, as the system fills in the alpha for the 204 * application or the alpha that was passed into 205 * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)} is 206 * being used. 207 * 208 * @param alpha Alpha of windows that cause insets of type {@link Type}. 209 * @see #getAlpha() 210 */ setAlpha(@loatRangefrom = 0f, to = 1f) float alpha)211 public void setAlpha(@FloatRange(from = 0f, to = 1f) float alpha) { 212 mImpl.setAlpha(alpha); 213 } 214 215 /** 216 * Class representing the range of an {@link WindowInsetsAnimationCompat} 217 */ 218 public static final class BoundsCompat { 219 220 private final Insets mLowerBound; 221 private final Insets mUpperBound; 222 BoundsCompat(@onNull Insets lowerBound, @NonNull Insets upperBound)223 public BoundsCompat(@NonNull Insets lowerBound, @NonNull Insets upperBound) { 224 mLowerBound = lowerBound; 225 mUpperBound = upperBound; 226 } 227 228 @RequiresApi(30) BoundsCompat(WindowInsetsAnimation.@onNull Bounds bounds)229 private BoundsCompat(WindowInsetsAnimation.@NonNull Bounds bounds) { 230 mLowerBound = Impl30.getLowerBounds(bounds); 231 mUpperBound = Impl30.getHigherBounds(bounds); 232 } 233 234 /** 235 * Queries the lower inset bound of the animation. If the animation is about showing or 236 * hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper 237 * bound is the same as {@link WindowInsetsCompat#getInsets(int)} for the fully shown 238 * state. This 239 * is the same as {@link WindowInsetsAnimationControllerCompat#getHiddenStateInsets} and 240 * {@link WindowInsetsAnimationControllerCompat#getShownStateInsets} in case the listener 241 * gets invoked because of an animation that originates from 242 * {@link WindowInsetsAnimationControllerCompat}. 243 * <p> 244 * However, if the size of a window that causes insets is changing, these are the 245 * lower/upper bounds of that size animation. 246 * </p> 247 * There are no overlapping animations for a specific type, but there may be multiple 248 * animations running at the same time for different inset types. 249 * 250 * @see #getUpperBound() 251 * @see WindowInsetsAnimationControllerCompat#getHiddenStateInsets 252 */ getLowerBound()253 public @NonNull Insets getLowerBound() { 254 return mLowerBound; 255 } 256 257 /** 258 * Queries the upper inset bound of the animation. If the animation is about showing or 259 * hiding a window that cause insets, the lower bound is {@link Insets#NONE} nd the upper 260 * bound is the same as {@link WindowInsetsCompat#getInsets(int)} for the fully shown 261 * state. This is the same as 262 * {@link WindowInsetsAnimationControllerCompat#getHiddenStateInsets} and 263 * {@link WindowInsetsAnimationControllerCompat#getShownStateInsets} in case the listener 264 * gets invoked because of an animation that originates from 265 * {@link WindowInsetsAnimationControllerCompat}. 266 * <p> 267 * However, if the size of a window that causes insets is changing, these are the 268 * lower/upper bounds of that size animation. 269 * <p> 270 * There are no overlapping animations for a specific type, but there may be multiple 271 * animations running at the same time for different inset types. 272 * 273 * @see #getLowerBound() 274 * @see WindowInsetsAnimationControllerCompat#getShownStateInsets 275 */ getUpperBound()276 public @NonNull Insets getUpperBound() { 277 return mUpperBound; 278 } 279 280 /** 281 * Insets both the lower and upper bound by the specified insets. This is to be used in 282 * {@link WindowInsetsAnimationCompat.Callback#onStart} to indicate that a part of the 283 * insets has been used to offset or clip its children, and the children shouldn't worry 284 * about that part anymore. 285 * 286 * @param insets The amount to inset. 287 * @return A copy of this instance inset in the given directions. 288 * @see WindowInsetsCompat#inset 289 * @see WindowInsetsAnimationCompat.Callback#onStart 290 */ inset(@onNull Insets insets)291 public @NonNull BoundsCompat inset(@NonNull Insets insets) { 292 return new BoundsCompat( 293 // TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate 294 // place eventually. 295 WindowInsetsCompat.insetInsets( 296 mLowerBound, insets.left, insets.top, insets.right, insets.bottom), 297 WindowInsetsCompat.insetInsets( 298 mUpperBound, insets.left, insets.top, insets.right, insets.bottom)); 299 } 300 301 @Override toString()302 public String toString() { 303 return "Bounds{lower=" + mLowerBound + " upper=" + mUpperBound + "}"; 304 } 305 306 /** 307 * Creates a new instance of {@link WindowInsetsAnimation.Bounds} from this compat instance. 308 */ 309 @RequiresApi(30) toBounds()310 public WindowInsetsAnimation.@NonNull Bounds toBounds() { 311 return Impl30.createPlatformBounds(this); 312 } 313 314 /** 315 * Create a new insance of {@link BoundsCompat} using the provided 316 * platform {@link android.view.WindowInsetsAnimation.Bounds}. 317 */ 318 @RequiresApi(30) toBoundsCompat( WindowInsetsAnimation.@onNull Bounds bounds)319 public static @NonNull BoundsCompat toBoundsCompat( 320 WindowInsetsAnimation.@NonNull Bounds bounds) { 321 return new BoundsCompat(bounds); 322 } 323 } 324 325 @RequiresApi(30) toWindowInsetsAnimationCompat( WindowInsetsAnimation windowInsetsAnimation)326 static WindowInsetsAnimationCompat toWindowInsetsAnimationCompat( 327 WindowInsetsAnimation windowInsetsAnimation) { 328 return new WindowInsetsAnimationCompat(windowInsetsAnimation); 329 } 330 331 /** 332 * Interface that allows the application to listen to animation events for windows that cause 333 * insets. 334 */ 335 public abstract static class Callback { 336 337 /** 338 * Return value for {@link #getDispatchMode()}: Dispatching of animation events should 339 * stop at this level in the view hierarchy, and no animation events should be dispatch to 340 * the subtree of the view hierarchy. 341 */ 342 public static final int DISPATCH_MODE_STOP = 0; 343 344 /** 345 * Return value for {@link #getDispatchMode()}: Dispatching of animation events should 346 * continue in the view hierarchy. 347 */ 348 public static final int DISPATCH_MODE_CONTINUE_ON_SUBTREE = 1; 349 WindowInsetsCompat mDispachedInsets; 350 351 @IntDef(value = { 352 DISPATCH_MODE_STOP, 353 DISPATCH_MODE_CONTINUE_ON_SUBTREE 354 }) 355 @Retention(RetentionPolicy.SOURCE) 356 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 357 public @interface DispatchMode { 358 } 359 360 @DispatchMode 361 private final int mDispatchMode; 362 363 /** 364 * Creates a new {@link WindowInsetsAnimationCompat} callback with the given 365 * {@link #getDispatchMode() dispatch mode}. 366 * 367 * @param dispatchMode The dispatch mode for this callback. See {@link #getDispatchMode()}. 368 */ Callback(@ispatchMode int dispatchMode)369 public Callback(@DispatchMode int dispatchMode) { 370 mDispatchMode = dispatchMode; 371 } 372 373 /** 374 * Retrieves the dispatch mode of this listener. Dispatch of the all animation events is 375 * hierarchical: It will starts at the root of the view hierarchy and then traverse it and 376 * invoke the callback of the specific {@link View} that is being traversed. 377 * The method may return either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that 378 * animation events should be propagated to the subtree of the view hierarchy, or 379 * {@link #DISPATCH_MODE_STOP} to stop dispatching. In that case, all animation callbacks 380 * related to the animation passed in will be stopped from propagating to the subtree of the 381 * hierarchy. 382 * <p> 383 * Also note that {@link #DISPATCH_MODE_STOP} behaves the same way as 384 * returning {@link WindowInsetsCompat#CONSUMED} during the regular insets dispatch in 385 * {@link View#onApplyWindowInsets}. 386 * 387 * @return Either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that dispatching of 388 * animation events will continue to the subtree of the view hierarchy, or 389 * {@link #DISPATCH_MODE_STOP} to indicate that animation events will stop 390 * dispatching. 391 */ 392 @DispatchMode getDispatchMode()393 public final int getDispatchMode() { 394 return mDispatchMode; 395 } 396 397 /** 398 * Called when an insets animation is about to start and before the views have been 399 * re-laid out due to an animation. 400 * <p> 401 * This ordering allows the application to inspect the end state after the animation has 402 * finished, and then revert to the starting state of the animation in the first 403 * {@link #onProgress} callback by using post-layout view properties like {@link View#setX} 404 * and related methods. 405 * <p> 406 * The ordering of events during an insets animation is 407 * the following: 408 * <ul> 409 * <li>Application calls {@link WindowInsetsControllerCompat#hide(int)}, 410 * {@link WindowInsetsControllerCompat#show(int)}, 411 * {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}</li> 412 * <li>onPrepare is called on the view hierarchy listeners</li> 413 * <li>{@link View#onApplyWindowInsets} will be called with the end state of the 414 * animation</li> 415 * <li>View hierarchy gets laid out according to the changes the application has 416 * requested due to the new insets being dispatched</li> 417 * <li>{@link #onStart} is called <em>before</em> the view 418 * hierarchy gets drawn in the new laid out state</li> 419 * <li>{@link #onProgress} is called immediately after with the animation start 420 * state</li> 421 * <li>The frame gets drawn.</li> 422 * </ul> 423 * <p> 424 * Note: If the animation is application controlled by using 425 * {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}, the end state of 426 * the animation is undefined as the application may decide on the end state only by 427 * passing in {@code shown} parameter when calling 428 * {@link WindowInsetsAnimationControllerCompat#finish}. In this situation, the system 429 * will dispatch the insets in the opposite visibility state before the animation starts. 430 * Example: When controlling the input method with 431 * {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation} and the input method 432 * is currently showing, {@link View#onApplyWindowInsets} will receive a 433 * {@link WindowInsetsCompat} instance for which {@link WindowInsetsCompat#isVisible} 434 * will return {@code false} for {@link WindowInsetsCompat.Type#ime}. 435 * 436 * @param animation The animation that is about to start. 437 */ onPrepare(@onNull WindowInsetsAnimationCompat animation)438 public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) { 439 } 440 441 /** 442 * Called when an insets animation gets started. 443 * <p> 444 * This ordering allows the application to inspect the end state after the animation has 445 * finished, and then revert to the starting state of the animation in the first 446 * {@link #onProgress} callback by using post-layout view properties like {@link View#setX} 447 * and related methods. 448 * <p> 449 * The ordering of events during an insets animation is 450 * the following: 451 * <ul> 452 * <li>Application calls {@link WindowInsetsControllerCompat#hide(int)}, 453 * {@link WindowInsetsControllerCompat#show(int)}, 454 * {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}</li> 455 * <li>onPrepare is called on the view hierarchy listeners</li> 456 * <li>{@link View#onApplyWindowInsets} will be called with the end state of the 457 * animation</li> 458 * <li>View hierarchy gets laid out according to the changes the application has 459 * requested due to the new insets being dispatched</li> 460 * <li>{@link #onStart} is called <em>before</em> the view 461 * hierarchy gets drawn in the new laid out state</li> 462 * <li>{@link #onProgress} is called immediately after with the animation start 463 * state</li> 464 * <li>The frame gets drawn.</li> 465 * </ul> 466 * <p> 467 * Note that, like {@link #onProgress}, dispatch of the animation start event is 468 * hierarchical: It will starts at the root of the view hierarchy and then traverse it 469 * and invoke the callback of the specific {@link View} that is being traversed. The 470 * method may return a modified instance of the bounds by calling 471 * {@link BoundsCompat#inset} to indicate that a part of the insets 472 * have been used to offset or clip its children, and the children shouldn't worry about 473 * that part anymore. Furthermore, if {@link #getDispatchMode()} returns 474 * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore. 475 * 476 * @param animation The animation that is about to start. 477 * @param bounds The bounds in which animation happens. 478 * @return The animation bounds representing the part of the insets that should be 479 * dispatched to 480 * the subtree of the hierarchy. 481 */ onStart( @onNull WindowInsetsAnimationCompat animation, @NonNull BoundsCompat bounds)482 public @NonNull BoundsCompat onStart( 483 @NonNull WindowInsetsAnimationCompat animation, 484 @NonNull BoundsCompat bounds) { 485 return bounds; 486 } 487 488 /** 489 * Called when the insets change as part of running an animation. Note that even if multiple 490 * animations for different types are running, there will only be one progress callback per 491 * frame. The {@code insets} passed as an argument represents the overall state and will 492 * include all types, regardless of whether they are animating or not. 493 * <p> 494 * Note that insets dispatch is hierarchical: It will start at the root of the view 495 * hierarchy, and then traverse it and invoke the callback of the specific {@link View} 496 * being traversed. The method may return a modified instance by calling 497 * {@link WindowInsetsCompat#inset(int, int, int, int)} to indicate that a part of the 498 * insets have been used to offset or clip its children, and the children shouldn't worry 499 * about that part anymore. Furthermore, if {@link #getDispatchMode()} returns 500 * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore. 501 * 502 * @param insets The current insets. 503 * @param runningAnimations The currently running animations. 504 * @return The insets to dispatch to the subtree of the hierarchy. 505 */ onProgress(@onNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations)506 public abstract @NonNull WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, 507 @NonNull List<WindowInsetsAnimationCompat> runningAnimations); 508 509 /** 510 * Called when an insets animation has ended. 511 * 512 * @param animation The animation that has ended. This will be the same instance 513 * as passed into {@link #onStart} 514 */ onEnd(@onNull WindowInsetsAnimationCompat animation)515 public void onEnd(@NonNull WindowInsetsAnimationCompat animation) { 516 } 517 } 518 setCallback(@onNull View view, @Nullable Callback callback)519 static void setCallback(@NonNull View view, @Nullable Callback callback) { 520 if (Build.VERSION.SDK_INT >= 30) { 521 Impl30.setCallback(view, callback); 522 } else if (Build.VERSION.SDK_INT >= 21) { 523 Impl21.setCallback(view, callback); 524 } 525 // Do nothing pre 21 526 } 527 528 private static class Impl { 529 @InsetsType 530 private final int mTypeMask; 531 private float mFraction; 532 private final @Nullable Interpolator mInterpolator; 533 private final long mDurationMillis; 534 private float mAlpha = 1f; 535 Impl(int typeMask, @Nullable Interpolator interpolator, long durationMillis)536 Impl(int typeMask, @Nullable Interpolator interpolator, long durationMillis) { 537 mTypeMask = typeMask; 538 mInterpolator = interpolator; 539 mDurationMillis = durationMillis; 540 } 541 getTypeMask()542 public int getTypeMask() { 543 return mTypeMask; 544 } 545 getFraction()546 public float getFraction() { 547 return mFraction; 548 } 549 getInterpolatedFraction()550 public float getInterpolatedFraction() { 551 if (mInterpolator != null) { 552 return mInterpolator.getInterpolation(mFraction); 553 } 554 return mFraction; 555 } 556 getInterpolator()557 public @Nullable Interpolator getInterpolator() { 558 return mInterpolator; 559 } 560 getDurationMillis()561 public long getDurationMillis() { 562 return mDurationMillis; 563 } 564 getAlpha()565 public float getAlpha() { 566 return mAlpha; 567 } 568 setFraction(float fraction)569 public void setFraction(float fraction) { 570 mFraction = fraction; 571 } 572 setAlpha(float alpha)573 public void setAlpha(float alpha) { 574 mAlpha = alpha; 575 } 576 577 } 578 579 @RequiresApi(21) 580 private static class Impl21 extends Impl { 581 582 /** 583 * A fixed interpolator to use when simulating the window insets animation for showing the 584 * IME. 585 * 586 * This interpolator was picked via experimentation to subjectively improve the end result. 587 */ 588 private static final Interpolator SHOW_IME_INTERPOLATOR = 589 new PathInterpolator(0, 1.1f, 0f, 1f); 590 591 /** 592 * A fixed interpolator to use when simulating the window insets animation for hiding the 593 * IME. 594 */ 595 private static final Interpolator HIDE_IME_INTERPOLATOR = 596 new FastOutLinearInInterpolator(); 597 598 /** 599 * A fixed interpolator to use when simulating the window insets animation for showing 600 * system bars. 601 * 602 * This interpolator and the factor was to align with the legacy animation described in 603 * dock_[side]_enter.xml before API 30. 604 */ 605 private static final Interpolator SHOW_SYSTEM_BAR_INTERPOLATOR = 606 new DecelerateInterpolator(1.5f /* factor */); 607 608 /** 609 * A fixed interpolator to use when simulating the window insets animation for hiding 610 * system bars. 611 * 612 * This interpolator and the factor was to align with the legacy animation described in 613 * dock_[side]_exit.xml before API 30. 614 */ 615 private static final Interpolator HIDE_SYSTEM_BAR_INTERPOLATOR = 616 new AccelerateInterpolator(1.5f /* factor */); 617 Impl21(int typeMask, @Nullable Interpolator interpolator, long durationMillis)618 Impl21(int typeMask, @Nullable Interpolator interpolator, long durationMillis) { 619 super(typeMask, interpolator, durationMillis); 620 } 621 setCallback(final @NonNull View view, final @Nullable Callback callback)622 static void setCallback(final @NonNull View view, 623 final @Nullable Callback callback) { 624 final View.OnApplyWindowInsetsListener proxyListener = callback != null 625 ? createProxyListener(view, callback) 626 : null; 627 view.setTag(R.id.tag_window_insets_animation_callback, proxyListener); 628 629 // We rely on View.OnApplyWindowInsetsListener, but one might already be set by the 630 // library, so we only register it on the view if none is set yet. 631 // If any of them is set via ViewGroupCompat#installCompatInsetsDispatch or 632 // ViewCompat.setOnApplyWindowInsetsListener, this Callback will be called by their 633 // listener. 634 if (view.getTag(R.id.tag_compat_insets_dispatch) == null 635 && view.getTag(R.id.tag_on_apply_window_listener) == null) { 636 view.setOnApplyWindowInsetsListener(proxyListener); 637 } 638 } 639 createProxyListener( @onNull View view, final @NonNull Callback callback)640 private static View.@NonNull OnApplyWindowInsetsListener createProxyListener( 641 @NonNull View view, final @NonNull Callback callback) { 642 return new Impl21OnApplyWindowInsetsListener(view, callback); 643 } 644 computeAnimationBounds( @onNull WindowInsetsCompat targetInsets, @NonNull WindowInsetsCompat startingInsets, int mask)645 static @NonNull BoundsCompat computeAnimationBounds( 646 @NonNull WindowInsetsCompat targetInsets, 647 @NonNull WindowInsetsCompat startingInsets, int mask) { 648 Insets targetInsetsInsets = targetInsets.getInsets(mask); 649 Insets startingInsetsInsets = startingInsets.getInsets(mask); 650 final Insets lowerBound = Insets.of( 651 Math.min(targetInsetsInsets.left, startingInsetsInsets.left), 652 Math.min(targetInsetsInsets.top, startingInsetsInsets.top), 653 Math.min(targetInsetsInsets.right, startingInsetsInsets.right), 654 Math.min(targetInsetsInsets.bottom, startingInsetsInsets.bottom) 655 ); 656 final Insets upperBound = Insets.of( 657 Math.max(targetInsetsInsets.left, startingInsetsInsets.left), 658 Math.max(targetInsetsInsets.top, startingInsetsInsets.top), 659 Math.max(targetInsetsInsets.right, startingInsetsInsets.right), 660 Math.max(targetInsetsInsets.bottom, startingInsetsInsets.bottom) 661 ); 662 return new BoundsCompat(lowerBound, upperBound); 663 } 664 665 @SuppressLint("WrongConstant") // We iterate over all the constants. buildAnimationMask(@onNull WindowInsetsCompat targetInsets, @NonNull WindowInsetsCompat currentInsets, int[] showingTypes, int[] hidingTypes)666 static void buildAnimationMask(@NonNull WindowInsetsCompat targetInsets, 667 @NonNull WindowInsetsCompat currentInsets, int[] showingTypes, int[] hidingTypes) { 668 for (int i = WindowInsetsCompat.Type.FIRST; i <= WindowInsetsCompat.Type.LAST; 669 i = i << 1) { 670 final Insets target = targetInsets.getInsets(i); 671 final Insets current = currentInsets.getInsets(i); 672 final boolean showing = target.left > current.left 673 || target.top > current.top 674 || target.right > current.right 675 || target.bottom > current.bottom; 676 final boolean hiding = target.left < current.left 677 || target.top < current.top 678 || target.right < current.right 679 || target.bottom < current.bottom; 680 // If both showing and hiding are true, it can be the side change of navigation bar. 681 // Don't consider that it is playing an animation. 682 if (showing != hiding) { 683 if (showing) { 684 showingTypes[0] |= i; 685 } else { 686 hidingTypes[0] |= i; 687 } 688 } 689 } 690 } 691 692 /** 693 * Determine which interpolator to use based on which insets are being animated. 694 * 695 * This allows for a smoother animation especially in the common case of showing and hiding 696 * the IME. 697 */ 698 static @Nullable Interpolator createInsetInterpolator(int showingTypes, int hidingTypes) { 699 if ((showingTypes & WindowInsetsCompat.Type.ime()) != 0) { 700 return SHOW_IME_INTERPOLATOR; 701 } else if ((hidingTypes & WindowInsetsCompat.Type.ime()) != 0) { 702 return HIDE_IME_INTERPOLATOR; 703 } else if ((showingTypes & WindowInsetsCompat.Type.systemBars()) != 0) { 704 return SHOW_SYSTEM_BAR_INTERPOLATOR; 705 } else if ((hidingTypes & WindowInsetsCompat.Type.systemBars()) != 0) { 706 return HIDE_SYSTEM_BAR_INTERPOLATOR; 707 } 708 return null; 709 } 710 711 @SuppressLint("WrongConstant") 712 static WindowInsetsCompat interpolateInsets( 713 WindowInsetsCompat target, WindowInsetsCompat starting, 714 float fraction, int typeMask) { 715 WindowInsetsCompat.Builder builder = new WindowInsetsCompat.Builder(target); 716 for (int i = WindowInsetsCompat.Type.FIRST; i <= WindowInsetsCompat.Type.LAST; 717 i = i << 1) { 718 if ((typeMask & i) == 0) { 719 builder.setInsets(i, target.getInsets(i)); 720 continue; 721 } 722 Insets targetInsets = target.getInsets(i); 723 Insets startingInsets = starting.getInsets(i); 724 Insets interpolatedInsets = WindowInsetsCompat.insetInsets( 725 targetInsets, 726 (int) (0.5 + (targetInsets.left - startingInsets.left) * (1 - fraction)), 727 (int) (0.5 + (targetInsets.top - startingInsets.top) * (1 - fraction)), 728 (int) (0.5 + (targetInsets.right - startingInsets.right) * (1 - fraction)), 729 (int) (0.5 + (targetInsets.bottom - startingInsets.bottom) * (1 - fraction)) 730 731 ); 732 builder.setInsets(i, interpolatedInsets); 733 } 734 735 return builder.build(); 736 } 737 738 /** 739 * Wrapper class around a {@link Callback} that will trigger the callback when 740 * {@link View#onApplyWindowInsets(WindowInsets)} is called 741 */ 742 @RequiresApi(21) 743 private static class Impl21OnApplyWindowInsetsListener implements 744 View.OnApplyWindowInsetsListener { 745 746 private static final int COMPAT_ANIMATION_DURATION_IME = 160; 747 private static final int COMPAT_ANIMATION_DURATION_SYSTEM_BAR = 250; 748 749 final Callback mCallback; 750 // We save the last insets to compute the starting insets for the animation. 751 private WindowInsetsCompat mLastInsets; 752 753 Impl21OnApplyWindowInsetsListener(@NonNull View view, @NonNull Callback callback) { 754 mCallback = callback; 755 WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); 756 mLastInsets = rootWindowInsets != null 757 // Insets are not immutable on SDK < 26 so we make copy to ensure it's not 758 // changed until we need them. 759 ? new WindowInsetsCompat.Builder(rootWindowInsets).build() 760 : null; 761 } 762 763 @Override 764 public @NonNull WindowInsets onApplyWindowInsets(final View v, 765 @NonNull WindowInsets insets) { 766 // We cannot rely on the compat insets value until the view is laid out. 767 if (!v.isLaidOut()) { 768 mLastInsets = toWindowInsetsCompat(insets, v); 769 return forwardToViewIfNeeded(v, insets); 770 } 771 772 final WindowInsetsCompat targetInsets = toWindowInsetsCompat(insets, v); 773 774 if (mLastInsets == null) { 775 mLastInsets = ViewCompat.getRootWindowInsets(v); 776 } 777 778 if (mLastInsets == null) { 779 if (DEBUG) { 780 Log.d(TAG, "Couldn't initialize last insets"); 781 } 782 mLastInsets = targetInsets; 783 return forwardToViewIfNeeded(v, insets); 784 } 785 786 if (DEBUG) { 787 int allTypes = WindowInsetsCompat.Type.all(); 788 Log.d(TAG, String.format("lastInsets: %s\ntargetInsets: %s", 789 mLastInsets.getInsets(allTypes), 790 targetInsets.getInsets(allTypes))); 791 } 792 793 // When we start dispatching the insets animation, we save the instance of insets 794 // that have been dispatched first as a marker to avoid dispatching the callback 795 // in children. 796 Callback callback = getCallback(v); 797 if (callback != null && Objects.equals(callback.mDispachedInsets, targetInsets)) { 798 return forwardToViewIfNeeded(v, insets); 799 } 800 801 // We only run the animation when the some insets are animating 802 final int[] showingTypes = new int[1]; 803 final int[] hidingTypes = new int[1]; 804 buildAnimationMask(targetInsets, mLastInsets, showingTypes, hidingTypes); 805 final int animationMask = showingTypes[0] | hidingTypes[0]; 806 807 if (animationMask == 0) { 808 if (DEBUG) { 809 Log.d(TAG, "Insets applied but no window animation to run"); 810 } 811 mLastInsets = targetInsets; 812 return forwardToViewIfNeeded(v, insets); 813 } 814 815 final WindowInsetsCompat startingInsets = this.mLastInsets; 816 817 final Interpolator interpolator = createInsetInterpolator( 818 showingTypes[0], hidingTypes[0]); 819 820 final WindowInsetsAnimationCompat anim = 821 new WindowInsetsAnimationCompat(animationMask, interpolator, 822 (animationMask & WindowInsetsCompat.Type.ime()) != 0 823 ? COMPAT_ANIMATION_DURATION_IME 824 : COMPAT_ANIMATION_DURATION_SYSTEM_BAR); 825 anim.setFraction(0); 826 827 final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration( 828 anim.getDurationMillis()); 829 830 // Compute the bounds of the animation 831 final BoundsCompat animationBounds = computeAnimationBounds(targetInsets, 832 startingInsets, animationMask 833 ); 834 835 dispatchOnPrepare(v, anim, targetInsets, false); 836 837 animator.addUpdateListener( 838 new ValueAnimator.AnimatorUpdateListener() { 839 @Override 840 public void onAnimationUpdate(ValueAnimator animator) { 841 anim.setFraction(animator.getAnimatedFraction()); 842 WindowInsetsCompat interpolateInsets = interpolateInsets( 843 targetInsets, 844 startingInsets, 845 anim.getInterpolatedFraction(), animationMask); 846 List<WindowInsetsAnimationCompat> runningAnimations = 847 Collections.singletonList(anim); 848 dispatchOnProgress(v, interpolateInsets, runningAnimations); 849 } 850 }); 851 852 animator.addListener(new AnimatorListenerAdapter() { 853 854 @Override 855 public void onAnimationEnd(Animator animator) { 856 anim.setFraction(1); 857 dispatchOnEnd(v, anim); 858 } 859 }); 860 861 // We need to call onStart and start the animator before the next draw 862 // to ensure the animation starts before the relayout caused by the change of 863 // insets. 864 OneShotPreDrawListener.add(v, new Runnable() { 865 @Override 866 public void run() { 867 dispatchOnStart(v, anim, animationBounds); 868 animator.start(); 869 } 870 }); 871 this.mLastInsets = targetInsets; 872 873 return forwardToViewIfNeeded(v, insets); 874 } 875 } 876 877 /** 878 * Forward the call to view.onApplyWindowInsets if there is no other listener attached to 879 * the view. 880 */ 881 static @NonNull WindowInsets forwardToViewIfNeeded(@NonNull View v, 882 @NonNull WindowInsets insets) { 883 // If the app set an on apply window listener, it will be called after this 884 // and will decide whether to call the view's onApplyWindowInsets. 885 if (v.getTag(R.id.tag_on_apply_window_listener) != null) { 886 return insets; 887 } 888 return v.onApplyWindowInsets(insets); 889 } 890 891 static void dispatchOnPrepare(View v, WindowInsetsAnimationCompat anim, 892 WindowInsetsCompat insets, boolean stopDispatch) { 893 final Callback callback = getCallback(v); 894 if (callback != null) { 895 callback.mDispachedInsets = insets; 896 if (!stopDispatch) { 897 callback.onPrepare(anim); 898 stopDispatch = callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP; 899 } 900 } 901 // When stopDispatch is true, we don't call onPrepare but we still need to propagate 902 // the dispatched insets to the children to mark them with the latest dispatched 903 // insets so their compat callback in not called when onApplyWindowInsets is called. 904 if (v instanceof ViewGroup) { 905 ViewGroup viewGroup = (ViewGroup) v; 906 for (int i = 0; i < viewGroup.getChildCount(); i++) { 907 View child = viewGroup.getChildAt(i); 908 dispatchOnPrepare(child, anim, insets, stopDispatch); 909 } 910 } 911 } 912 913 static void dispatchOnStart(View v, 914 WindowInsetsAnimationCompat anim, 915 BoundsCompat animationBounds) { 916 final Callback callback = getCallback(v); 917 if (callback != null) { 918 callback.onStart(anim, animationBounds); 919 if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) { 920 return; 921 } 922 } 923 if (v instanceof ViewGroup) { 924 ViewGroup viewGroup = (ViewGroup) v; 925 for (int i = 0; i < viewGroup.getChildCount(); i++) { 926 View child = viewGroup.getChildAt(i); 927 dispatchOnStart(child, anim, animationBounds); 928 } 929 } 930 } 931 932 static void dispatchOnProgress(@NonNull View v, 933 @NonNull WindowInsetsCompat interpolateInsets, 934 @NonNull List<WindowInsetsAnimationCompat> runningAnimations) { 935 final Callback callback = getCallback(v); 936 WindowInsetsCompat insets = interpolateInsets; 937 if (callback != null) { 938 insets = callback.onProgress(insets, runningAnimations); 939 if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) { 940 return; 941 } 942 } 943 if (v instanceof ViewGroup) { 944 ViewGroup viewGroup = (ViewGroup) v; 945 for (int i = 0; i < viewGroup.getChildCount(); i++) { 946 View child = viewGroup.getChildAt(i); 947 dispatchOnProgress(child, insets, runningAnimations); 948 } 949 } 950 } 951 952 static void dispatchOnEnd(@NonNull View v, 953 @NonNull WindowInsetsAnimationCompat anim) { 954 final Callback callback = getCallback(v); 955 if (callback != null) { 956 callback.onEnd(anim); 957 if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) { 958 return; 959 } 960 } 961 if (v instanceof ViewGroup) { 962 ViewGroup viewGroup = (ViewGroup) v; 963 for (int i = 0; i < viewGroup.getChildCount(); i++) { 964 View child = viewGroup.getChildAt(i); 965 dispatchOnEnd(child, anim); 966 } 967 } 968 } 969 970 static @Nullable Callback getCallback(View child) { 971 Object listener = child.getTag( 972 R.id.tag_window_insets_animation_callback); 973 Callback callback = null; 974 if (listener instanceof Impl21OnApplyWindowInsetsListener) { 975 callback = ((Impl21OnApplyWindowInsetsListener) listener).mCallback; 976 } 977 return callback; 978 } 979 } 980 981 @RequiresApi(30) 982 private static class Impl30 extends Impl { 983 984 private final @NonNull WindowInsetsAnimation mWrapped; 985 986 Impl30(@NonNull WindowInsetsAnimation wrapped) { 987 super(0, null, 0); 988 mWrapped = wrapped; 989 } 990 991 Impl30(int typeMask, Interpolator interpolator, long durationMillis) { 992 this(new WindowInsetsAnimation(typeMask, interpolator, durationMillis)); 993 } 994 995 @Override 996 public int getTypeMask() { 997 return mWrapped.getTypeMask(); 998 } 999 1000 @Override 1001 public @Nullable Interpolator getInterpolator() { 1002 return mWrapped.getInterpolator(); 1003 } 1004 1005 @Override 1006 public long getDurationMillis() { 1007 return mWrapped.getDurationMillis(); 1008 } 1009 1010 @Override 1011 public float getFraction() { 1012 return mWrapped.getFraction(); 1013 } 1014 1015 @Override 1016 public void setFraction(float fraction) { 1017 mWrapped.setFraction(fraction); 1018 } 1019 1020 @Override 1021 public float getInterpolatedFraction() { 1022 return mWrapped.getInterpolatedFraction(); 1023 } 1024 1025 @Override 1026 public float getAlpha() { 1027 return mWrapped.getAlpha(); 1028 } 1029 1030 @Override 1031 public void setAlpha(float alpha) { 1032 mWrapped.setAlpha(alpha); 1033 } 1034 1035 @RequiresApi(30) 1036 private static class ProxyCallback extends WindowInsetsAnimation.Callback { 1037 1038 private final Callback mCompat; 1039 1040 ProxyCallback(final WindowInsetsAnimationCompat.@NonNull Callback compat) { 1041 super(compat.getDispatchMode()); 1042 mCompat = compat; 1043 } 1044 1045 private List<WindowInsetsAnimationCompat> mRORunningAnimations; 1046 private ArrayList<WindowInsetsAnimationCompat> mTmpRunningAnimations; 1047 private final HashMap<WindowInsetsAnimation, WindowInsetsAnimationCompat> 1048 mAnimations = new HashMap<>(); 1049 1050 private @NonNull WindowInsetsAnimationCompat getWindowInsetsAnimationCompat( 1051 @NonNull WindowInsetsAnimation animation) { 1052 WindowInsetsAnimationCompat animationCompat = mAnimations.get( 1053 animation); 1054 if (animationCompat == null) { 1055 animationCompat = toWindowInsetsAnimationCompat(animation); 1056 mAnimations.put(animation, animationCompat); 1057 } 1058 return animationCompat; 1059 } 1060 1061 @Override 1062 public void onPrepare(@NonNull WindowInsetsAnimation animation) { 1063 mCompat.onPrepare(getWindowInsetsAnimationCompat(animation)); 1064 } 1065 1066 @Override 1067 public WindowInsetsAnimation.@NonNull Bounds onStart( 1068 @NonNull WindowInsetsAnimation animation, 1069 WindowInsetsAnimation.@NonNull Bounds bounds) { 1070 return mCompat.onStart( 1071 getWindowInsetsAnimationCompat(animation), 1072 BoundsCompat.toBoundsCompat(bounds)).toBounds(); 1073 } 1074 1075 @Override 1076 public @NonNull WindowInsets onProgress(@NonNull WindowInsets insets, 1077 @NonNull List<WindowInsetsAnimation> runningAnimations) { 1078 if (mTmpRunningAnimations == null) { 1079 mTmpRunningAnimations = new ArrayList<>(runningAnimations.size()); 1080 mRORunningAnimations = Collections.unmodifiableList(mTmpRunningAnimations); 1081 } else { 1082 mTmpRunningAnimations.clear(); 1083 } 1084 1085 for (int i = runningAnimations.size() - 1; i >= 0; i--) { 1086 WindowInsetsAnimation animation = runningAnimations.get(i); 1087 WindowInsetsAnimationCompat animationCompat = 1088 getWindowInsetsAnimationCompat(animation); 1089 animationCompat.setFraction(animation.getFraction()); 1090 mTmpRunningAnimations.add(animationCompat); 1091 } 1092 return mCompat.onProgress( 1093 WindowInsetsCompat.toWindowInsetsCompat(insets), 1094 mRORunningAnimations).toWindowInsets(); 1095 } 1096 1097 @Override 1098 public void onEnd(@NonNull WindowInsetsAnimation animation) { 1099 mCompat.onEnd(getWindowInsetsAnimationCompat(animation)); 1100 mAnimations.remove(animation); 1101 } 1102 } 1103 1104 public static void setCallback(@NonNull View view, @Nullable Callback callback) { 1105 WindowInsetsAnimation.Callback platformCallback = 1106 callback != null ? new ProxyCallback(callback) : null; 1107 view.setWindowInsetsAnimationCallback(platformCallback); 1108 } 1109 1110 public static WindowInsetsAnimation.@NonNull Bounds createPlatformBounds( 1111 @NonNull BoundsCompat bounds) { 1112 return new WindowInsetsAnimation.Bounds(bounds.getLowerBound().toPlatformInsets(), 1113 bounds.getUpperBound().toPlatformInsets()); 1114 } 1115 1116 public static @NonNull Insets getLowerBounds(WindowInsetsAnimation.@NonNull Bounds bounds) { 1117 return Insets.toCompatInsets(bounds.getLowerBound()); 1118 } 1119 1120 public static @NonNull Insets getHigherBounds( 1121 WindowInsetsAnimation.@NonNull Bounds bounds) { 1122 return Insets.toCompatInsets(bounds.getUpperBound()); 1123 } 1124 } 1125 } 1126