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