/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.view;
import static androidx.core.view.WindowInsetsCompat.toWindowInsetsCompat;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.R;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat.Type;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
/**
* Class representing an animation of a set of windows that cause insets.
*/
public final class WindowInsetsAnimationCompat {
private static final boolean DEBUG = false;
private static final String TAG = "WindowInsetsAnimCompat";
private Impl mImpl;
/**
* Creates a new {@link WindowInsetsAnimationCompat} object.
* <p>
* This should only be used for testing, as usually the system creates this object for the
* application to listen to with {@link WindowInsetsAnimationCompat.Callback}.
* </p>
*
* @param typeMask The bitmask of {@link WindowInsetsCompat.Type}s that are animating.
* @param interpolator The interpolator of the animation.
* @param durationMillis The duration of the animation in
* {@link java.util.concurrent.TimeUnit#MILLISECONDS}.
*/
public WindowInsetsAnimationCompat(
@InsetsType int typeMask, @Nullable Interpolator interpolator,
long durationMillis) {
if (Build.VERSION.SDK_INT >= 30) {
mImpl = new Impl30(typeMask, interpolator, durationMillis);
} else if (Build.VERSION.SDK_INT >= 21) {
mImpl = new Impl21(typeMask, interpolator, durationMillis);
} else {
mImpl = new Impl(0, interpolator, durationMillis);
}
}
@RequiresApi(30)
private WindowInsetsAnimationCompat(@NonNull WindowInsetsAnimation animation) {
this(0, null, 0);
if (Build.VERSION.SDK_INT >= 30) {
mImpl = new Impl30(animation);
}
}
/**
* @return The bitmask of {@link Type} that are animating.
*/
@InsetsType
public int getTypeMask() {
return mImpl.getTypeMask();
}
/**
* Returns the raw fractional progress of this animation between
* start state of the animation and the end state of the animation. Note
* that this progress is the global progress of the animation, whereas
* {@link WindowInsetsAnimationCompat.Callback#onProgress} will only dispatch the insets that
* may be inset with {@link WindowInsetsCompat#inset} by parents of views in the hierarchy.
* Progress per insets animation is global for the entire animation. One animation animates
* all things together (in, out, ...). If they don't animate together, we'd have
* multiple animations.
* <p>
* Note: In case the application is controlling the animation, the valued returned here will
* be the same as the application passed into
*
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(
* androidx.core.graphics.Insets, float, float)}.
* </p>
*
* @return The current progress of this animation.
*/
@FloatRange(from = 0f, to = 1f)
public float getFraction() {
return mImpl.getFraction();
}
/**
* Returns the interpolated fractional progress of this animation between
* start state of the animation and the end state of the animation. Note
* that this progress is the global progress of the animation, whereas
* {@link WindowInsetsAnimationCompat.Callback#onProgress} will only dispatch the
* insets that may
* be inset with {@link WindowInsetsCompat#inset} by parents of views in the hierarchy.
* Progress per insets animation is global for the entire animation. One animation animates
* all things together (in, out, ...). If they don't animate together, we'd have
* multiple animations.
* <p>
* Note: In case the application is controlling the animation, the valued returned here will
* be the same as the application passed into
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)},
* interpolated with the interpolator passed into
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}.
* <p>
* Note: For system-initiated animations, this will always return a valid value between 0
* and 1.
*
* @return The current interpolated progress of this animation.
* @see #getFraction() for raw fraction.
*/
public float getInterpolatedFraction() {
return mImpl.getInterpolatedFraction();
}
/**
* Retrieves the interpolator used for this animation, or {@code null} if this animation
* doesn't follow an interpolation curved. For system-initiated animations, this will never
* return {@code null}.
*
* @return The interpolator used for this animation.
*/
@Nullable
public Interpolator getInterpolator() {
return mImpl.getInterpolator();
}
/**
* @return duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or
* -1 if the animation doesn't have a fixed duration.
*/
public long getDurationMillis() {
return mImpl.getDurationMillis();
}
/**
* Set fraction of the progress if {@link Type} animation is controlled by the app.
* <p>
* Note: This should only be used for testing, as the system fills in the fraction for the
* application or the fraction that was passed into
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)} is
* being used.
*
* @param fraction fractional progress between 0 and 1 where 0 represents hidden and
* zero progress and 1 represent fully shown final state.
* @see #getFraction()
*/
public void setFraction(@FloatRange(from = 0f, to = 1f) float fraction) {
mImpl.setFraction(fraction);
}
/**
* Retrieves the translucency of the windows that are animating.
*
* @return Alpha of windows that cause insets of type {@link Type}.
*/
@FloatRange(from = 0f, to = 1f)
public float getAlpha() {
return mImpl.getAlpha();
}
/**
* Sets the translucency of the windows that are animating.
* <p>
* Note: This should only be used for testing, as the system fills in the alpha for the
* application or the alpha that was passed into
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)} is
* being used.
*
* @param alpha Alpha of windows that cause insets of type {@link Type}.
* @see #getAlpha()
*/
public void setAlpha(@FloatRange(from = 0f, to = 1f) float alpha) {
mImpl.setAlpha(alpha);
}
/**
* Class representing the range of an {@link WindowInsetsAnimationCompat}
*/
public static final class BoundsCompat {
private final Insets mLowerBound;
private final Insets mUpperBound;
public BoundsCompat(@NonNull Insets lowerBound, @NonNull Insets upperBound) {
mLowerBound = lowerBound;
mUpperBound = upperBound;
}
@RequiresApi(30)
private BoundsCompat(@NonNull WindowInsetsAnimation.Bounds bounds) {
mLowerBound = Impl30.getLowerBounds(bounds);
mUpperBound = Impl30.getHigherBounds(bounds);
}
/**
* Queries the lower inset bound of the animation. If the animation is about showing or
* hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper
* bound is the same as {@link WindowInsetsCompat#getInsets(int)} for the fully shown
* state. This
* is the same as {@link WindowInsetsAnimationControllerCompat#getHiddenStateInsets} and
* {@link WindowInsetsAnimationControllerCompat#getShownStateInsets} in case the listener
* gets invoked because of an animation that originates from
* {@link WindowInsetsAnimationControllerCompat}.
* <p>
* However, if the size of a window that causes insets is changing, these are the
* lower/upper bounds of that size animation.
* </p>
* There are no overlapping animations for a specific type, but there may be multiple
* animations running at the same time for different inset types.
*
* @see #getUpperBound()
* @see WindowInsetsAnimationControllerCompat#getHiddenStateInsets
*/
@NonNull
public Insets getLowerBound() {
return mLowerBound;
}
/**
* Queries the upper inset bound of the animation. If the animation is about showing or
* hiding a window that cause insets, the lower bound is {@link Insets#NONE} nd the upper
* bound is the same as {@link WindowInsetsCompat#getInsets(int)} for the fully shown
* state. This is the same as
* {@link WindowInsetsAnimationControllerCompat#getHiddenStateInsets} and
* {@link WindowInsetsAnimationControllerCompat#getShownStateInsets} in case the listener
* gets invoked because of an animation that originates from
* {@link WindowInsetsAnimationControllerCompat}.
* <p>
* However, if the size of a window that causes insets is changing, these are the
* lower/upper bounds of that size animation.
* <p>
* There are no overlapping animations for a specific type, but there may be multiple
* animations running at the same time for different inset types.
*
* @see #getLowerBound()
* @see WindowInsetsAnimationControllerCompat#getShownStateInsets
*/
@NonNull
public Insets getUpperBound() {
return mUpperBound;
}
/**
* Insets both the lower and upper bound by the specified insets. This is to be used in
* {@link WindowInsetsAnimationCompat.Callback#onStart} to indicate that a part of the
* insets has been used to offset or clip its children, and the children shouldn't worry
* about that part anymore.
*
* @param insets The amount to inset.
* @return A copy of this instance inset in the given directions.
* @see WindowInsetsCompat#inset
* @see WindowInsetsAnimationCompat.Callback#onStart
*/
@NonNull
public BoundsCompat inset(@NonNull Insets insets) {
return new BoundsCompat(
// TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate
// place eventually.
WindowInsetsCompat.insetInsets(
mLowerBound, insets.left, insets.top, insets.right, insets.bottom),
WindowInsetsCompat.insetInsets(
mUpperBound, insets.left, insets.top, insets.right, insets.bottom));
}
@Override
public String toString() {
return "Bounds{lower=" + mLowerBound + " upper=" + mUpperBound + "}";
}
/**
* Creates a new instance of {@link WindowInsetsAnimation.Bounds} from this compat instance.
*/
@RequiresApi(30)
@NonNull
public WindowInsetsAnimation.Bounds toBounds() {
return Impl30.createPlatformBounds(this);
}
/**
* Create a new insance of {@link BoundsCompat} using the provided
* platform {@link android.view.WindowInsetsAnimation.Bounds}.
*/
@RequiresApi(30)
@NonNull
public static BoundsCompat toBoundsCompat(@NonNull WindowInsetsAnimation.Bounds bounds) {
return new BoundsCompat(bounds);
}
}
@RequiresApi(30)
static WindowInsetsAnimationCompat toWindowInsetsAnimationCompat(
WindowInsetsAnimation windowInsetsAnimation) {
return new WindowInsetsAnimationCompat(windowInsetsAnimation);
}
/**
* Interface that allows the application to listen to animation events for windows that cause
* insets.
*/
public abstract static class Callback {
/**
* Return value for {@link #getDispatchMode()}: Dispatching of animation events should
* stop at this level in the view hierarchy, and no animation events should be dispatch to
* the subtree of the view hierarchy.
*/
public static final int DISPATCH_MODE_STOP = 0;
/**
* Return value for {@link #getDispatchMode()}: Dispatching of animation events should
* continue in the view hierarchy.
*/
public static final int DISPATCH_MODE_CONTINUE_ON_SUBTREE = 1;
WindowInsets mDispachedInsets;
/** @hide */
@IntDef(value = {
DISPATCH_MODE_STOP,
DISPATCH_MODE_CONTINUE_ON_SUBTREE
})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface DispatchMode {
}
@DispatchMode
private final int mDispatchMode;
/**
* Creates a new {@link WindowInsetsAnimationCompat} callback with the given
* {@link #getDispatchMode() dispatch mode}.
*
* @param dispatchMode The dispatch mode for this callback. See {@link #getDispatchMode()}.
*/
public Callback(@DispatchMode int dispatchMode) {
mDispatchMode = dispatchMode;
}
/**
* Retrieves the dispatch mode of this listener. Dispatch of the all animation events is
* hierarchical: It will starts at the root of the view hierarchy and then traverse it and
* invoke the callback of the specific {@link View} that is being traversed.
* The method may return either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that
* animation events should be propagated to the subtree of the view hierarchy, or
* {@link #DISPATCH_MODE_STOP} to stop dispatching. In that case, all animation callbacks
* related to the animation passed in will be stopped from propagating to the subtree of the
* hierarchy.
* <p>
* Also note that {@link #DISPATCH_MODE_STOP} behaves the same way as
* returning {@link WindowInsetsCompat#CONSUMED} during the regular insets dispatch in
* {@link View#onApplyWindowInsets}.
*
* @return Either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that dispatching of
* animation events will continue to the subtree of the view hierarchy, or
* {@link #DISPATCH_MODE_STOP} to indicate that animation events will stop
* dispatching.
*/
@DispatchMode
public final int getDispatchMode() {
return mDispatchMode;
}
/**
* Called when an insets animation is about to start and before the views have been
* re-laid out due to an animation.
* <p>
* This ordering allows the application to inspect the end state after the animation has
* finished, and then revert to the starting state of the animation in the first
* {@link #onProgress} callback by using post-layout view properties like {@link View#setX}
* and related methods.
* <p>
* The ordering of events during an insets animation is
* the following:
* <ul>
* <li>Application calls {@link WindowInsetsControllerCompat#hide(int)},
* {@link WindowInsetsControllerCompat#show(int)},
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}</li>
* <li>onPrepare is called on the view hierarchy listeners</li>
* <li>{@link View#onApplyWindowInsets} will be called with the end state of the
* animation</li>
* <li>View hierarchy gets laid out according to the changes the application has
* requested due to the new insets being dispatched</li>
* <li>{@link #onStart} is called <em>before</em> the view
* hierarchy gets drawn in the new laid out state</li>
* <li>{@link #onProgress} is called immediately after with the animation start
* state</li>
* <li>The frame gets drawn.</li>
* </ul>
* <p>
* Note: If the animation is application controlled by using
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}, the end state of
* the animation is undefined as the application may decide on the end state only by
* passing in {@code shown} parameter when calling
* {@link WindowInsetsAnimationControllerCompat#finish}. In this situation, the system
* will dispatch the insets in the opposite visibility state before the animation starts.
* Example: When controlling the input method with
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation} and the input method
* is currently showing, {@link View#onApplyWindowInsets} will receive a
* {@link WindowInsetsCompat} instance for which {@link WindowInsetsCompat#isVisible}
* will return {@code false} for {@link WindowInsetsCompat.Type#ime}.
*
* @param animation The animation that is about to start.
*/
public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
}
/**
* Called when an insets animation gets started.
* <p>
* This ordering allows the application to inspect the end state after the animation has
* finished, and then revert to the starting state of the animation in the first
* {@link #onProgress} callback by using post-layout view properties like {@link View#setX}
* and related methods.
* <p>
* The ordering of events during an insets animation is
* the following:
* <ul>
* <li>Application calls {@link WindowInsetsControllerCompat#hide(int)},
* {@link WindowInsetsControllerCompat#show(int)},
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}</li>
* <li>onPrepare is called on the view hierarchy listeners</li>
* <li>{@link View#onApplyWindowInsets} will be called with the end state of the
* animation</li>
* <li>View hierarchy gets laid out according to the changes the application has
* requested due to the new insets being dispatched</li>
* <li>{@link #onStart} is called <em>before</em> the view
* hierarchy gets drawn in the new laid out state</li>
* <li>{@link #onProgress} is called immediately after with the animation start
* state</li>
* <li>The frame gets drawn.</li>
* </ul>
* <p>
* Note that, like {@link #onProgress}, dispatch of the animation start event is
* hierarchical: It will starts at the root of the view hierarchy and then traverse it
* and invoke the callback of the specific {@link View} that is being traversed. The
* method may return a modified instance of the bounds by calling
* {@link BoundsCompat#inset} to indicate that a part of the insets
* have been used to offset or clip its children, and the children shouldn't worry about
* that part anymore. Furthermore, if {@link #getDispatchMode()} returns
* {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
*
* @param animation The animation that is about to start.
* @param bounds The bounds in which animation happens.
* @return The animation bounds representing the part of the insets that should be
* dispatched to
* the subtree of the hierarchy.
*/
@NonNull
public BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
@NonNull BoundsCompat bounds) {
return bounds;
}
/**
* Called when the insets change as part of running an animation. Note that even if multiple
* animations for different types are running, there will only be one progress callback per
* frame. The {@code insets} passed as an argument represents the overall state and will
* include all types, regardless of whether they are animating or not.
* <p>
* Note that insets dispatch is hierarchical: It will start at the root of the view
* hierarchy, and then traverse it and invoke the callback of the specific {@link View}
* being traversed. The method may return a modified instance by calling
* {@link WindowInsetsCompat#inset(int, int, int, int)} to indicate that a part of the
* insets have been used to offset or clip its children, and the children shouldn't worry
* about that part anymore. Furthermore, if {@link #getDispatchMode()} returns
* {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
*
* @param insets The current insets.
* @param runningAnimations The currently running animations.
* @return The insets to dispatch to the subtree of the hierarchy.
*/
@NonNull
public abstract WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations);
/**
* Called when an insets animation has ended.
*
* @param animation The animation that has ended. This will be the same instance
* as passed into {@link #onStart}
*/
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
}
}
static void setCallback(@NonNull View view, @Nullable Callback callback) {
if (Build.VERSION.SDK_INT >= 30) {
Impl30.setCallback(view, callback);
} else if (Build.VERSION.SDK_INT >= 21) {
Impl21.setCallback(view, callback);
}
// Do nothing pre 21
}
private static class Impl {
@InsetsType
private final int mTypeMask;
private float mFraction;
@Nullable
private final Interpolator mInterpolator;
private final long mDurationMillis;
private float mAlpha;
Impl(int typeMask, @Nullable Interpolator interpolator, long durationMillis) {
mTypeMask = typeMask;
mInterpolator = interpolator;
mDurationMillis = durationMillis;
}
public int getTypeMask() {
return mTypeMask;
}
public float getFraction() {
return mFraction;
}
public float getInterpolatedFraction() {
if (mInterpolator != null) {
return mInterpolator.getInterpolation(mFraction);
}
return mFraction;
}
@Nullable
public Interpolator getInterpolator() {
return mInterpolator;
}
public long getDurationMillis() {
return mDurationMillis;
}
public float getAlpha() {
return mAlpha;
}
public void setFraction(float fraction) {
mFraction = fraction;
}
public void setAlpha(float alpha) {
mAlpha = alpha;
}
}
@RequiresApi(21)
private static class Impl21 extends Impl {
Impl21(int typeMask, @Nullable Interpolator interpolator, long durationMillis) {
super(typeMask, interpolator, durationMillis);
}
static void setCallback(@NonNull final View view,
@Nullable final Callback callback) {
Object userListener = view.getTag(R.id.tag_on_apply_window_listener);
if (callback == null) {
view.setTag(R.id.tag_window_insets_animation_callback, null);
if (userListener == null) {
// If no user defined listener is set, that means our listener is the one set.
// Make sure to remove it.
view.setOnApplyWindowInsetsListener(null);
}
} else {
View.OnApplyWindowInsetsListener proxyListener =
createProxyListener(view, callback);
view.setTag(R.id.tag_window_insets_animation_callback, proxyListener);
// We rely on OnApplyWindowInsetsListener, but one might already be set by the
// application, so we only register it on the view if none is set yet.
// If one is set using ViewCompat.setOnApplyWindowInsetsListener,
// this Callback will be called by the exiting listener.
if (userListener == null) {
view.setOnApplyWindowInsetsListener(proxyListener);
}
}
}
@NonNull
private static View.OnApplyWindowInsetsListener createProxyListener(
@NonNull View view, @NonNull final Callback callback) {
return new Impl21OnApplyWindowInsetsListener(view, callback);
}
@NonNull
static BoundsCompat computeAnimationBounds(
@NonNull WindowInsetsCompat targetInsets,
@NonNull WindowInsetsCompat startingInsets, int mask) {
Insets targetInsetsInsets = targetInsets.getInsets(mask);
Insets startingInsetsInsets = startingInsets.getInsets(mask);
final Insets lowerBound = Insets.of(
Math.min(targetInsetsInsets.left, startingInsetsInsets.left),
Math.min(targetInsetsInsets.top, startingInsetsInsets.top),
Math.min(targetInsetsInsets.right, startingInsetsInsets.right),
Math.min(targetInsetsInsets.bottom, startingInsetsInsets.bottom)
);
final Insets upperBound = Insets.of(
Math.max(targetInsetsInsets.left, startingInsetsInsets.left),
Math.max(targetInsetsInsets.top, startingInsetsInsets.top),
Math.max(targetInsetsInsets.right, startingInsetsInsets.right),
Math.max(targetInsetsInsets.bottom, startingInsetsInsets.bottom)
);
return new BoundsCompat(lowerBound, upperBound);
}
@SuppressLint("WrongConstant") // We iterate over all the constants.
static int buildAnimationMask(@NonNull WindowInsetsCompat targetInsets,
@NonNull WindowInsetsCompat currentInsets) {
int animatingMask = 0;
for (int i = WindowInsetsCompat.Type.FIRST; i <= WindowInsetsCompat.Type.LAST;
i = i << 1) {
if (!targetInsets.getInsets(i).equals(currentInsets.getInsets(i))) {
animatingMask |= i;
}
}
return animatingMask;
}
@SuppressLint("WrongConstant")
static WindowInsetsCompat interpolateInsets(
WindowInsetsCompat target, WindowInsetsCompat starting,
float fraction, int typeMask) {
WindowInsetsCompat.Builder builder = new WindowInsetsCompat.Builder(target);
for (int i = WindowInsetsCompat.Type.FIRST; i <= WindowInsetsCompat.Type.LAST;
i = i << 1) {
if ((typeMask & i) == 0) {
builder.setInsets(i, target.getInsets(i));
continue;
}
Insets targetInsets = target.getInsets(i);
Insets startingInsets = starting.getInsets(i);
Insets interpolatedInsets = WindowInsetsCompat.insetInsets(
targetInsets,
(int) (0.5 + (targetInsets.left - startingInsets.left) * (1 - fraction)),
(int) (0.5 + (targetInsets.top - startingInsets.top) * (1 - fraction)),
(int) (0.5 + (targetInsets.right - startingInsets.right) * (1 - fraction)),
(int) (0.5 + (targetInsets.bottom - startingInsets.bottom) * (1 - fraction))
);
builder.setInsets(i, interpolatedInsets);
}
return builder.build();
}
/**
* Wrapper class around a {@link Callback} that will trigger the callback when
* {@link View#onApplyWindowInsets(WindowInsets)} is called
*/
@RequiresApi(21)
private static class Impl21OnApplyWindowInsetsListener implements
View.OnApplyWindowInsetsListener {
private static final int COMPAT_ANIMATION_DURATION = 160;
final Callback mCallback;
// We save the last insets to compute the starting insets for the animation.
private WindowInsetsCompat mLastInsets;
Impl21OnApplyWindowInsetsListener(@NonNull View view, @NonNull Callback callback) {
mCallback = callback;
WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
mLastInsets = rootWindowInsets != null
// Insets are not immutable on SDK < 26 so we make copy to ensure it's not
// changed until we need them.
? new WindowInsetsCompat.Builder(rootWindowInsets).build()
: null;
}
@Override
public WindowInsets onApplyWindowInsets(final View v, final WindowInsets insets) {
// We cannot rely on the compat insets value until the view is laid out.
if (!v.isLaidOut()) {
mLastInsets = toWindowInsetsCompat(insets, v);
return forwardToViewIfNeeded(v, insets);
}
final WindowInsetsCompat targetInsets = toWindowInsetsCompat(insets, v);
if (mLastInsets == null) {
mLastInsets = ViewCompat.getRootWindowInsets(v);
}
if (mLastInsets == null) {
if (DEBUG) {
Log.d(TAG, "Couldn't initialize last insets");
}
mLastInsets = targetInsets;
return forwardToViewIfNeeded(v, insets);
}
if (DEBUG) {
int allTypes = WindowInsetsCompat.Type.all();
Log.d(TAG, String.format("lastInsets: %s\ntargetInsets: %s",
mLastInsets.getInsets(allTypes),
targetInsets.getInsets(allTypes)));
}
// When we start dispatching the insets animation, we save the instance of insets
// that have been dispatched first as a marker to avoid dispatching the callback
// in children.
Callback callback = getCallback(v);
if (callback != null && Objects.equals(callback.mDispachedInsets, insets)) {
return forwardToViewIfNeeded(v, insets);
}
// We only run the animation when the some insets are animating
final int animationMask = buildAnimationMask(targetInsets, mLastInsets);
if (animationMask == 0) {
if (DEBUG) {
Log.d(TAG, "Insets applied bug no window animation to run");
}
return forwardToViewIfNeeded(v, insets);
}
final WindowInsetsCompat startingInsets = this.mLastInsets;
final WindowInsetsAnimationCompat anim =
new WindowInsetsAnimationCompat(animationMask, new DecelerateInterpolator(),
COMPAT_ANIMATION_DURATION);
anim.setFraction(0);
final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(
anim.getDurationMillis());
// Compute the bounds of the animation
final BoundsCompat animationBounds = computeAnimationBounds(targetInsets,
startingInsets, animationMask
);
dispatchOnPrepare(v, anim, insets, false);
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
anim.setFraction(animator.getAnimatedFraction());
WindowInsetsCompat interpolateInsets = interpolateInsets(
targetInsets,
startingInsets,
anim.getInterpolatedFraction(), animationMask);
List<WindowInsetsAnimationCompat> runningAnimations =
Collections.singletonList(anim);
dispatchOnProgress(v, interpolateInsets, runningAnimations);
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
anim.setFraction(1);
dispatchOnEnd(v, anim);
}
});
// We need to call onStart and start the animator before the next draw
// to ensure the animation starts before the relayout caused by the change of
// insets.
OneShotPreDrawListener.add(v, new Runnable() {
@Override
public void run() {
dispatchOnStart(v, anim, animationBounds);
animator.start();
}
});
this.mLastInsets = targetInsets;
return forwardToViewIfNeeded(v, insets);
}
}
/**
* Forward the call to view.onApplyWindowInsets if there is no other listener attached to
* the view.
*/
@NonNull
static WindowInsets forwardToViewIfNeeded(@NonNull View v, @NonNull WindowInsets insets) {
// If the app set an on apply window listener, it will be called after this
// and will decide whether to call the view's onApplyWindowInsets.
if (v.getTag(R.id.tag_on_apply_window_listener) != null) {
return insets;
}
return v.onApplyWindowInsets(insets);
}
static void dispatchOnPrepare(View v, WindowInsetsAnimationCompat anim,
WindowInsets insets, boolean stopDispatch) {
final Callback callback = getCallback(v);
if (callback != null) {
callback.mDispachedInsets = insets;
if (!stopDispatch) {
callback.onPrepare(anim);
stopDispatch = callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP;
}
}
// When stopDispatch is true, we don't call onPrepare but we still need to propagate
// the dispatched insets to the children to mark them with the latest dispatched
// insets so their compat callback in not called when onApplyWindowInsets is called.
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
dispatchOnPrepare(child, anim, insets, stopDispatch);
}
}
}
static void dispatchOnStart(View v,
WindowInsetsAnimationCompat anim,
BoundsCompat animationBounds) {
final Callback callback = getCallback(v);
if (callback != null) {
callback.onStart(anim, animationBounds);
if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) {
return;
}
}
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
dispatchOnStart(child, anim, animationBounds);
}
}
}
static void dispatchOnProgress(@NonNull View v,
@NonNull WindowInsetsCompat interpolateInsets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
final Callback callback = getCallback(v);
WindowInsetsCompat insets = interpolateInsets;
if (callback != null) {
insets = callback.onProgress(insets, runningAnimations);
if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) {
return;
}
}
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
dispatchOnProgress(child, insets, runningAnimations);
}
}
}
static void dispatchOnEnd(@NonNull View v,
@NonNull WindowInsetsAnimationCompat anim) {
final Callback callback = getCallback(v);
if (callback != null) {
callback.onEnd(anim);
if (callback.getDispatchMode() == Callback.DISPATCH_MODE_STOP) {
return;
}
}
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
dispatchOnEnd(child, anim);
}
}
}
@Nullable
static Callback getCallback(View child) {
Object listener = child.getTag(
R.id.tag_window_insets_animation_callback);
Callback callback = null;
if (listener instanceof Impl21OnApplyWindowInsetsListener) {
callback = ((Impl21OnApplyWindowInsetsListener) listener).mCallback;
}
return callback;
}
}
@RequiresApi(30)
private static class Impl30 extends Impl {
@NonNull
private final WindowInsetsAnimation mWrapped;
Impl30(@NonNull WindowInsetsAnimation wrapped) {
super(0, null, 0);
mWrapped = wrapped;
}
Impl30(int typeMask, Interpolator interpolator, long durationMillis) {
this(new WindowInsetsAnimation(typeMask, interpolator, durationMillis));
}
@Override
public int getTypeMask() {
return mWrapped.getTypeMask();
}
@Override
@Nullable
public Interpolator getInterpolator() {
return mWrapped.getInterpolator();
}
@Override
public long getDurationMillis() {
return mWrapped.getDurationMillis();
}
@Override
public float getFraction() {
return mWrapped.getFraction();
}
@Override
public void setFraction(float fraction) {
mWrapped.setFraction(fraction);
}
@Override
public float getInterpolatedFraction() {
return mWrapped.getInterpolatedFraction();
}
@RequiresApi(30)
private static class ProxyCallback extends WindowInsetsAnimation.Callback {
private final Callback mCompat;
ProxyCallback(@NonNull final WindowInsetsAnimationCompat.Callback compat) {
super(compat.getDispatchMode());
mCompat = compat;
}
private List<WindowInsetsAnimationCompat> mRORunningAnimations;
private ArrayList<WindowInsetsAnimationCompat> mTmpRunningAnimations;
private final HashMap<WindowInsetsAnimation, WindowInsetsAnimationCompat>
mAnimations = new HashMap<>();
@NonNull
private WindowInsetsAnimationCompat getWindowInsetsAnimationCompat(
@NonNull WindowInsetsAnimation animation) {
WindowInsetsAnimationCompat animationCompat = mAnimations.get(
animation);
if (animationCompat == null) {
animationCompat = toWindowInsetsAnimationCompat(animation);
mAnimations.put(animation, animationCompat);
}
return animationCompat;
}
@Override
public void onPrepare(@NonNull WindowInsetsAnimation animation) {
mCompat.onPrepare(getWindowInsetsAnimationCompat(animation));
}
@NonNull
@Override
public WindowInsetsAnimation.Bounds onStart(
@NonNull WindowInsetsAnimation animation,
@NonNull WindowInsetsAnimation.Bounds bounds) {
return mCompat.onStart(
getWindowInsetsAnimationCompat(animation),
BoundsCompat.toBoundsCompat(bounds)).toBounds();
}
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations) {
if (mTmpRunningAnimations == null) {
mTmpRunningAnimations = new ArrayList<>(runningAnimations.size());
mRORunningAnimations = Collections.unmodifiableList(mTmpRunningAnimations);
} else {
mTmpRunningAnimations.clear();
}
for (int i = runningAnimations.size() - 1; i >= 0; i--) {
WindowInsetsAnimation animation = runningAnimations.get(i);
WindowInsetsAnimationCompat animationCompat =
getWindowInsetsAnimationCompat(animation);
animationCompat.setFraction(animation.getFraction());
mTmpRunningAnimations.add(animationCompat);
}
return mCompat.onProgress(
WindowInsetsCompat.toWindowInsetsCompat(insets),
mRORunningAnimations).toWindowInsets();
}
@Override
public void onEnd(@NonNull WindowInsetsAnimation animation) {
mCompat.onEnd(getWindowInsetsAnimationCompat(animation));
mAnimations.remove(animation);
}
}
public static void setCallback(@NonNull View view, @Nullable Callback callback) {
WindowInsetsAnimation.Callback platformCallback =
callback != null ? new ProxyCallback(callback) : null;
view.setWindowInsetsAnimationCallback(platformCallback);
}
@NonNull
public static WindowInsetsAnimation.Bounds createPlatformBounds(
@NonNull BoundsCompat bounds) {
return new WindowInsetsAnimation.Bounds(bounds.getLowerBound().toPlatformInsets(),
bounds.getUpperBound().toPlatformInsets());
}
@NonNull
public static Insets getLowerBounds(@NonNull WindowInsetsAnimation.Bounds bounds) {
return Insets.toCompatInsets(bounds.getLowerBound());
}
@NonNull
public static Insets getHigherBounds(@NonNull WindowInsetsAnimation.Bounds bounds) {
return Insets.toCompatInsets(bounds.getUpperBound());
}
}
}