/* * Copyright (C) 2018 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 android.view; import static android.view.InsetsState.ITYPE_CAPTION_BAR; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.toInternalType; import static android.view.InsetsState.toPublicType; import static android.view.WindowInsets.Type.all; import static android.view.WindowInsets.Type.ime; import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.os.CancellationSignal; import android.os.Handler; import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; import android.view.InsetsSourceConsumer.ShowResult; import android.view.InsetsState.InternalInsetsType; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; /** * Implements {@link WindowInsetsController} on the client. * @hide */ public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks { public interface Host { Handler getHandler(); /** * Notifies host that {@link InsetsController#getState()} has changed. */ void notifyInsetsChanged(); void dispatchWindowInsetsAnimationPrepare(@NonNull WindowInsetsAnimation animation); Bounds dispatchWindowInsetsAnimationStart( @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds); WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets, @NonNull List runningAnimations); void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation); /** * Requests host to apply surface params in synchronized manner. */ void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params); /** * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean) */ void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible, boolean hasControl); /** * Called when insets have been modified by the client and should be reported back to WM. */ void onInsetsModified(InsetsState insetsState); /** * @return Whether the host has any callbacks it wants to synchronize the animations with. * If there are no callbacks, the animation will be off-loaded to another thread and * slightly different animation curves are picked. */ boolean hasAnimationCallbacks(); /** * @see WindowInsetsController#setSystemBarsAppearance */ void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask); /** * @see WindowInsetsController#getSystemBarsAppearance() */ @Appearance int getSystemBarsAppearance(); /** * @see WindowInsetsController#setSystemBarsBehavior */ void setSystemBarsBehavior(@Behavior int behavior); /** * @see WindowInsetsController#getSystemBarsBehavior */ @Behavior int getSystemBarsBehavior(); /** * Releases a surface and ensure that this is done after {@link #applySurfaceParams} has * finished applying params. */ void releaseSurfaceControlFromRt(SurfaceControl surfaceControl); /** * If this host is a view hierarchy, adds a pre-draw runnable to ensure proper ordering as * described in {@link WindowInsetsAnimation.Callback#onPrepare}. * * If this host isn't a view hierarchy, the runnable can be executed immediately. */ void addOnPreDrawRunnable(Runnable r); /** * Adds a runnbale to be executed during {@link Choreographer#CALLBACK_INSETS_ANIMATION} * phase. */ void postInsetsAnimationCallback(Runnable r); /** * Obtains {@link InputMethodManager} instance from host. */ InputMethodManager getInputMethodManager(); } private static final int ANIMATION_DURATION_SHOW_MS = 275; private static final int ANIMATION_DURATION_HIDE_MS = 340; private static final int ANIMATION_DURATION_SYNC_IME_MS = 285; private static final int ANIMATION_DURATION_UNSYNC_IME_MS = 200; private static final int PENDING_CONTROL_TIMEOUT_MS = 2000; public static final Interpolator SYSTEM_BARS_INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); private static final Interpolator SYNC_IME_INTERPOLATOR = new PathInterpolator(0.2f, 0f, 0f, 1f); private static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new PathInterpolator(0, 0, 0.2f, 1f); private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new PathInterpolator(0.4f, 0f, 1f, 1f); /** * 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. */ public 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. */ public 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}. *

* 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 { } /** Not running an animation. */ @VisibleForTesting public static final int ANIMATION_TYPE_NONE = -1; /** Running animation will show insets */ @VisibleForTesting public static final int ANIMATION_TYPE_SHOW = 0; /** Running animation will hide insets */ @VisibleForTesting public static final int ANIMATION_TYPE_HIDE = 1; /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */ @VisibleForTesting public static final int ANIMATION_TYPE_USER = 2; @Retention(RetentionPolicy.SOURCE) @IntDef(value = {ANIMATION_TYPE_NONE, ANIMATION_TYPE_SHOW, ANIMATION_TYPE_HIDE, ANIMATION_TYPE_USER}) @interface AnimationType { } /** * Translation animation evaluator. */ private static TypeEvaluator sEvaluator = (fraction, startValue, endValue) -> Insets.of( (int) (startValue.left + fraction * (endValue.left - startValue.left)), (int) (startValue.top + fraction * (endValue.top - startValue.top)), (int) (startValue.right + fraction * (endValue.right - startValue.right)), (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom))); /** * The default implementation of listener, to be used by InsetsController and InsetsPolicy to * animate insets. */ public static class InternalAnimationControlListener implements WindowInsetsAnimationControlListener { private WindowInsetsAnimationController mController; private ValueAnimator mAnimator; private final boolean mShow; private final boolean mHasAnimationCallbacks; private final @InsetsType int mRequestedTypes; private final long mDurationMs; private ThreadLocal mSfAnimationHandlerThreadLocal = new ThreadLocal() { @Override protected AnimationHandler initialValue() { AnimationHandler handler = new AnimationHandler(); handler.setProvider(new SfVsyncFrameCallbackProvider()); return handler; } }; public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks, int requestedTypes) { mShow = show; mHasAnimationCallbacks = hasAnimationCallbacks; mRequestedTypes = requestedTypes; mDurationMs = calculateDurationMs(); } @Override public void onReady(WindowInsetsAnimationController controller, int types) { mController = controller; mAnimator = ValueAnimator.ofFloat(0f, 1f); mAnimator.setDuration(mDurationMs); mAnimator.setInterpolator(new LinearInterpolator()); Insets start = mShow ? controller.getHiddenStateInsets() : controller.getShownStateInsets(); Insets end = mShow ? controller.getShownStateInsets() : controller.getHiddenStateInsets(); Interpolator insetsInterpolator = getInterpolator(); Interpolator alphaInterpolator = getAlphaInterpolator(); mAnimator.addUpdateListener(animation -> { float rawFraction = animation.getAnimatedFraction(); float alphaFraction = mShow ? rawFraction : 1 - rawFraction; float insetsFraction = insetsInterpolator.getInterpolation(rawFraction); controller.setInsetsAndAlpha( sEvaluator.evaluate(insetsFraction, start, end), alphaInterpolator.getInterpolation(alphaFraction), rawFraction); }); mAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { onAnimationFinish(); } }); if (!mHasAnimationCallbacks) { mAnimator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); } mAnimator.start(); } @Override public void onFinished(WindowInsetsAnimationController controller) { } @Override public void onCancelled(WindowInsetsAnimationController controller) { // Animator can be null when it is cancelled before onReady() completes. if (mAnimator != null) { mAnimator.cancel(); } } Interpolator getInterpolator() { if ((mRequestedTypes & ime()) != 0) { if (mHasAnimationCallbacks) { return SYNC_IME_INTERPOLATOR; } else if (mShow) { return LINEAR_OUT_SLOW_IN_INTERPOLATOR; } else { return FAST_OUT_LINEAR_IN_INTERPOLATOR; } } else { return SYSTEM_BARS_INTERPOLATOR; } } Interpolator getAlphaInterpolator() { if ((mRequestedTypes & ime()) != 0) { if (mHasAnimationCallbacks) { return input -> 1f; } else if (mShow) { // Alpha animation takes half the time with linear interpolation; return input -> Math.min(1f, 2 * input); } else { return FAST_OUT_LINEAR_IN_INTERPOLATOR; } } else { return input -> 1f; } } protected void onAnimationFinish() { mController.finish(mShow); } /** * To get the animation duration in MS. */ public long getDurationMs() { return mDurationMs; } private long calculateDurationMs() { if ((mRequestedTypes & ime()) != 0) { if (mHasAnimationCallbacks) { return ANIMATION_DURATION_SYNC_IME_MS; } else { return ANIMATION_DURATION_UNSYNC_IME_MS; } } else { return mShow ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS; } } } /** * Represents a running animation */ private static class RunningAnimation { RunningAnimation(InsetsAnimationControlRunner runner, int type) { this.runner = runner; this.type = type; } final InsetsAnimationControlRunner runner; final @AnimationType int type; /** * Whether {@link WindowInsetsAnimation.Callback#onStart(WindowInsetsAnimation, Bounds)} has * been dispatched already for this animation. */ boolean startDispatched; } /** * Represents a control request that we had to defer because we are waiting for the IME to * process our show request. */ private static class PendingControlRequest { PendingControlRequest(@InsetsType int types, WindowInsetsAnimationControlListener listener, long durationMs, Interpolator interpolator, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, CancellationSignal cancellationSignal, boolean useInsetsAnimationThread) { this.types = types; this.listener = listener; this.durationMs = durationMs; this.interpolator = interpolator; this.animationType = animationType; this.layoutInsetsDuringAnimation = layoutInsetsDuringAnimation; this.cancellationSignal = cancellationSignal; this.useInsetsAnimationThread = useInsetsAnimationThread; } final @InsetsType int types; final WindowInsetsAnimationControlListener listener; final long durationMs; final Interpolator interpolator; final @AnimationType int animationType; final @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation; final CancellationSignal cancellationSignal; final boolean useInsetsAnimationThread; } private final String TAG = "InsetsControllerImpl"; /** The local state */ private final InsetsState mState = new InsetsState(); /** The state dispatched from server */ private final InsetsState mLastDispatchedState = new InsetsState(); /** The state sent to server */ private final InsetsState mRequestedState = new InsetsState(); private final Rect mFrame = new Rect(); private final BiFunction mConsumerCreator; private final SparseArray mSourceConsumers = new SparseArray<>(); private final Host mHost; private final Handler mHandler; private final SparseArray mTmpControlArray = new SparseArray<>(); private final ArrayList mRunningAnimations = new ArrayList<>(); private final ArrayList mTmpRunningAnims = new ArrayList<>(); private final List mUnmodifiableTmpRunningAnims = Collections.unmodifiableList(mTmpRunningAnims); private final ArrayList mTmpFinishedControls = new ArrayList<>(); private WindowInsets mLastInsets; private boolean mAnimCallbackScheduled; private final Runnable mAnimCallback; /** Pending control request that is waiting on IME to be ready to be shown */ private PendingControlRequest mPendingImeControlRequest; private int mLastLegacySoftInputMode; private int mLastLegacySystemUiFlags; private DisplayCutout mLastDisplayCutout; private boolean mStartingAnimation; private int mCaptionInsetsHeight = 0; private Runnable mPendingControlTimeout = this::abortPendingImeControlRequest; private final ArrayList mControllableInsetsChangedListeners = new ArrayList<>(); /** Set of inset types for which an animation was started since last resetting this field */ private @InsetsType int mLastStartedAnimTypes; public InsetsController(Host host) { this(host, (controller, type) -> { if (type == ITYPE_IME) { return new ImeInsetsSourceConsumer(controller.mState, Transaction::new, controller); } else { return new InsetsSourceConsumer(type, controller.mState, Transaction::new, controller); } }, host.getHandler()); } @VisibleForTesting public InsetsController(Host host, BiFunction consumerCreator, Handler handler) { mHost = host; mConsumerCreator = consumerCreator; mHandler = handler; mAnimCallback = () -> { mAnimCallbackScheduled = false; if (mRunningAnimations.isEmpty()) { return; } mTmpFinishedControls.clear(); mTmpRunningAnims.clear(); InsetsState state = new InsetsState(mState, true /* copySources */); for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { RunningAnimation runningAnimation = mRunningAnimations.get(i); InsetsAnimationControlRunner runner = runningAnimation.runner; if (runner instanceof InsetsAnimationControlImpl) { InsetsAnimationControlImpl control = (InsetsAnimationControlImpl) runner; // Keep track of running animation to be dispatched. Aggregate it here such that // if it gets finished within applyChangeInsets we still dispatch it to // onProgress. if (runningAnimation.startDispatched) { mTmpRunningAnims.add(control.getAnimation()); } if (control.applyChangeInsets(state)) { mTmpFinishedControls.add(control); } } } WindowInsets insets = state.calculateInsets(mFrame, mState /* ignoringVisibilityState*/, mLastInsets.isRound(), mLastInsets.shouldAlwaysConsumeSystemBars(), mLastDisplayCutout, mLastLegacySoftInputMode, mLastLegacySystemUiFlags, null /* typeSideMap */); mHost.dispatchWindowInsetsAnimationProgress(insets, mUnmodifiableTmpRunningAnims); for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) { dispatchAnimationEnd(mTmpFinishedControls.get(i).getAnimation()); } }; } @VisibleForTesting public void onFrameChanged(Rect frame) { if (mFrame.equals(frame)) { return; } mHost.notifyInsetsChanged(); mFrame.set(frame); } @Override public InsetsState getState() { return mState; } @Override public boolean isRequestedVisible(int type) { return getSourceConsumer(type).isRequestedVisible(); } public InsetsState getLastDispatchedState() { return mLastDispatchedState; } @VisibleForTesting public boolean onStateChanged(InsetsState state) { boolean localStateChanged = !mState.equals(state, true /* excludingCaptionInsets */) || !captionInsetsUnchanged(); if (!localStateChanged && mLastDispatchedState.equals(state)) { return false; } updateState(state); mLastDispatchedState.set(state, true /* copySources */); applyLocalVisibilityOverride(); if (localStateChanged) { mHost.notifyInsetsChanged(); } if (!mState.equals(mLastDispatchedState, true /* excludingCaptionInsets */)) { updateRequestedState(); } return true; } private void updateState(InsetsState newState) { mState.setDisplayFrame(newState.getDisplayFrame()); for (int i = newState.getSourcesCount() - 1; i >= 0; i--) { InsetsSource source = newState.sourceAt(i); getSourceConsumer(source.getType()).updateSource(source); } for (int i = mState.getSourcesCount() - 1; i >= 0; i--) { InsetsSource source = mState.sourceAt(i); if (newState.peekSource(source.getType()) == null) { mState.removeSource(source.getType()); } } if (mCaptionInsetsHeight != 0) { mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top, mFrame.right, mFrame.top + mCaptionInsetsHeight)); } } private boolean captionInsetsUnchanged() { if (mState.peekSource(ITYPE_CAPTION_BAR) == null && mCaptionInsetsHeight == 0) { return true; } if (mState.peekSource(ITYPE_CAPTION_BAR) != null && mCaptionInsetsHeight == mState.peekSource(ITYPE_CAPTION_BAR).getFrame().height()) { return true; } return false; } /** * @see InsetsState#calculateInsets */ @VisibleForTesting public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars, DisplayCutout cutout, int legacySoftInputMode, int legacySystemUiFlags) { mLastLegacySoftInputMode = legacySoftInputMode; mLastLegacySystemUiFlags = legacySystemUiFlags; mLastDisplayCutout = cutout; mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState*/, isScreenRound, alwaysConsumeSystemBars, cutout, legacySoftInputMode, legacySystemUiFlags, null /* typeSideMap */); return mLastInsets; } /** * @see InsetsState#calculateVisibleInsets(Rect, int) */ public Rect calculateVisibleInsets(@SoftInputModeFlags int softInputMode) { return mState.calculateVisibleInsets(mFrame, softInputMode); } /** * Called when the server has dispatched us a new set of inset controls. */ public void onControlsChanged(InsetsSourceControl[] activeControls) { if (activeControls != null) { for (InsetsSourceControl activeControl : activeControls) { if (activeControl != null) { // TODO(b/122982984): Figure out why it can be null. mTmpControlArray.put(activeControl.getType(), activeControl); } } } final boolean hasControl = mTmpControlArray.size() > 0; final int[] showTypes = new int[1]; final int[] hideTypes = new int[1]; // Ensure to update all existing source consumers for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); final InsetsSourceControl control = mTmpControlArray.get(consumer.getType()); // control may be null, but we still need to update the control to null if it got // revoked. consumer.setControl(control, showTypes, hideTypes); } // Ensure to create source consumers if not available yet. for (int i = mTmpControlArray.size() - 1; i >= 0; i--) { final InsetsSourceControl control = mTmpControlArray.valueAt(i); InsetsSourceConsumer consumer = getSourceConsumer(control.getType()); consumer.setControl(control, showTypes, hideTypes); } mTmpControlArray.clear(); // Do not override any animations that the app started in the OnControllableInsetsChanged // listeners. int animatingTypes = invokeControllableInsetsChangedListeners(); showTypes[0] &= ~animatingTypes; hideTypes[0] &= ~animatingTypes; if (showTypes[0] != 0) { applyAnimation(showTypes[0], true /* show */, false /* fromIme */); } if (hideTypes[0] != 0) { applyAnimation(hideTypes[0], false /* show */, false /* fromIme */); } if (hasControl && mRequestedState.getSourcesCount() > 0) { // We might have changed our requested visibilities while we don't have the control, // so we need to update our requested state once we have control. Otherwise, our // requested state at the server side might be incorrect. updateRequestedState(); } } @Override public void show(@InsetsType int types) { show(types, false /* fromIme */); } @VisibleForTesting public void show(@InsetsType int types, boolean fromIme) { // Handle pending request ready in case there was one set. if (fromIme && mPendingImeControlRequest != null) { PendingControlRequest pendingRequest = mPendingImeControlRequest; mPendingImeControlRequest = null; mHandler.removeCallbacks(mPendingControlTimeout); controlAnimationUnchecked( pendingRequest.types, pendingRequest.cancellationSignal, pendingRequest.listener, mFrame, true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator, pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation, pendingRequest.useInsetsAnimationThread); return; } // TODO: Support a ResultReceiver for IME. // TODO(b/123718661): Make show() work for multi-session IME. int typesReady = 0; final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { @InternalInsetsType int internalType = internalTypes.valueAt(i); @AnimationType int animationType = getAnimationType(internalType); InsetsSourceConsumer consumer = getSourceConsumer(internalType); if (consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE || animationType == ANIMATION_TYPE_SHOW) { // no-op: already shown or animating in (because window visibility is // applied before starting animation). continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } applyAnimation(typesReady, true /* show */, fromIme); } @Override public void hide(@InsetsType int types) { hide(types, false /* fromIme */); } void hide(@InsetsType int types, boolean fromIme) { int typesReady = 0; final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { @InternalInsetsType int internalType = internalTypes.valueAt(i); @AnimationType int animationType = getAnimationType(internalType); InsetsSourceConsumer consumer = getSourceConsumer(internalType); if (!consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE || animationType == ANIMATION_TYPE_HIDE) { // no-op: already hidden or animating out. continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } applyAnimation(typesReady, false /* show */, fromIme /* fromIme */); } @Override public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis, @Nullable Interpolator interpolator, @Nullable CancellationSignal cancellationSignal, @NonNull WindowInsetsAnimationControlListener listener) { controlWindowInsetsAnimation(types, cancellationSignal, listener, false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER); } private void controlWindowInsetsAnimation(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, @AnimationType int animationType) { if (!checkDisplayFramesForControlling()) { listener.onCancelled(null); return; } controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs, interpolator, animationType, getLayoutInsetsDuringAnimationMode(types), false /* useInsetsAnimationThread */); } private boolean checkDisplayFramesForControlling() { // If the frame of our window doesn't span the entire display, the control API makes very // little sense, as we don't deal with negative insets. So just cancel immediately. return mState.getDisplayFrame().equals(mFrame); } private void controlAnimationUnchecked(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme, long durationMs, Interpolator interpolator, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, boolean useInsetsAnimationThread) { if (types == 0) { // nothing to animate. listener.onCancelled(null); return; } cancelExistingControllers(types); mLastStartedAnimTypes |= types; final ArraySet internalTypes = InsetsState.toInternalType(types); final SparseArray controls = new SparseArray<>(); Pair typesReadyPair = collectSourceControls( fromIme, internalTypes, controls, animationType); int typesReady = typesReadyPair.first; boolean imeReady = typesReadyPair.second; if (!imeReady) { // IME isn't ready, all requested types will be animated once IME is ready abortPendingImeControlRequest(); final PendingControlRequest request = new PendingControlRequest(types, listener, durationMs, interpolator, animationType, layoutInsetsDuringAnimation, cancellationSignal, useInsetsAnimationThread); mPendingImeControlRequest = request; mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS); if (cancellationSignal != null) { cancellationSignal.setOnCancelListener(() -> { if (mPendingImeControlRequest == request) { abortPendingImeControlRequest(); } }); } return; } if (typesReady == 0) { listener.onCancelled(null); return; } final InsetsAnimationControlRunner runner = useInsetsAnimationThread ? new InsetsAnimationThreadControlRunner(controls, frame, mState, listener, typesReady, this, durationMs, interpolator, animationType, mHost.getHandler()) : new InsetsAnimationControlImpl(controls, frame, mState, listener, typesReady, this, durationMs, interpolator, animationType); mRunningAnimations.add(new RunningAnimation(runner, animationType)); if (cancellationSignal != null) { cancellationSignal.setOnCancelListener(runner::cancel); } if (layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN) { showDirectly(types); } else { hideDirectly(types, false /* animationFinished */, animationType); } } /** * @return Pair of (types ready to animate, IME ready to animate). */ private Pair collectSourceControls(boolean fromIme, ArraySet internalTypes, SparseArray controls, @AnimationType int animationType) { int typesReady = 0; boolean imeReady = true; for (int i = internalTypes.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); boolean show = animationType == ANIMATION_TYPE_SHOW || animationType == ANIMATION_TYPE_USER; boolean canRun = false; if (show) { // Show request switch(consumer.requestShow(fromIme)) { case ShowResult.SHOW_IMMEDIATELY: canRun = true; break; case ShowResult.IME_SHOW_DELAYED: imeReady = false; break; case ShowResult.IME_SHOW_FAILED: // IME cannot be shown (since it didn't have focus), proceed // with animation of other types. break; } } else { // Hide request // TODO: Move notifyHidden() to beginning of the hide animation // (when visibility actually changes using hideDirectly()). if (!fromIme) { consumer.notifyHidden(); } canRun = true; } if (!canRun) { continue; } final InsetsSourceControl control = consumer.getControl(); if (control != null) { controls.put(consumer.getType(), new InsetsSourceControl(control)); typesReady |= toPublicType(consumer.getType()); } else if (animationType == ANIMATION_TYPE_SHOW) { // We don't have a control at the moment. However, we still want to update requested // visibility state such that in case we get control, we can apply show animation. consumer.show(fromIme); } else if (animationType == ANIMATION_TYPE_HIDE) { consumer.hide(); } } return new Pair<>(typesReady, imeReady); } private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode( @InsetsType int types) { final ArraySet 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 = mRunningAnimations.size() - 1; i >= 0; i--) { InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; if ((control.getTypes() & types) != 0) { cancelAnimation(control, true /* invokeCallback */); } } if ((types & ime()) != 0) { abortPendingImeControlRequest(); } } private void abortPendingImeControlRequest() { if (mPendingImeControlRequest != null) { mPendingImeControlRequest.listener.onCancelled(null); mPendingImeControlRequest = null; mHandler.removeCallbacks(mPendingControlTimeout); } } @VisibleForTesting @Override public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) { cancelAnimation(runner, false /* invokeCallback */); if (shown) { showDirectly(runner.getTypes()); } else { hideDirectly(runner.getTypes(), true /* animationFinished */, runner.getAnimationType()); } } @Override public void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params) { mHost.applySurfaceParams(params); } void notifyControlRevoked(InsetsSourceConsumer consumer) { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; if ((control.getTypes() & toPublicType(consumer.getType())) != 0) { cancelAnimation(control, true /* invokeCallback */); } } if (consumer.getType() == ITYPE_IME) { abortPendingImeControlRequest(); } } private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) { if (invokeCallback) { control.cancel(); } for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { RunningAnimation runningAnimation = mRunningAnimations.get(i); if (runningAnimation.runner == control) { mRunningAnimations.remove(i); ArraySet types = toInternalType(control.getTypes()); for (int j = types.size() - 1; j >= 0; j--) { if (getSourceConsumer(types.valueAt(j)).notifyAnimationFinished()) { mHost.notifyInsetsChanged(); } } break; } } } private void applyLocalVisibilityOverride() { for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer controller = mSourceConsumers.valueAt(i); controller.applyLocalVisibilityOverride(); } } @VisibleForTesting public @NonNull InsetsSourceConsumer getSourceConsumer(@InternalInsetsType int type) { InsetsSourceConsumer controller = mSourceConsumers.get(type); if (controller != null) { return controller; } controller = mConsumerCreator.apply(this, type); mSourceConsumers.put(type, controller); return controller; } @VisibleForTesting public void notifyVisibilityChanged() { mHost.notifyInsetsChanged(); updateRequestedState(); } /** * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean) */ public void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible, boolean hasControl) { mHost.updateCompatSysUiVisibility(type, visible, hasControl); } /** * Called when current window gains focus. */ public void onWindowFocusGained() { getSourceConsumer(ITYPE_IME).onWindowFocusGained(); } /** * Called when current window loses focus. */ public void onWindowFocusLost() { getSourceConsumer(ITYPE_IME).onWindowFocusLost(); } /** * Used by {@link ImeInsetsSourceConsumer} when IME decides to be shown/hidden. * @hide */ @VisibleForTesting public void applyImeVisibility(boolean setVisible) { if (setVisible) { show(Type.IME, true /* fromIme */); } else { hide(Type.IME); } } @VisibleForTesting public @AnimationType int getAnimationType(@InternalInsetsType int type) { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; if (control.controlsInternalType(type)) { return mRunningAnimations.get(i).type; } } return ANIMATION_TYPE_NONE; } /** * Sends the local visibility state back to window manager if it is changed. */ private void updateRequestedState() { boolean changed = false; for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); final @InternalInsetsType int type = consumer.getType(); if (type == ITYPE_CAPTION_BAR) { continue; } if (consumer.getControl() != null) { final InsetsSource localSource = mState.getSource(type); if (!localSource.equals(mRequestedState.peekSource(type))) { // Our requested state is stale. Update it here and send it to window manager. mRequestedState.addSource(new InsetsSource(localSource)); changed = true; } if (!localSource.equals(mLastDispatchedState.peekSource(type))) { // The server state is not what we expected. This can happen while we don't have // the control. Since we have the control now, we need to send our request again // to modify the server state. changed = true; } } } if (!changed) { return; } mHost.onInsetsModified(mRequestedState); } @VisibleForTesting public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) { if (types == 0) { // nothing to animate. return; } boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks(); final InternalAnimationControlListener listener = new InternalAnimationControlListener(show, hasAnimationCallbacks, types); // Show/hide animations always need to be relative to the display frame, in order that shown // and hidden state insets are correct. controlAnimationUnchecked( types, null /* cancellationSignal */, listener, mState.getDisplayFrame(), fromIme, listener.getDurationMs(), listener.getInterpolator(), show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN, !hasAnimationCallbacks /* useInsetsAnimationThread */); } private void hideDirectly( @InsetsType int types, boolean animationFinished, @AnimationType int animationType) { final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).hide(animationFinished, animationType); } } private void showDirectly(@InsetsType int types) { final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).show(false /* fromIme */); } } /** * Cancel on-going animation to show/hide {@link InsetsType}. */ @VisibleForTesting public void cancelExistingAnimations() { cancelExistingControllers(all()); } void dump(String prefix, PrintWriter pw) { pw.println(prefix); pw.println("InsetsController:"); mState.dump(prefix + " ", pw); } @VisibleForTesting @Override public void startAnimation(InsetsAnimationControlImpl controller, WindowInsetsAnimationControlListener listener, int types, WindowInsetsAnimation animation, Bounds bounds) { mHost.dispatchWindowInsetsAnimationPrepare(animation); mHost.addOnPreDrawRunnable(() -> { if (controller.isCancelled()) { return; } for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { RunningAnimation runningAnimation = mRunningAnimations.get(i); if (runningAnimation.runner == controller) { runningAnimation.startDispatched = true; } } mHost.dispatchWindowInsetsAnimationStart(animation, bounds); mStartingAnimation = true; controller.mReadyDispatched = true; listener.onReady(controller, types); mStartingAnimation = false; }); } @VisibleForTesting public void dispatchAnimationEnd(WindowInsetsAnimation animation) { mHost.dispatchWindowInsetsAnimationEnd(animation); } @VisibleForTesting @Override public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) { if (mStartingAnimation || runner.getAnimationType() == ANIMATION_TYPE_USER) { mAnimCallback.run(); mAnimCallbackScheduled = false; return; } if (!mAnimCallbackScheduled) { mHost.postInsetsAnimationCallback(mAnimCallback); mAnimCallbackScheduled = true; } } @Override public void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask) { mHost.setSystemBarsAppearance(appearance, mask); } @Override public @Appearance int getSystemBarsAppearance() { return mHost.getSystemBarsAppearance(); } @Override public void setCaptionInsetsHeight(int height) { mCaptionInsetsHeight = height; } @Override public void setSystemBarsBehavior(@Behavior int behavior) { mHost.setSystemBarsBehavior(behavior); } @Override public @Appearance int getSystemBarsBehavior() { return mHost.getSystemBarsBehavior(); } private @InsetsType int calculateControllableTypes() { if (!checkDisplayFramesForControlling()) { return 0; } @InsetsType int result = 0; for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); if (consumer.getControl() != null) { result |= toPublicType(consumer.mType); } } return result; } /** * @return The types that are now animating due to a listener invoking control/show/hide */ private @InsetsType int invokeControllableInsetsChangedListeners() { mLastStartedAnimTypes = 0; @InsetsType int types = calculateControllableTypes(); int size = mControllableInsetsChangedListeners.size(); for (int i = 0; i < size; i++) { mControllableInsetsChangedListeners.get(i).onControllableInsetsChanged(this, types); } return mLastStartedAnimTypes; } @Override public void addOnControllableInsetsChangedListener( OnControllableInsetsChangedListener listener) { Objects.requireNonNull(listener); mControllableInsetsChangedListeners.add(listener); listener.onControllableInsetsChanged(this, calculateControllableTypes()); } @Override public void removeOnControllableInsetsChangedListener( OnControllableInsetsChangedListener listener) { Objects.requireNonNull(listener); mControllableInsetsChangedListeners.remove(listener); } @Override public void releaseSurfaceControlFromRt(SurfaceControl sc) { mHost.releaseSurfaceControlFromRt(sc); } Host getHost() { return mHost; } }