diff options
| author | 2020-01-10 13:35:28 +0000 | |
|---|---|---|
| committer | 2020-01-10 13:35:28 +0000 | |
| commit | f093b48b58e9975b607e2e41405084145c46448b (patch) | |
| tree | 94f258bfb82e6542645b7eae24e2c27b6c3afcb7 | |
| parent | b9816f6e5d1ba30fc4a4fc3f19e4f390525bc2c7 (diff) | |
| parent | a51168aaee6b623f340e6ae4691e7c5ca1a629ce (diff) | |
Merge "Correct layout/draw/animation interleaving for insets callbacks"
15 files changed, 314 insertions, 127 deletions
diff --git a/api/current.txt b/api/current.txt index 04a4946f28a2..0c93555fc483 100644 --- a/api/current.txt +++ b/api/current.txt @@ -51607,9 +51607,10 @@ package android.view { method public boolean dispatchUnhandledMove(android.view.View, int); method protected void dispatchVisibilityChanged(@NonNull android.view.View, int); method public void dispatchWindowFocusChanged(boolean); - method public void dispatchWindowInsetsAnimationFinished(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method public void dispatchWindowInsetsAnimationFinish(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method public void dispatchWindowInsetsAnimationPrepare(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); method @NonNull public android.view.WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull android.view.WindowInsets); - method @NonNull public android.view.WindowInsetsAnimationCallback.AnimationBounds dispatchWindowInsetsAnimationStarted(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); + method @NonNull public android.view.WindowInsetsAnimationCallback.AnimationBounds dispatchWindowInsetsAnimationStart(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); method public void dispatchWindowSystemUiVisiblityChanged(int); method public void dispatchWindowVisibilityChanged(int); method @CallSuper public void draw(android.graphics.Canvas); @@ -53289,9 +53290,10 @@ package android.view { } public interface WindowInsetsAnimationCallback { - method public default void onFinished(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method public default void onFinish(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method public default void onPrepare(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); method @NonNull public android.view.WindowInsets onProgress(@NonNull android.view.WindowInsets); - method @NonNull public default android.view.WindowInsetsAnimationCallback.AnimationBounds onStarted(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); + method @NonNull public default android.view.WindowInsetsAnimationCallback.AnimationBounds onStart(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); } public static final class WindowInsetsAnimationCallback.AnimationBounds { diff --git a/core/java/android/view/InsetsAnimationControlCallbacks.java b/core/java/android/view/InsetsAnimationControlCallbacks.java index 6fdadc60afea..27edb0b69bdd 100644 --- a/core/java/android/view/InsetsAnimationControlCallbacks.java +++ b/core/java/android/view/InsetsAnimationControlCallbacks.java @@ -16,17 +16,28 @@ package android.view; +import android.view.InsetsController.LayoutInsetsDuringAnimation; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; +import android.view.WindowInsetsAnimationCallback.InsetsAnimation; + /** * Provide an interface to let InsetsAnimationControlImpl call back into its owner. * @hide */ public interface InsetsAnimationControlCallbacks { + /** - * Dispatch the animation started event to all listeners. - * @param animation + * Executes the necessary code to start the animation in the correct order, including: + * <ul> + * <li>Dispatch {@link WindowInsetsAnimationCallback#onPrepare}</li> + * <li>Update insets state and run layout according to {@code layoutDuringAnimation}</li> + * <li>Dispatch {@link WindowInsetsAnimationCallback#onStart}</li> + * <li>Dispatch {@link WindowInsetsAnimationControlListener#onReady}</li> + * </ul> */ - void dispatchAnimationStarted(WindowInsetsAnimationCallback.InsetsAnimation animation, - WindowInsetsAnimationCallback.AnimationBounds bounds); + void startAnimation(InsetsAnimationControlImpl controller, + WindowInsetsAnimationControlListener listener, int types, InsetsAnimation animation, + AnimationBounds bounds, @LayoutInsetsDuringAnimation int layoutDuringAnimation); /** * Schedule the apply by posting the animation callback. diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index a3245b9916ec..6589e75c7bc2 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -16,6 +16,8 @@ package android.view; +import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN; +import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; import static android.view.InsetsState.ISIDE_BOTTOM; import static android.view.InsetsState.ISIDE_FLOATING; import static android.view.InsetsState.ISIDE_LEFT; @@ -30,6 +32,7 @@ import android.util.ArraySet; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.SparseSetArray; +import android.view.InsetsController.LayoutInsetsDuringAnimation; import android.view.InsetsState.InternalInsetsSide; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowInsets.Type.InsetsType; @@ -80,7 +83,8 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll public InsetsAnimationControlImpl(SparseArray<InsetsSourceControl> controls, Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, @InsetsType int types, - InsetsAnimationControlCallbacks controller, long durationMs, boolean fade) { + InsetsAnimationControlCallbacks controller, long durationMs, boolean fade, + @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation) { mControls = controls; mListener = listener; mTypes = types; @@ -95,14 +99,11 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll mFrame = new Rect(frame); buildTypeSourcesMap(mTypeSideMap, mSideSourceMap, mControls); - // TODO: Check for controllability first and wait for IME if needed. - listener.onReady(this, types); - mAnimation = new WindowInsetsAnimationCallback.InsetsAnimation(mTypes, InsetsController.INTERPOLATOR, durationMs); mAnimation.setAlpha(getCurrentAlpha()); - mController.dispatchAnimationStarted(mAnimation, - new AnimationBounds(mHiddenInsets, mShownInsets)); + mController.startAnimation(this, listener, types, mAnimation, + new AnimationBounds(mHiddenInsets, mShownInsets), layoutInsetsDuringAnimation); } @Override diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 2a7a4e3a922c..0207abdda355 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -37,6 +37,7 @@ import android.util.SparseArray; import android.view.InsetsSourceConsumer.ShowResult; import android.view.InsetsState.InternalInsetsType; import android.view.SurfaceControl.Transaction; +import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimationCallback.AnimationBounds; @@ -47,6 +48,8 @@ import android.view.animation.PathInterpolator; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** @@ -67,6 +70,37 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private @interface AnimationDirection{} /** + * Layout mode during insets animation: The views should be laid out as if the changing inset + * types are fully shown. Before starting the animation, {@link View#onApplyWindowInsets} will + * be called as if the changing insets types are shown, which will result in the views being + * laid out as if the insets are fully shown. + */ + static final int LAYOUT_INSETS_DURING_ANIMATION_SHOWN = 0; + + /** + * Layout mode during insets animation: The views should be laid out as if the changing inset + * types are fully hidden. Before starting the animation, {@link View#onApplyWindowInsets} will + * be called as if the changing insets types are hidden, which will result in the views being + * laid out as if the insets are fully hidden. + */ + static final int LAYOUT_INSETS_DURING_ANIMATION_HIDDEN = 1; + + /** + * Determines the behavior of how the views should be laid out during an insets animation that + * is controlled by the application by calling {@link #controlWindowInsetsAnimation}. + * <p> + * When the animation is system-initiated, the layout mode is always chosen such that the + * pre-animation layout will represent the opposite of the starting state, i.e. when insets + * are appearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_SHOWN} will be used. When insets + * are disappearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_HIDDEN} will be used. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {LAYOUT_INSETS_DURING_ANIMATION_SHOWN, + LAYOUT_INSETS_DURING_ANIMATION_HIDDEN}) + @interface LayoutInsetsDuringAnimation { + } + + /** * Translation animation evaluator. */ private static TypeEvaluator<Insets> sEvaluator = (fraction, startValue, endValue) -> Insets.of( @@ -109,11 +143,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public void onReady(WindowInsetsAnimationController controller, int types) { mController = controller; - if (mShow) { - showDirectly(types); - } else { - hideDirectly(types); - } + mAnimationDirection = mShow ? DIRECTION_SHOW : DIRECTION_HIDE; mAnimator = ObjectAnimator.ofObject( controller, @@ -131,7 +161,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation onAnimationFinish(); } }); + mStartingAnimation = true; mAnimator.start(); + mStartingAnimation = false; } @Override @@ -185,6 +217,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private int mPendingTypesToShow; private int mLastLegacySoftInputMode; + private boolean mStartingAnimation; private SyncRtSurfaceTransactionApplier mApplier; @@ -312,7 +345,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // Only one animator (with multiple InsetsType) can run at a time. // previous one should be cancelled for simplicity. cancelExistingAnimation(); - } else if (consumer.isVisible() + } else if (consumer.isRequestedVisible() && (mAnimationDirection == DIRECTION_NONE || mAnimationDirection == DIRECTION_HIDE)) { // no-op: already shown or animating in (because window visibility is @@ -338,7 +371,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (mAnimationDirection == DIRECTION_SHOW) { cancelExistingAnimation(); - } else if (!consumer.isVisible() + } else if (!consumer.isRequestedVisible() && (mAnimationDirection == DIRECTION_NONE || mAnimationDirection == DIRECTION_HIDE)) { // no-op: already hidden or animating out. @@ -363,12 +396,14 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation listener.onCancelled(); return; } - controlAnimationUnchecked(types, listener, mFrame, fromIme, durationMs, false /* fade */); + controlAnimationUnchecked(types, listener, mFrame, fromIme, durationMs, false /* fade */, + getLayoutInsetsDuringAnimationMode(types)); } private void controlAnimationUnchecked(@InsetsType int types, WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme, - long durationMs, boolean fade) { + long durationMs, boolean fade, + @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation) { if (types == 0) { // nothing to animate. return; @@ -398,7 +433,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(controls, - frame, mState, listener, typesReady, this, durationMs, fade); + frame, mState, listener, typesReady, this, durationMs, fade, + layoutInsetsDuringAnimation); mAnimationControls.add(controller); } @@ -412,7 +448,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation boolean isReady = true; for (int i = internalTypes.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); - boolean setVisible = !consumer.isVisible(); + boolean setVisible = !consumer.isRequestedVisible(); if (setVisible) { // Show request switch(consumer.requestShow(fromIme)) { @@ -454,6 +490,29 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return typesReady; } + private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode( + @InsetsType int types) { + + final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types); + + // Generally, we want to layout the opposite of the current state. This is to make animation + // callbacks easy to use: The can capture the layout values and then treat that as end-state + // during the animation. + // + // However, if controlling multiple sources, we want to treat it as shown if any of the + // types is currently hidden. + for (int i = internalTypes.size() - 1; i >= 0; i--) { + InsetsSourceConsumer consumer = mSourceConsumers.get(internalTypes.valueAt(i)); + if (consumer == null) { + continue; + } + if (!consumer.isRequestedVisible()) { + return LAYOUT_INSETS_DURING_ANIMATION_SHOWN; + } + } + return LAYOUT_INSETS_DURING_ANIMATION_HIDDEN; + } + private void cancelExistingControllers(@InsetsType int types) { for (int i = mAnimationControls.size() - 1; i >= 0; i--) { InsetsAnimationControlImpl control = mAnimationControls.get(i); @@ -597,7 +656,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // and hidden state insets are correct. controlAnimationUnchecked( types, listener, mState.getDisplayFrame(), fromIme, listener.getDurationMs(), - true /* fade */); + true /* fade */, show + ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN + : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN); } private void hideDirectly(@InsetsType int types) { @@ -629,18 +690,40 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @VisibleForTesting @Override - public void dispatchAnimationStarted(InsetsAnimation animation, AnimationBounds bounds) { - mViewRoot.mView.dispatchWindowInsetsAnimationStarted(animation, bounds); + public void startAnimation(InsetsAnimationControlImpl controller, + WindowInsetsAnimationControlListener listener, int types, InsetsAnimation animation, + AnimationBounds bounds, int layoutDuringAnimation) { + if (layoutDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN) { + showDirectly(types); + } else { + hideDirectly(types); + } + mViewRoot.mView.dispatchWindowInsetsAnimationPrepare(animation); + mViewRoot.mView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + mViewRoot.mView.getViewTreeObserver().removeOnPreDrawListener(this); + mViewRoot.mView.dispatchWindowInsetsAnimationStart(animation, bounds); + listener.onReady(controller, types); + return true; + } + }); + mViewRoot.mView.invalidate(); } @VisibleForTesting public void dispatchAnimationFinished(InsetsAnimation animation) { - mViewRoot.mView.dispatchWindowInsetsAnimationFinished(animation); + mViewRoot.mView.dispatchWindowInsetsAnimationFinish(animation); } @VisibleForTesting @Override public void scheduleApplyChangeInsets() { + if (mStartingAnimation) { + mAnimCallback.run(); + mAnimCallbackScheduled = false; + return; + } if (!mAnimCallbackScheduled) { mViewRoot.mChoreographer.postCallback(Choreographer.CALLBACK_INSETS_ANIMATION, mAnimCallback, null /* token*/); diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index c6d9898a425c..b2a5d915c2a6 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -53,7 +53,7 @@ public class InsetsSourceConsumer { } protected final InsetsController mController; - protected boolean mVisible; + protected boolean mRequestedVisible; private final Supplier<Transaction> mTransactionSupplier; private final @InternalInsetsType int mType; private final InsetsState mState; @@ -66,7 +66,7 @@ public class InsetsSourceConsumer { mState = state; mTransactionSupplier = transactionSupplier; mController = controller; - mVisible = InsetsState.getDefaultVisibility(type); + mRequestedVisible = InsetsState.getDefaultVisibility(type); } public void setControl(@Nullable InsetsSourceControl control) { @@ -94,12 +94,12 @@ public class InsetsSourceConsumer { @VisibleForTesting public void show() { - setVisible(true); + setRequestedVisible(true); } @VisibleForTesting public void hide() { - setVisible(false); + setRequestedVisible(false); } /** @@ -126,16 +126,16 @@ public class InsetsSourceConsumer { if (mSourceControl == null) { return false; } - if (mState.getSource(mType).isVisible() == mVisible) { + if (mState.getSource(mType).isVisible() == mRequestedVisible) { return false; } - mState.getSource(mType).setVisible(mVisible); + mState.getSource(mType).setVisible(mRequestedVisible); return true; } @VisibleForTesting - public boolean isVisible() { - return mVisible; + public boolean isRequestedVisible() { + return mRequestedVisible; } /** @@ -157,11 +157,15 @@ public class InsetsSourceConsumer { // no-op for types that always return ShowResult#SHOW_IMMEDIATELY. } - private void setVisible(boolean visible) { - if (mVisible == visible) { + /** + * Sets requested visibility from the client, regardless of whether we are able to control it at + * the moment. + */ + private void setRequestedVisible(boolean requestedVisible) { + if (mRequestedVisible == requestedVisible) { return; } - mVisible = visible; + mRequestedVisible = requestedVisible; applyLocalVisibilityOverride(); mController.notifyVisibilityChanged(); } @@ -173,7 +177,7 @@ public class InsetsSourceConsumer { } final Transaction t = mTransactionSupplier.get(); - if (mVisible) { + if (mRequestedVisible) { t.show(mSourceControl.getLeash()); } else { t.hide(mSourceControl.getLeash()); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0db80e2749c3..13d609b16541 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -11117,7 +11117,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Dispatches {@link WindowInsetsAnimationCallback#onStarted(InsetsAnimation, AnimationBounds)} + * Dispatches {@link WindowInsetsAnimationCallback#onPrepare(InsetsAnimation)} + * when Window Insets animation is being prepared. + * @param animation current animation + * + * @see WindowInsetsAnimationCallback#onPrepare(InsetsAnimation) + */ + public void dispatchWindowInsetsAnimationPrepare( + @NonNull InsetsAnimation animation) { + if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { + mListenerInfo.mWindowInsetsAnimationCallback.onPrepare(animation); + } + } + + /** + * Dispatches {@link WindowInsetsAnimationCallback#onStart(InsetsAnimation, AnimationBounds)} * when Window Insets animation is started. * @param animation current animation * @param bounds the upper and lower {@link AnimationBounds} that provides range of @@ -11125,10 +11139,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return the upper and lower {@link AnimationBounds}. */ @NonNull - public AnimationBounds dispatchWindowInsetsAnimationStarted( + public AnimationBounds dispatchWindowInsetsAnimationStart( @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { - return mListenerInfo.mWindowInsetsAnimationCallback.onStarted(animation, bounds); + return mListenerInfo.mWindowInsetsAnimationCallback.onStart(animation, bounds); } return bounds; } @@ -11149,13 +11163,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Dispatches {@link WindowInsetsAnimationCallback#onFinished(InsetsAnimation)} + * Dispatches {@link WindowInsetsAnimationCallback#onFinish(InsetsAnimation)} * when Window Insets animation finishes. * @param animation The current ongoing {@link InsetsAnimation}. */ - public void dispatchWindowInsetsAnimationFinished(@NonNull InsetsAnimation animation) { + public void dispatchWindowInsetsAnimationFinish(@NonNull InsetsAnimation animation) { if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { - mListenerInfo.mWindowInsetsAnimationCallback.onFinished(animation); + mListenerInfo.mWindowInsetsAnimationCallback.onFinish(animation); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 5fb71773db8f..047d7da7536f 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -7199,13 +7199,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override + public void dispatchWindowInsetsAnimationPrepare( + @NonNull InsetsAnimation animation) { + super.dispatchWindowInsetsAnimationPrepare(animation); + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i).dispatchWindowInsetsAnimationPrepare(animation); + } + } + + @Override @NonNull - public AnimationBounds dispatchWindowInsetsAnimationStarted( + public AnimationBounds dispatchWindowInsetsAnimationStart( @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { - super.dispatchWindowInsetsAnimationStarted(animation, bounds); + super.dispatchWindowInsetsAnimationStart(animation, bounds); final int count = getChildCount(); for (int i = 0; i < count; i++) { - getChildAt(i).dispatchWindowInsetsAnimationStarted(animation, bounds); + getChildAt(i).dispatchWindowInsetsAnimationStart(animation, bounds); } return bounds; } @@ -7222,11 +7232,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - public void dispatchWindowInsetsAnimationFinished(@NonNull InsetsAnimation animation) { - super.dispatchWindowInsetsAnimationFinished(animation); + public void dispatchWindowInsetsAnimationFinish(@NonNull InsetsAnimation animation) { + super.dispatchWindowInsetsAnimationFinish(animation); final int count = getChildCount(); for (int i = 0; i < count; i++) { - getChildAt(i).dispatchWindowInsetsAnimationFinished(animation); + getChildAt(i).dispatchWindowInsetsAnimationFinish(animation); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index bf8dc65abe28..ab89ef46e09e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1464,6 +1464,7 @@ public final class ViewRootImpl implements ViewParent, return; } mApplyInsetsRequested = true; + requestLayout(); // If this changes during traversal, no need to schedule another one as it will dispatch it // during the current traversal. diff --git a/core/java/android/view/WindowInsetsAnimationCallback.java b/core/java/android/view/WindowInsetsAnimationCallback.java index 5e71f271f1d4..e84c3e33c000 100644 --- a/core/java/android/view/WindowInsetsAnimationCallback.java +++ b/core/java/android/view/WindowInsetsAnimationCallback.java @@ -20,6 +20,7 @@ import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; +import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; @@ -30,7 +31,47 @@ import android.view.animation.Interpolator; public interface WindowInsetsAnimationCallback { /** - * Called when an inset animation gets started. + * Called when an insets animation is about to start and before the views have been laid out in + * the end state of the animation. The ordering of events during an insets animation is the + * following: + * <p> + * <ul> + * <li>Application calls {@link WindowInsetsController#hideInputMethod()}, + * {@link WindowInsetsController#showInputMethod()}, + * {@link WindowInsetsController#controlInputMethodAnimation(long, WindowInsetsAnimationControlListener)}</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> + * 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> + * Note: If the animation is application controlled by using + * {@link WindowInsetsController#controlInputMethodAnimation}, the end state of the animation + * is undefined as the application may decide on the end state only by passing in the + * {@code shown} parameter when calling {@link WindowInsetsAnimationController#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 WindowInsetsController#controlInputMethodAnimation} and the input method is currently + * showing, {@link View#onApplyWindowInsets} will receive a {@link WindowInsets} instance for + * which {@link WindowInsets#isVisible} will return {@code false} for {@link Type#ime}. + * + * @param animation The animation that is about to start. + */ + default void onPrepare(@NonNull InsetsAnimation animation) { + } + + /** + * Called when an insets animation gets started. * <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 @@ -45,7 +86,7 @@ public interface WindowInsetsAnimationCallback { * subtree of the hierarchy. */ @NonNull - default AnimationBounds onStarted( + default AnimationBounds onStart( @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { return bounds; } @@ -72,12 +113,12 @@ public interface WindowInsetsAnimationCallback { WindowInsets onProgress(@NonNull WindowInsets insets); /** - * Called when an inset animation has finished. + * Called when an insets animation has finished. * * @param animation The animation that has finished running. This will be the same instance as - * passed into {@link #onStarted} + * passed into {@link #onStart} */ - default void onFinished(@NonNull InsetsAnimation animation) { + default void onFinish(@NonNull InsetsAnimation animation) { } /** @@ -253,14 +294,14 @@ public interface WindowInsetsAnimationCallback { /** * Insets both the lower and upper bound by the specified insets. This is to be used in - * {@link WindowInsetsAnimationCallback#onStarted} to indicate that a part of the insets has + * {@link WindowInsetsAnimationCallback#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 WindowInsets#inset - * @see WindowInsetsAnimationCallback#onStarted + * @see WindowInsetsAnimationCallback#onStart */ @NonNull public AnimationBounds inset(@NonNull Insets insets) { diff --git a/core/java/android/view/WindowInsetsAnimationControlListener.java b/core/java/android/view/WindowInsetsAnimationControlListener.java index 8a226c1bbe23..f91254de33ff 100644 --- a/core/java/android/view/WindowInsetsAnimationControlListener.java +++ b/core/java/android/view/WindowInsetsAnimationControlListener.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.Hide; import android.annotation.NonNull; import android.view.WindowInsets.Type.InsetsType; import android.view.inputmethod.EditorInfo; @@ -26,6 +27,12 @@ import android.view.inputmethod.EditorInfo; public interface WindowInsetsAnimationControlListener { /** + * @hide + */ + default void onPrepare(int types) { + } + + /** * Called when the animation is ready to be controlled. This may be delayed when the IME needs * to redraw because of an {@link EditorInfo} change, or when the window is starting up. * diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index 6de56be2f3c5..9d7f292dbdf5 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -149,7 +149,8 @@ public interface WindowInsetsController { * * @param types The {@link InsetsType}s the application has requested to control. * @param durationMillis duration of animation in - * {@link java.util.concurrent.TimeUnit#MILLISECONDS} + * {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or -1 if the + * animation doesn't have a predetermined duration. * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the * windows are ready to be controlled, among other callbacks. * @hide @@ -162,7 +163,8 @@ public interface WindowInsetsController { * modifying the position of the IME when it's causing insets. * * @param durationMillis duration of the animation in - * {@link java.util.concurrent.TimeUnit#MILLISECONDS} + * {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or -1 if the + * animation doesn't have a predetermined duration. * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the * IME are ready to be controlled, among other callbacks. */ diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index 0e19ca84d433..d0fd92a838c9 100644 --- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java @@ -90,12 +90,12 @@ public class ImeInsetsSourceConsumerTest { mImeConsumer.onWindowFocusGained(); mImeConsumer.applyImeVisibility(true); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test if setVisibility can hide IME mImeConsumer.applyImeVisibility(false); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); } diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java index 179929f2aae0..fa61a0a0250b 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -16,11 +16,11 @@ package android.view; +import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL; import static android.view.WindowInsets.Type.systemBars; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -39,8 +39,6 @@ import android.view.SurfaceControl.Transaction; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.test.InsetsModeSession; -import androidx.test.runner.AndroidJUnit4; - import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -52,6 +50,8 @@ import org.mockito.MockitoAnnotations; import java.util.List; +import androidx.test.runner.AndroidJUnit4; + /** * Tests for {@link InsetsAnimationControlImpl}. * @@ -116,7 +116,7 @@ public class InsetsAnimationControlImplTest { mController = new InsetsAnimationControlImpl(controls, new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(), mMockController, 10 /* durationMs */, - false /* fade */); + false /* fade */, LAYOUT_INSETS_DURING_ANIMATION_SHOWN); } @Test diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index a89fc1e6315f..1db96b15f83a 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -68,6 +68,7 @@ public class InsetsControllerTest { private InsetsController mController; private SurfaceSession mSession = new SurfaceSession(); private SurfaceControl mLeash; + private ViewRootImpl mViewRoot; @Before public void setup() { @@ -77,13 +78,13 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { Context context = InstrumentationRegistry.getTargetContext(); // cannot mock ViewRootImpl since it's final. - ViewRootImpl viewRootImpl = new ViewRootImpl(context, context.getDisplay()); + mViewRoot = new ViewRootImpl(context, context.getDisplay()); try { - viewRootImpl.setView(new TextView(context), new LayoutParams(), null); + mViewRoot.setView(new TextView(context), new LayoutParams(), null); } catch (BadTokenException e) { // activity isn't running, we will ignore BadTokenException. } - mController = new InsetsController(viewRootImpl); + mController = new InsetsController(mViewRoot); final Rect rect = new Rect(5, 5, 5, 5); mController.calculateInsets( false, @@ -117,16 +118,22 @@ public class InsetsControllerTest { @Test public void testControlsRevoked_duringAnim() { - InsetsSourceControl control = - new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point()); - mController.onControlsChanged(new InsetsSourceControl[] { control }); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + InsetsSourceControl control = + new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point()); + mController.onControlsChanged(new InsetsSourceControl[] { control }); - WindowInsetsAnimationControlListener mockListener = - mock(WindowInsetsAnimationControlListener.class); - mController.controlWindowInsetsAnimation(statusBars(), 10 /* durationMs */, mockListener); - verify(mockListener).onReady(any(), anyInt()); - mController.onControlsChanged(new InsetsSourceControl[0]); - verify(mockListener).onCancelled(); + WindowInsetsAnimationControlListener mockListener = + mock(WindowInsetsAnimationControlListener.class); + mController.controlWindowInsetsAnimation(statusBars(), 10 /* durationMs */, + mockListener); + + // Ready gets deferred until next predraw + mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); + verify(mockListener).onReady(any(), anyInt()); + mController.onControlsChanged(new InsetsSourceControl[0]); + verify(mockListener).onCancelled(); + }); } @Test @@ -154,16 +161,16 @@ public class InsetsControllerTest { mController.show(Type.all()); // quickly jump to final state by cancelling it. mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.applyImeVisibility(false /* setVisible */); mController.hide(Type.all()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost(); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); @@ -180,10 +187,10 @@ public class InsetsControllerTest { mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(); mController.applyImeVisibility(true); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.applyImeVisibility(false); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost(); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); @@ -201,16 +208,16 @@ public class InsetsControllerTest { // test show select types. mController.show(types); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test hide all mController.hide(types); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } @@ -227,29 +234,29 @@ public class InsetsControllerTest { // test show select types. mController.show(types); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test hide all mController.hide(Type.all()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test single show mController.show(Type.navigationBars()); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test single hide mController.hide(Type.navigationBars()); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); @@ -267,31 +274,31 @@ public class InsetsControllerTest { mController.show(Type.navigationBars()); mController.show(Type.systemBars()); mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.hide(Type.navigationBars()); mController.hide(Type.systemBars()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); int types = Type.navigationBars() | Type.systemBars(); // show two at a time and hide one by one. mController.show(types); mController.hide(Type.navigationBars()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.hide(Type.systemBars()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } @@ -309,15 +316,15 @@ public class InsetsControllerTest { mController.show(types); mController.hide(Type.navigationBars()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.hide(Type.systemBars()); mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(statusBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } @@ -336,12 +343,16 @@ public class InsetsControllerTest { ArgumentCaptor<WindowInsetsAnimationController> controllerCaptor = ArgumentCaptor.forClass(WindowInsetsAnimationController.class); + + // Ready gets deferred until next predraw + mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); + verify(mockListener).onReady(controllerCaptor.capture(), anyInt()); controllerCaptor.getValue().finish(false /* shown */); }); waitUntilNextFrame(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isVisible()); + assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible()); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index 7af833bfcba4..492c03653990 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -96,7 +96,7 @@ public class InsetsSourceConsumerTest { @Test public void testHide() { mConsumer.hide(); - assertFalse("Consumer should not be visible", mConsumer.isVisible()); + assertFalse("Consumer should not be visible", mConsumer.isRequestedVisible()); verify(mSpyInsetsSource).setVisible(eq(false)); } @@ -106,7 +106,7 @@ public class InsetsSourceConsumerTest { // Insets source starts out visible mConsumer.hide(); mConsumer.show(); - assertTrue("Consumer should be visible", mConsumer.isVisible()); + assertTrue("Consumer should be visible", mConsumer.isRequestedVisible()); verify(mSpyInsetsSource).setVisible(eq(false)); verify(mSpyInsetsSource).setVisible(eq(true)); } |