| /* |
| * 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.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; |
| import static android.os.Trace.TRACE_TAG_VIEW; |
| import static android.view.InsetsControllerProto.CONTROL; |
| import static android.view.InsetsControllerProto.STATE; |
| import static android.view.InsetsSource.ID_IME; |
| import static android.view.InsetsSource.ID_IME_CAPTION_BAR; |
| import static android.view.WindowInsets.Type.FIRST; |
| import static android.view.WindowInsets.Type.LAST; |
| import static android.view.WindowInsets.Type.all; |
| import static android.view.WindowInsets.Type.captionBar; |
| import static android.view.WindowInsets.Type.ime; |
| |
| import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; |
| |
| 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.app.ActivityThread; |
| import android.content.Context; |
| import android.content.res.CompatibilityInfo; |
| import android.graphics.Insets; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.CancellationSignal; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Trace; |
| import android.text.TextUtils; |
| import android.util.IntArray; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| import android.util.proto.ProtoOutputStream; |
| import android.view.InsetsSourceConsumer.ShowResult; |
| 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.ImeTracker; |
| import android.view.inputmethod.ImeTracker.InputMethodJankContext; |
| import android.view.inputmethod.InputMethodManager; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.inputmethod.ImeTracing; |
| import com.android.internal.inputmethod.SoftInputShowHideReason; |
| import com.android.internal.util.function.TriFunction; |
| |
| 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; |
| |
| /** |
| * Implements {@link WindowInsetsController} on the client. |
| * @hide |
| */ |
| public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks { |
| |
| private int mTypesBeingCancelled; |
| |
| 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<WindowInsetsAnimation> 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, int, int) |
| */ |
| default void updateCompatSysUiVisibility(@InsetsType int visibleTypes, |
| @InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) { } |
| |
| /** |
| * Called when the requested visibilities of insets have been modified by the client. |
| * The visibilities should be reported back to WM. |
| * |
| * @param types Bitwise flags of types requested visible. |
| */ |
| void updateRequestedVisibleTypes(@InsetsType int types); |
| |
| /** |
| * @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(); |
| |
| /** |
| * @return title of the rootView, if it has one. |
| * Note: this method is for debugging purposes only. |
| */ |
| @Nullable |
| String getRootViewTitle(); |
| |
| /** |
| * @return the context related to the rootView. |
| */ |
| @Nullable |
| default Context getRootViewContext() { |
| return null; |
| } |
| |
| /** @see ViewRootImpl#dipToPx */ |
| int dipToPx(int dips); |
| |
| /** |
| * @return token associated with the host, if it has one. |
| */ |
| @Nullable |
| IBinder getWindowToken(); |
| |
| /** |
| * @return Translator associated with the host, if it has one. |
| */ |
| @Nullable |
| default CompatibilityInfo.Translator getTranslator() { |
| return null; |
| } |
| |
| /** |
| * Notifies when the state of running animation is changed. The state is either "running" or |
| * "idle". |
| * |
| * @param running {@code true} if there is any animation running; {@code false} otherwise. |
| */ |
| default void notifyAnimationRunningStateChanged(boolean running) {} |
| |
| /** @see ViewRootImpl#isHandlingPointerEvent */ |
| default boolean isHandlingPointerEvent() { |
| return false; |
| } |
| } |
| |
| private static final String TAG = "InsetsController"; |
| private static final int ANIMATION_DURATION_MOVE_IN_MS = 275; |
| private static final int ANIMATION_DURATION_MOVE_OUT_MS = 340; |
| private static final int ANIMATION_DURATION_FADE_IN_MS = 500; |
| private static final int ANIMATION_DURATION_FADE_OUT_MS = 1500; |
| |
| /** Visible for WindowManagerWrapper */ |
| public static final int ANIMATION_DURATION_RESIZE = 300; |
| |
| private static final int ANIMATION_DELAY_DIM_MS = 500; |
| |
| 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; |
| |
| private static final Interpolator SYSTEM_BARS_INSETS_INTERPOLATOR = |
| new PathInterpolator(0.4f, 0f, 0.2f, 1f); |
| private static final Interpolator SYSTEM_BARS_ALPHA_INTERPOLATOR = |
| new PathInterpolator(0.3f, 0f, 1f, 1f); |
| private static final Interpolator SYSTEM_BARS_DIM_INTERPOLATOR = alphaFraction -> { |
| // While playing dim animation, alphaFraction is changed from 1f to 0f. Here changes it to |
| // time-based fraction for computing delay and interpolation. |
| float fraction = 1 - alphaFraction; |
| final float fractionDelay = (float) ANIMATION_DELAY_DIM_MS / ANIMATION_DURATION_FADE_OUT_MS; |
| if (fraction <= fractionDelay) { |
| return 1f; |
| } else { |
| float innerFraction = (fraction - fractionDelay) / (1f - fractionDelay); |
| return 1f - SYSTEM_BARS_ALPHA_INTERPOLATOR.getInterpolation(innerFraction); |
| } |
| }; |
| 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); |
| |
| /** Visible for WindowManagerWrapper */ |
| public static final Interpolator RESIZE_INTERPOLATOR = new LinearInterpolator(); |
| |
| /** The amount IME will move up/down when animating in floating mode. */ |
| private static final int FLOATING_IME_BOTTOM_INSET_DP = -80; |
| |
| private static final int ID_CAPTION_BAR = |
| InsetsSource.createId(null /* owner */, 0 /* index */, captionBar()); |
| |
| static final boolean DEBUG = false; |
| static final boolean WARN = false; |
| |
| /** |
| * 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}. |
| * <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 { |
| } |
| |
| /** Not running an animation. */ |
| @VisibleForTesting |
| public static final int ANIMATION_TYPE_NONE = -1; |
| |
| /** Running animation will show insets */ |
| public static final int ANIMATION_TYPE_SHOW = 0; |
| |
| /** Running animation will hide insets */ |
| public static final int ANIMATION_TYPE_HIDE = 1; |
| |
| /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */ |
| @VisibleForTesting(visibility = PACKAGE) |
| public static final int ANIMATION_TYPE_USER = 2; |
| |
| /** Running animation will resize insets */ |
| @VisibleForTesting |
| public static final int ANIMATION_TYPE_RESIZE = 3; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(value = {ANIMATION_TYPE_NONE, ANIMATION_TYPE_SHOW, ANIMATION_TYPE_HIDE, |
| ANIMATION_TYPE_USER, ANIMATION_TYPE_RESIZE}) |
| public @interface AnimationType { |
| } |
| |
| /** |
| * Translation animation evaluator. |
| */ |
| private static TypeEvaluator<Insets> 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))); |
| |
| /** Logging listener. */ |
| private WindowInsetsAnimationControlListener mLoggingListener; |
| |
| /** Context for {@link android.view.inputmethod.ImeTracker.ImeJankTracker} to monitor jank. */ |
| private final InputMethodJankContext mJankContext = new InputMethodJankContext() { |
| @Override |
| public Context getDisplayContext() { |
| return mHost != null ? mHost.getRootViewContext() : null; |
| } |
| |
| @Override |
| public SurfaceControl getTargetSurfaceControl() { |
| final InsetsSourceControl imeSourceControl = getImeSourceConsumer().getControl(); |
| return imeSourceControl != null ? imeSourceControl.getLeash() : null; |
| } |
| |
| @Override |
| public String getHostPackageName() { |
| return mHost != null ? mHost.getRootViewContext().getPackageName() : null; |
| } |
| }; |
| |
| /** |
| * 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 @Behavior int mBehavior; |
| private final long mDurationMs; |
| private final boolean mDisable; |
| private final int mFloatingImeBottomInset; |
| private final WindowInsetsAnimationControlListener mLoggingListener; |
| private final InputMethodJankContext mInputMethodJankContext; |
| |
| public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks, |
| @InsetsType int requestedTypes, @Behavior int behavior, boolean disable, |
| int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener, |
| @Nullable InputMethodJankContext jankContext) { |
| mShow = show; |
| mHasAnimationCallbacks = hasAnimationCallbacks; |
| mRequestedTypes = requestedTypes; |
| mBehavior = behavior; |
| mDurationMs = calculateDurationMs(); |
| mDisable = disable; |
| mFloatingImeBottomInset = floatingImeBottomInset; |
| mLoggingListener = loggingListener; |
| mInputMethodJankContext = jankContext; |
| } |
| |
| @Override |
| public void onReady(WindowInsetsAnimationController controller, int types) { |
| mController = controller; |
| if (DEBUG) Log.d(TAG, "default animation onReady types: " + types); |
| if (mLoggingListener != null) { |
| mLoggingListener.onReady(controller, types); |
| } |
| |
| if (mDisable) { |
| onAnimationFinish(); |
| return; |
| } |
| mAnimator = ValueAnimator.ofFloat(0f, 1f); |
| mAnimator.setDuration(mDurationMs); |
| mAnimator.setInterpolator(new LinearInterpolator()); |
| Insets hiddenInsets = controller.getHiddenStateInsets(); |
| // IME with zero insets is a special case: it will animate-in from offscreen and end |
| // with final insets of zero and vice-versa. |
| hiddenInsets = controller.hasZeroInsetsIme() |
| ? Insets.of(hiddenInsets.left, hiddenInsets.top, hiddenInsets.right, |
| mFloatingImeBottomInset) |
| : hiddenInsets; |
| Insets start = mShow |
| ? hiddenInsets |
| : controller.getShownStateInsets(); |
| Insets end = mShow |
| ? controller.getShownStateInsets() |
| : hiddenInsets; |
| Interpolator insetsInterpolator = getInsetsInterpolator(); |
| 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); |
| if (DEBUG) Log.d(TAG, "Default animation setInsetsAndAlpha fraction: " |
| + insetsFraction); |
| }); |
| mAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| if (mInputMethodJankContext == null) return; |
| ImeTracker.forJank().onRequestAnimation( |
| mInputMethodJankContext, |
| getAnimationType(), |
| !mHasAnimationCallbacks); |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| if (mInputMethodJankContext == null) return; |
| ImeTracker.forJank().onCancelAnimation(getAnimationType()); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| onAnimationFinish(); |
| if (mInputMethodJankContext == null) return; |
| ImeTracker.forJank().onFinishAnimation(getAnimationType()); |
| } |
| }); |
| mAnimator.start(); |
| } |
| |
| @Override |
| public void onFinished(WindowInsetsAnimationController controller) { |
| if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:" |
| + Type.toString(mRequestedTypes)); |
| if (mLoggingListener != null) { |
| mLoggingListener.onFinished(controller); |
| } |
| } |
| |
| @Override |
| public void onCancelled(WindowInsetsAnimationController controller) { |
| // Animator can be null when it is cancelled before onReady() completes. |
| if (mAnimator != null) { |
| mAnimator.cancel(); |
| } |
| if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:" |
| + mRequestedTypes); |
| if (mLoggingListener != null) { |
| mLoggingListener.onCancelled(controller); |
| } |
| } |
| |
| protected Interpolator getInsetsInterpolator() { |
| 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 { |
| if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) { |
| return SYSTEM_BARS_INSETS_INTERPOLATOR; |
| } else { |
| // Makes insets stay at the shown position. |
| return input -> mShow ? 1f : 0f; |
| } |
| } |
| } |
| |
| 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 { |
| if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) { |
| return input -> 1f; |
| } else { |
| if (mShow) { |
| return SYSTEM_BARS_ALPHA_INTERPOLATOR; |
| } else { |
| return SYSTEM_BARS_DIM_INTERPOLATOR; |
| } |
| } |
| } |
| } |
| |
| protected void onAnimationFinish() { |
| mController.finish(mShow); |
| if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + 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 { |
| if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) { |
| return mShow ? ANIMATION_DURATION_MOVE_IN_MS : ANIMATION_DURATION_MOVE_OUT_MS; |
| } else { |
| return mShow ? ANIMATION_DURATION_FADE_IN_MS : ANIMATION_DURATION_FADE_OUT_MS; |
| } |
| } |
| } |
| |
| /** |
| * Returns the current animation type. |
| */ |
| @AnimationType |
| private int getAnimationType() { |
| return mShow ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE; |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| @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; |
| } |
| |
| /** The local state */ |
| private final InsetsState mState = new InsetsState(); |
| |
| /** The state dispatched from server */ |
| private final InsetsState mLastDispatchedState = new InsetsState(); |
| |
| private final Rect mFrame = new Rect(); |
| private final TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer> |
| mConsumerCreator; |
| private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>(); |
| private final InsetsSourceConsumer mImeSourceConsumer; |
| private final Host mHost; |
| private final Handler mHandler; |
| |
| private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>(); |
| private final ArrayList<RunningAnimation> mRunningAnimations = 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 */ |
| @Nullable |
| private PendingControlRequest mPendingImeControlRequest; |
| |
| private int mWindowType; |
| private int mLastLegacySoftInputMode; |
| private int mLastLegacyWindowFlags; |
| private int mLastLegacySystemUiFlags; |
| private int mLastActivityType; |
| private boolean mStartingAnimation; |
| private int mCaptionInsetsHeight = 0; |
| private int mImeCaptionBarInsetsHeight = 0; |
| private boolean mAnimationsDisabled; |
| private boolean mCompatSysUiVisibilityStaled; |
| private @Appearance int mAppearanceControlled; |
| private @Appearance int mAppearanceFromResource; |
| private boolean mBehaviorControlled; |
| private boolean mIsPredictiveBackImeHideAnimInProgress; |
| |
| private final Runnable mPendingControlTimeout = this::abortPendingImeControlRequest; |
| private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners |
| = new ArrayList<>(); |
| |
| /** Set of inset types for which an animation was started since last resetting this field */ |
| private @InsetsType int mLastStartedAnimTypes; |
| |
| /** Set of inset types which are existing */ |
| private @InsetsType int mExistingTypes = 0; |
| |
| /** Set of inset types which are visible */ |
| private @InsetsType int mVisibleTypes = WindowInsets.Type.defaultVisible(); |
| |
| /** Set of inset types which are requested visible */ |
| private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); |
| |
| /** Set of inset types which are requested visible which are reported to the host */ |
| private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); |
| |
| /** Set of inset types that we have controls of */ |
| private @InsetsType int mControllableTypes; |
| |
| private final Runnable mInvokeControllableInsetsChangedListeners = |
| this::invokeControllableInsetsChangedListeners; |
| |
| private final InsetsState.OnTraverseCallbacks mRemoveGoneSources = |
| new InsetsState.OnTraverseCallbacks() { |
| |
| private final IntArray mPendingRemoveIndexes = new IntArray(); |
| |
| @Override |
| public void onIdNotFoundInState2(int index1, InsetsSource source1) { |
| if (source1.getId() == ID_IME_CAPTION_BAR) { |
| return; |
| } |
| |
| // Don't change the indexes of the sources while traversing. Remove it later. |
| mPendingRemoveIndexes.add(index1); |
| } |
| |
| @Override |
| public void onFinish(InsetsState state1, InsetsState state2) { |
| for (int i = mPendingRemoveIndexes.size() - 1; i >= 0; i--) { |
| state1.removeSourceAt(mPendingRemoveIndexes.get(i)); |
| } |
| mPendingRemoveIndexes.clear(); |
| } |
| }; |
| |
| private final InsetsState.OnTraverseCallbacks mStartResizingAnimationIfNeeded = |
| new InsetsState.OnTraverseCallbacks() { |
| |
| private @InsetsType int mTypes; |
| private InsetsState mToState; |
| |
| @Override |
| public void onStart(InsetsState state1, InsetsState state2) { |
| mTypes = 0; |
| mToState = null; |
| } |
| |
| @Override |
| public void onIdMatch(InsetsSource source1, InsetsSource source2) { |
| final Rect frame1 = source1.getFrame(); |
| final Rect frame2 = source2.getFrame(); |
| if (!source1.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) |
| || !source2.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) |
| || !source1.isVisible() || !source2.isVisible() |
| || frame1.equals(frame2) || frame1.isEmpty() || frame2.isEmpty() |
| || !(Rect.intersects(mFrame, source1.getFrame()) |
| || Rect.intersects(mFrame, source2.getFrame()))) { |
| return; |
| } |
| mTypes |= source1.getType(); |
| if (mToState == null) { |
| mToState = new InsetsState(); |
| } |
| mToState.addSource(new InsetsSource(source2)); |
| } |
| |
| @Override |
| public void onFinish(InsetsState state1, InsetsState state2) { |
| if (mTypes == 0) { |
| return; |
| } |
| cancelExistingControllers(mTypes); |
| final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner( |
| mFrame, state1, mToState, RESIZE_INTERPOLATOR, |
| ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this); |
| if (mRunningAnimations.isEmpty()) { |
| mHost.notifyAnimationRunningStateChanged(true); |
| } |
| mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType())); |
| } |
| }; |
| |
| public InsetsController(Host host) { |
| this(host, (controller, id, type) -> { |
| if (type == ime()) { |
| return new ImeInsetsSourceConsumer(id, controller.mState, |
| Transaction::new, controller); |
| } else { |
| return new InsetsSourceConsumer(id, type, controller.mState, |
| Transaction::new, controller); |
| } |
| }, host.getHandler()); |
| } |
| |
| @VisibleForTesting |
| public InsetsController(Host host, |
| TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer> consumerCreator, |
| Handler handler) { |
| mHost = host; |
| mConsumerCreator = consumerCreator; |
| mHandler = handler; |
| mAnimCallback = () -> { |
| mAnimCallbackScheduled = false; |
| if (mRunningAnimations.isEmpty()) { |
| return; |
| } |
| |
| final List<WindowInsetsAnimation> runningAnimations = new ArrayList<>(); |
| final List<WindowInsetsAnimation> finishedAnimations = new ArrayList<>(); |
| final InsetsState state = new InsetsState(mState, true /* copySources */); |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| RunningAnimation runningAnimation = mRunningAnimations.get(i); |
| if (DEBUG) Log.d(TAG, "Running animation type: " + runningAnimation.type); |
| final InsetsAnimationControlRunner runner = runningAnimation.runner; |
| if (runner instanceof WindowInsetsAnimationController) { |
| |
| // 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) { |
| runningAnimations.add(runner.getAnimation()); |
| } |
| |
| if (((InternalInsetsAnimationController) runner).applyChangeInsets(state)) { |
| finishedAnimations.add(runner.getAnimation()); |
| } |
| } |
| } |
| |
| WindowInsets insets = state.calculateInsets(mFrame, |
| mState /* ignoringVisibilityState */, mLastInsets.isRound(), |
| mLastLegacySoftInputMode, mLastLegacyWindowFlags, mLastLegacySystemUiFlags, |
| mWindowType, mLastActivityType, null /* idSideMap */); |
| mHost.dispatchWindowInsetsAnimationProgress(insets, |
| Collections.unmodifiableList(runningAnimations)); |
| if (DEBUG) { |
| for (WindowInsetsAnimation anim : runningAnimations) { |
| Log.d(TAG, String.format("Running animation type: %d, progress: %f", |
| anim.getTypeMask(), anim.getInterpolatedFraction())); |
| } |
| } |
| |
| for (int i = finishedAnimations.size() - 1; i >= 0; i--) { |
| dispatchAnimationEnd(finishedAnimations.get(i)); |
| } |
| }; |
| |
| // Make mImeSourceConsumer always non-null. |
| mImeSourceConsumer = getSourceConsumer(ID_IME, ime()); |
| } |
| |
| @VisibleForTesting |
| public void onFrameChanged(Rect frame) { |
| if (mFrame.equals(frame)) { |
| return; |
| } |
| if (mImeCaptionBarInsetsHeight != 0) { |
| setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight); |
| } |
| mHost.notifyInsetsChanged(); |
| mFrame.set(frame); |
| } |
| |
| @Override |
| public InsetsState getState() { |
| return mState; |
| } |
| |
| @Override |
| public @InsetsType int getRequestedVisibleTypes() { |
| return mRequestedVisibleTypes; |
| } |
| |
| public InsetsState getLastDispatchedState() { |
| return mLastDispatchedState; |
| } |
| |
| public boolean onStateChanged(InsetsState state) { |
| boolean stateChanged = !mState.equals(state, false /* excludesCaptionBar */, |
| false /* excludesInvisibleIme */); |
| if (!stateChanged && mLastDispatchedState.equals(state)) { |
| return false; |
| } |
| if (DEBUG) Log.d(TAG, "onStateChanged: " + state); |
| |
| final InsetsState lastState = new InsetsState(mState, true /* copySources */); |
| updateState(state); |
| applyLocalVisibilityOverride(); |
| updateCompatSysUiVisibility(); |
| |
| if (!mState.equals(lastState, false /* excludesCaptionBar */, |
| true /* excludesInvisibleIme */)) { |
| if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged"); |
| mHost.notifyInsetsChanged(); |
| if (mLastDispatchedState.getDisplayFrame().equals(state.getDisplayFrame())) { |
| // Here compares the raw states instead of the overridden ones because we don't want |
| // to animate an insets source that its mServerVisible is false. |
| InsetsState.traverse(mLastDispatchedState, state, mStartResizingAnimationIfNeeded); |
| } |
| } |
| mLastDispatchedState.set(state, true /* copySources */); |
| return true; |
| } |
| |
| private void updateState(InsetsState newState) { |
| mState.set(newState, 0 /* types */); |
| @InsetsType int existingTypes = 0; |
| @InsetsType int visibleTypes = 0; |
| @InsetsType int[] cancelledUserAnimationTypes = {0}; |
| for (int i = 0, size = newState.sourceSize(); i < size; i++) { |
| final InsetsSource source = newState.sourceAt(i); |
| @InsetsType int type = source.getType(); |
| @AnimationType int animationType = getAnimationType(type); |
| final InsetsSourceConsumer consumer = mSourceConsumers.get(source.getId()); |
| if (consumer != null) { |
| consumer.updateSource(source, animationType); |
| } else { |
| mState.addSource(source); |
| } |
| existingTypes |= type; |
| if (source.isVisible()) { |
| visibleTypes |= type; |
| } |
| } |
| |
| // If a type doesn't have a source, treat it as visible if it is visible by default. |
| visibleTypes |= WindowInsets.Type.defaultVisible() & ~existingTypes; |
| |
| if (mVisibleTypes != visibleTypes) { |
| if (WindowInsets.Type.hasCompatSystemBars(mVisibleTypes ^ visibleTypes)) { |
| mCompatSysUiVisibilityStaled = true; |
| } |
| mVisibleTypes = visibleTypes; |
| } |
| if (mExistingTypes != existingTypes) { |
| if (WindowInsets.Type.hasCompatSystemBars(mExistingTypes ^ existingTypes)) { |
| mCompatSysUiVisibilityStaled = true; |
| } |
| mExistingTypes = existingTypes; |
| } |
| InsetsState.traverse(mState, newState, mRemoveGoneSources); |
| |
| if (cancelledUserAnimationTypes[0] != 0) { |
| mHandler.post(() -> show(cancelledUserAnimationTypes[0])); |
| } |
| } |
| |
| /** |
| * @see InsetsState#calculateInsets(Rect, InsetsState, boolean, int, int, int, int, int, |
| * android.util.SparseIntArray) |
| */ |
| @VisibleForTesting |
| public WindowInsets calculateInsets(boolean isScreenRound, int windowType, int activityType, |
| int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags) { |
| mWindowType = windowType; |
| mLastActivityType = activityType; |
| mLastLegacySoftInputMode = legacySoftInputMode; |
| mLastLegacyWindowFlags = legacyWindowFlags; |
| mLastLegacySystemUiFlags = legacySystemUiFlags; |
| mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState */, |
| isScreenRound, legacySoftInputMode, legacyWindowFlags, |
| legacySystemUiFlags, windowType, activityType, null /* idSideMap */); |
| return mLastInsets; |
| } |
| |
| /** |
| * @see InsetsState#calculateVisibleInsets(Rect, int, int, int, int) |
| */ |
| public Insets calculateVisibleInsets(int windowType, int activityType, |
| @SoftInputModeFlags int softInputMode, int windowFlags) { |
| return mState.calculateVisibleInsets(mFrame, windowType, activityType, softInputMode, |
| windowFlags); |
| } |
| |
| /** |
| * 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.getId(), activeControl); |
| } |
| } |
| } |
| |
| @InsetsType int controllableTypes = 0; |
| int consumedControlCount = 0; |
| final @InsetsType int[] showTypes = new int[1]; |
| final @InsetsType 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); |
| if (consumer.getId() == ID_IME_CAPTION_BAR) { |
| // The inset control for the IME caption bar will never be dispatched |
| // by the server. |
| continue; |
| } |
| |
| final InsetsSourceControl control = mTmpControlArray.get(consumer.getId()); |
| if (control != null) { |
| controllableTypes |= control.getType(); |
| consumedControlCount++; |
| } |
| |
| // 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. |
| if (consumedControlCount != mTmpControlArray.size()) { |
| for (int i = mTmpControlArray.size() - 1; i >= 0; i--) { |
| final InsetsSourceControl control = mTmpControlArray.valueAt(i); |
| getSourceConsumer(control.getId(), control.getType()) |
| .setControl(control, showTypes, hideTypes); |
| } |
| } |
| |
| if (mTmpControlArray.size() > 0) { |
| // Update surface positions for animations. |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| mRunningAnimations.get(i).runner.updateSurfacePosition(mTmpControlArray); |
| } |
| } |
| 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) { |
| final var statsToken = (showTypes[0] & ime()) == 0 ? null |
| : ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW, |
| ImeTracker.ORIGIN_CLIENT, SoftInputShowHideReason.CONTROLS_CHANGED, |
| mHost.isHandlingPointerEvent() /* fromUser */); |
| applyAnimation(showTypes[0], true /* show */, false /* fromIme */, statsToken); |
| } |
| if (hideTypes[0] != 0) { |
| final var statsToken = (hideTypes[0] & ime()) == 0 ? null |
| : ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, |
| ImeTracker.ORIGIN_CLIENT, SoftInputShowHideReason.CONTROLS_CHANGED, |
| mHost.isHandlingPointerEvent() /* fromUser */); |
| applyAnimation(hideTypes[0], false /* show */, false /* fromIme */, statsToken); |
| } |
| |
| if (mControllableTypes != controllableTypes) { |
| if (WindowInsets.Type.hasCompatSystemBars(mControllableTypes ^ controllableTypes)) { |
| mCompatSysUiVisibilityStaled = true; |
| } |
| mControllableTypes = controllableTypes; |
| } |
| |
| // InsetsSourceConsumer#setControl might change the requested visibility. |
| reportRequestedVisibleTypes(); |
| } |
| |
| @VisibleForTesting(visibility = PACKAGE) |
| public void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) { |
| mIsPredictiveBackImeHideAnimInProgress = isInProgress; |
| if (isInProgress) { |
| // The InsetsAnimationControlRunner has layoutInsetsDuringAnimation set to SHOWN during |
| // predictive back. Let's set it to HIDDEN once the predictive back animation enters the |
| // post-commit phase. |
| // That prevents flickers in case the animation is cancelled by an incoming show request |
| // during the hide animation. |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; |
| if ((runner.getTypes() & ime()) != 0) { |
| runner.updateLayoutInsetsDuringAnimation(LAYOUT_INSETS_DURING_ANIMATION_HIDDEN); |
| break; |
| } |
| } |
| } |
| } |
| |
| boolean isPredictiveBackImeHideAnimInProgress() { |
| return mIsPredictiveBackImeHideAnimInProgress; |
| } |
| |
| @Override |
| public void show(@InsetsType int types) { |
| show(types, false /* fromIme */, null /* statsToken */); |
| } |
| |
| @VisibleForTesting(visibility = PACKAGE) |
| public void show(@InsetsType int types, boolean fromIme, |
| @Nullable ImeTracker.Token statsToken) { |
| if ((types & ime()) != 0) { |
| Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")"); |
| |
| if (statsToken == null) { |
| statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW, |
| ImeTracker.ORIGIN_CLIENT, |
| SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API, |
| mHost.isHandlingPointerEvent() /* fromUser */); |
| } |
| } |
| if (fromIme) { |
| ImeTracing.getInstance().triggerClientDump("InsetsController#show", |
| mHost.getInputMethodManager(), null /* icProto */); |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0); |
| } else { |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| } |
| // Handle pending request ready in case there was one set. |
| if (fromIme && mPendingImeControlRequest != null) { |
| if ((types & Type.ime()) != 0) { |
| ImeTracker.forLatency().onShown(statsToken, ActivityThread::currentApplication); |
| } |
| handlePendingControlRequest(statsToken); |
| return; |
| } |
| |
| // TODO: Support a ResultReceiver for IME. |
| // TODO(b/123718661): Make show() work for multi-session IME. |
| int typesReady = 0; |
| final boolean imeVisible = mState.isSourceOrDefaultVisible( |
| mImeSourceConsumer.getId(), ime()); |
| for (int type = FIRST; type <= LAST; type = type << 1) { |
| if ((types & type) == 0) { |
| continue; |
| } |
| @AnimationType final int animationType = getAnimationType(type); |
| final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0; |
| final boolean isIme = type == ime(); |
| var alreadyVisible = requestedVisible && (!isIme || imeVisible) |
| && animationType == ANIMATION_TYPE_NONE; |
| var alreadyAnimatingShow = animationType == ANIMATION_TYPE_SHOW; |
| if (alreadyVisible || alreadyAnimatingShow) { |
| // no-op: already shown or animating in (because window visibility is |
| // applied before starting animation). |
| if (DEBUG) Log.d(TAG, String.format( |
| "show ignored for type: %d animType: %d requestedVisible: %s", |
| type, animationType, requestedVisible)); |
| if (isIme) { |
| ImeTracker.forLogging().onCancelled(statsToken, |
| ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); |
| } |
| continue; |
| } |
| if (fromIme && animationType == ANIMATION_TYPE_USER |
| && !mIsPredictiveBackImeHideAnimInProgress) { |
| // App is already controlling the IME, don't cancel it. |
| if (isIme) { |
| ImeTracker.forLogging().onFailed( |
| statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); |
| } |
| continue; |
| } |
| if (isIme) { |
| ImeTracker.forLogging().onProgress( |
| statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); |
| } |
| typesReady |= type; |
| } |
| if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady); |
| if (fromIme && (typesReady & Type.ime()) != 0) { |
| ImeTracker.forLatency().onShown(statsToken, ActivityThread::currentApplication); |
| } |
| applyAnimation(typesReady, true /* show */, fromIme, statsToken); |
| } |
| |
| /** |
| * Handle the {@link #mPendingImeControlRequest} when: |
| * <ul> |
| * <li> The IME insets is ready to show. |
| * <li> The IME insets has being requested invisible. |
| * </ul> |
| */ |
| private void handlePendingControlRequest(@Nullable ImeTracker.Token statsToken) { |
| PendingControlRequest pendingRequest = mPendingImeControlRequest; |
| mPendingImeControlRequest = null; |
| mHandler.removeCallbacks(mPendingControlTimeout); |
| |
| // We are about to playing the default animation. Passing a null frame indicates the |
| // controlled types should be animated regardless of the frame. |
| controlAnimationUnchecked( |
| pendingRequest.types, pendingRequest.cancellationSignal, |
| pendingRequest.listener, null /* frame */, |
| true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator, |
| pendingRequest.animationType, |
| pendingRequest.layoutInsetsDuringAnimation, |
| pendingRequest.useInsetsAnimationThread, statsToken); |
| } |
| |
| @Override |
| public void hide(@InsetsType int types) { |
| hide(types, false /* fromIme */, null /* statsToken */); |
| } |
| |
| @VisibleForTesting |
| public void hide(@InsetsType int types, boolean fromIme, |
| @Nullable ImeTracker.Token statsToken) { |
| if ((types & ime()) != 0) { |
| Log.d(TAG, "hide(ime(), fromIme=" + fromIme + ")"); |
| |
| if (statsToken == null) { |
| statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, |
| ImeTracker.ORIGIN_CLIENT, |
| SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API, |
| mHost.isHandlingPointerEvent() /* fromUser */); |
| } |
| } |
| if (fromIme) { |
| ImeTracing.getInstance().triggerClientDump("InsetsController#hide", |
| mHost.getInputMethodManager(), null /* icProto */); |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0); |
| } else { |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0); |
| } |
| int typesReady = 0; |
| boolean hasImeRequestedHidden = false; |
| final boolean hadPendingImeControlRequest = mPendingImeControlRequest != null; |
| for (int type = FIRST; type <= LAST; type = type << 1) { |
| if ((types & type) == 0) { |
| continue; |
| } |
| @AnimationType final int animationType = getAnimationType(type); |
| final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0; |
| final boolean isImeAnimation = type == ime(); |
| if (mPendingImeControlRequest != null && !requestedVisible) { |
| // Remove the hide insets type from the pending show request. |
| mPendingImeControlRequest.types &= ~type; |
| if (mPendingImeControlRequest.types == 0) { |
| abortPendingImeControlRequest(); |
| } |
| } |
| if (isImeAnimation && !requestedVisible && animationType == ANIMATION_TYPE_NONE) { |
| hasImeRequestedHidden = true; |
| // Ensure to request hide IME in case there is any pending requested visible |
| // being applied from setControl when receiving the insets control. |
| if (hadPendingImeControlRequest |
| || getImeSourceConsumer().isRequestedVisibleAwaitingControl()) { |
| getImeSourceConsumer().requestHide(fromIme, statsToken); |
| } |
| } |
| if (!requestedVisible && animationType == ANIMATION_TYPE_NONE |
| || animationType == ANIMATION_TYPE_HIDE || (animationType |
| == ANIMATION_TYPE_USER && mIsPredictiveBackImeHideAnimInProgress)) { |
| // no-op: already hidden or animating out (because window visibility is |
| // applied before starting animation). |
| if (isImeAnimation) { |
| ImeTracker.forLogging().onCancelled(statsToken, |
| ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); |
| } |
| continue; |
| } |
| if (isImeAnimation) { |
| ImeTracker.forLogging().onProgress( |
| statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); |
| } |
| typesReady |= type; |
| } |
| if (hasImeRequestedHidden && mPendingImeControlRequest != null) { |
| // Handle the pending show request for other insets types since the IME insets has being |
| // requested hidden. |
| handlePendingControlRequest(statsToken); |
| getImeSourceConsumer().removeSurface(); |
| } |
| applyAnimation(typesReady, false /* show */, fromIme, statsToken); |
| } |
| |
| @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, |
| false /* fromPredictiveBack */); |
| } |
| |
| @VisibleForTesting(visibility = PACKAGE) |
| public void controlWindowInsetsAnimation(@InsetsType int types, |
| @Nullable CancellationSignal cancellationSignal, |
| WindowInsetsAnimationControlListener listener, |
| boolean fromIme, long durationMs, @Nullable Interpolator interpolator, |
| @AnimationType int animationType, boolean fromPredictiveBack) { |
| if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) { |
| listener.onCancelled(null); |
| return; |
| } |
| if (fromIme) { |
| ImeTracing.getInstance().triggerClientDump( |
| "InsetsController#controlWindowInsetsAnimation", |
| mHost.getInputMethodManager(), null /* icProto */); |
| } |
| |
| controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs, |
| interpolator, animationType, |
| getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack), |
| false /* useInsetsAnimationThread */, null /* statsToken */); |
| } |
| |
| private void controlAnimationUnchecked(@InsetsType int types, |
| @Nullable CancellationSignal cancellationSignal, |
| WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, |
| long durationMs, Interpolator interpolator, |
| @AnimationType int animationType, |
| @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, |
| boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { |
| final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN; |
| |
| // Basically, we accept the requested visibilities from the upstream callers... |
| setRequestedVisibleTypes(visible ? types : 0, types); |
| |
| // However, we might reject the request in some cases, such as delaying showing IME or |
| // rejecting showing IME. |
| controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme, |
| durationMs, interpolator, animationType, layoutInsetsDuringAnimation, |
| useInsetsAnimationThread, statsToken); |
| |
| // We are finishing setting the requested visible types. Report them to the server and/or |
| // the app. |
| reportRequestedVisibleTypes(); |
| } |
| |
| private void controlAnimationUncheckedInner(@InsetsType int types, |
| @Nullable CancellationSignal cancellationSignal, |
| WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, |
| long durationMs, Interpolator interpolator, |
| @AnimationType int animationType, |
| @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, |
| boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { |
| if ((types & mTypesBeingCancelled) != 0) { |
| final boolean monitoredAnimation = |
| animationType == ANIMATION_TYPE_SHOW || animationType == ANIMATION_TYPE_HIDE; |
| if (monitoredAnimation && (types & Type.ime()) != 0) { |
| if (animationType == ANIMATION_TYPE_SHOW) { |
| ImeTracker.forLatency().onShowCancelled(statsToken, |
| ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL, |
| ActivityThread::currentApplication); |
| } else { |
| ImeTracker.forLatency().onHideCancelled(statsToken, |
| ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL, |
| ActivityThread::currentApplication); |
| } |
| ImeTracker.forLogging().onCancelled(statsToken, |
| ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION); |
| } |
| throw new IllegalStateException("Cannot start a new insets animation of " |
| + Type.toString(types) |
| + " while an existing " + Type.toString(mTypesBeingCancelled) |
| + " is being cancelled."); |
| } |
| ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION); |
| if (types == 0) { |
| // nothing to animate. |
| listener.onCancelled(null); |
| if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked"); |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| return; |
| } |
| if (DEBUG) Log.d(TAG, "controlAnimation types: " + types); |
| mLastStartedAnimTypes |= types; |
| |
| final SparseArray<InsetsSourceControl> controls = new SparseArray<>(); |
| |
| Pair<Integer, Boolean> typesReadyPair = collectSourceControls( |
| fromIme, types, controls, animationType, statsToken); |
| int typesReady = typesReadyPair.first; |
| boolean imeReady = typesReadyPair.second; |
| if (DEBUG) Log.d(TAG, String.format( |
| "controlAnimationUnchecked, typesReady: %s imeReady: %s", typesReady, imeReady)); |
| 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 (DEBUG) Log.d(TAG, "Ime not ready. Create pending request"); |
| if (cancellationSignal != null) { |
| cancellationSignal.setOnCancelListener(() -> { |
| if (mPendingImeControlRequest == request) { |
| if (DEBUG) Log.d(TAG, |
| "Cancellation signal abortPendingImeControlRequest"); |
| abortPendingImeControlRequest(); |
| } |
| }); |
| } |
| |
| // The leashes are copied, but they won't be used. |
| releaseControls(controls); |
| |
| // The requested visibilities should be delayed as well. Otherwise, we might override |
| // the insets visibility before playing animation. |
| setRequestedVisibleTypes(mReportedRequestedVisibleTypes, types); |
| |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); |
| if (!fromIme) { |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| } |
| return; |
| } |
| |
| if (typesReady == 0) { |
| if (DEBUG) Log.d(TAG, "No types ready. onCancelled()"); |
| listener.onCancelled(null); |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); |
| if (!fromIme) { |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| } |
| return; |
| } |
| |
| cancelExistingControllers(typesReady); |
| |
| final InsetsAnimationControlRunner runner = useInsetsAnimationThread |
| ? new InsetsAnimationThreadControlRunner(controls, |
| frame, mState, listener, typesReady, this, durationMs, interpolator, |
| animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), |
| mHost.getHandler(), statsToken) |
| : new InsetsAnimationControlImpl(controls, |
| frame, mState, listener, typesReady, this, durationMs, interpolator, |
| animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), |
| statsToken); |
| if ((typesReady & WindowInsets.Type.ime()) != 0) { |
| ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl", |
| mHost.getInputMethodManager(), null /* icProto */); |
| if (animationType == ANIMATION_TYPE_HIDE) { |
| ImeTracker.forLatency().onHidden(statsToken, ActivityThread::currentApplication); |
| } |
| } |
| ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING); |
| if (mRunningAnimations.isEmpty()) { |
| mHost.notifyAnimationRunningStateChanged(true); |
| } |
| mRunningAnimations.add(new RunningAnimation(runner, animationType)); |
| if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: " |
| + useInsetsAnimationThread); |
| if (cancellationSignal != null) { |
| cancellationSignal.setOnCancelListener(() -> { |
| cancelAnimation(runner, true /* invokeCallback */); |
| }); |
| } else { |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.pendingAnim", 0); |
| } |
| onAnimationStateChanged(types, true /* running */); |
| |
| if (fromIme) { |
| switch (animationType) { |
| case ANIMATION_TYPE_SHOW: |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0); |
| break; |
| case ANIMATION_TYPE_HIDE: |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0); |
| break; |
| } |
| } else if (animationType == ANIMATION_TYPE_HIDE) { |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0); |
| } |
| } |
| |
| static void releaseControls(SparseArray<InsetsSourceControl> controls) { |
| for (int i = controls.size() - 1; i >= 0; i--) { |
| controls.valueAt(i).release(SurfaceControl::release); |
| } |
| } |
| |
| // TODO(b/242962223): Make this setter restrictive. |
| @Override |
| public void setSystemDrivenInsetsAnimationLoggingListener( |
| @Nullable WindowInsetsAnimationControlListener listener) { |
| mLoggingListener = listener; |
| } |
| |
| /** |
| * @return Pair of (types ready to animate, IME ready to animate). |
| */ |
| private Pair<Integer, Boolean> collectSourceControls(boolean fromIme, @InsetsType int types, |
| SparseArray<InsetsSourceControl> controls, @AnimationType int animationType, |
| @Nullable ImeTracker.Token statsToken) { |
| ImeTracker.forLogging().onProgress(statsToken, |
| ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS); |
| |
| int typesReady = 0; |
| boolean imeReady = true; |
| for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { |
| final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); |
| if ((consumer.getType() & types) == 0) { |
| continue; |
| } |
| boolean show = animationType == ANIMATION_TYPE_SHOW |
| || animationType == ANIMATION_TYPE_USER; |
| boolean canRun = true; |
| if (show) { |
| // Show request |
| switch(consumer.requestShow(fromIme, statsToken)) { |
| case ShowResult.SHOW_IMMEDIATELY: |
| break; |
| case ShowResult.IME_SHOW_DELAYED: |
| imeReady = false; |
| if (DEBUG) Log.d(TAG, "requestShow IME_SHOW_DELAYED"); |
| break; |
| case ShowResult.IME_SHOW_FAILED: |
| if (WARN) Log.w(TAG, "requestShow IME_SHOW_FAILED. fromIme: " |
| + fromIme); |
| // IME cannot be shown (since it didn't have focus), proceed |
| // with animation of other types. |
| canRun = false; |
| |
| // Reject the show request. |
| setRequestedVisibleTypes(0 /* visibleTypes */, consumer.getType()); |
| break; |
| } |
| } else { |
| consumer.requestHide(fromIme, statsToken); |
| } |
| if (!canRun) { |
| if (WARN) Log.w(TAG, String.format( |
| "collectSourceControls can't continue show for type: %s fromIme: %b", |
| WindowInsets.Type.toString(consumer.getType()), fromIme)); |
| continue; |
| } |
| final InsetsSourceControl control = consumer.getControl(); |
| if (control != null |
| && (control.getLeash() != null || control.getId() == ID_IME_CAPTION_BAR)) { |
| controls.put(control.getId(), new InsetsSourceControl(control)); |
| typesReady |= consumer.getType(); |
| } else if (fromIme) { |
| Log.w(TAG, "collectSourceControls can't continue for type: ime," |
| + " fromIme: true requires a control with a leash but we have " |
| + ((control == null) |
| ? "control: null" |
| : "control: non-null and control.getLeash(): null")); |
| ImeTracker.forLogging().onFailed(statsToken, |
| ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS); |
| } |
| } |
| return new Pair<>(typesReady, imeReady); |
| } |
| |
| private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode( |
| @InsetsType int types, boolean fromPredictiveBack) { |
| if (fromPredictiveBack) { |
| // When insets are animated by predictive back, we want insets to be shown to prevent a |
| // jump cut from shown to hidden at the start of the predictive back animation |
| return LAYOUT_INSETS_DURING_ANIMATION_SHOWN; |
| } |
| // 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. |
| return (mRequestedVisibleTypes & types) != types |
| ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN |
| : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN; |
| } |
| |
| private void cancelExistingControllers(@InsetsType int types) { |
| final int originalmTypesBeingCancelled = mTypesBeingCancelled; |
| mTypesBeingCancelled |= types; |
| try { |
| 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(); |
| } |
| } finally { |
| mTypesBeingCancelled = originalmTypesBeingCancelled; |
| } |
| } |
| |
| private void abortPendingImeControlRequest() { |
| if (mPendingImeControlRequest != null) { |
| mPendingImeControlRequest.listener.onCancelled(null); |
| mPendingImeControlRequest = null; |
| mHandler.removeCallbacks(mPendingControlTimeout); |
| if (DEBUG) Log.d(TAG, "abortPendingImeControlRequest"); |
| } |
| } |
| |
| @VisibleForTesting |
| @Override |
| public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) { |
| setRequestedVisibleTypes(shown ? runner.getTypes() : 0, runner.getTypes()); |
| cancelAnimation(runner, false /* invokeCallback */); |
| if (DEBUG) Log.d(TAG, "notifyFinished. shown: " + shown); |
| if (runner.getAnimationType() == ANIMATION_TYPE_RESIZE) { |
| // The resize animation doesn't show or hide the insets. We shouldn't change the |
| // requested visibility. |
| return; |
| } |
| final ImeTracker.Token statsToken = runner.getStatsToken(); |
| if (shown) { |
| ImeTracker.forLogging().onProgress(statsToken, |
| ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW); |
| ImeTracker.forLogging().onShown(statsToken); |
| } else { |
| ImeTracker.forLogging().onProgress(statsToken, |
| ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE); |
| ImeTracker.forLogging().onHidden(statsToken); |
| } |
| reportRequestedVisibleTypes(); |
| } |
| |
| @Override |
| public void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params) { |
| mHost.applySurfaceParams(params); |
| } |
| |
| void notifyControlRevoked(InsetsSourceConsumer consumer) { |
| final @InsetsType int type = consumer.getType(); |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; |
| control.notifyControlRevoked(type); |
| if (control.getControllingTypes() == 0) { |
| cancelAnimation(control, true /* invokeCallback */); |
| } |
| } |
| if (type == ime()) { |
| abortPendingImeControlRequest(); |
| } |
| if (consumer.getType() != ime()) { |
| // IME consumer should always be there since we need to communicate with |
| // InputMethodManager no matter we have the control or not. |
| mSourceConsumers.remove(consumer.getId()); |
| } |
| } |
| |
| private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) { |
| if (invokeCallback) { |
| ImeTracker.forLogging().onCancelled(control.getStatsToken(), |
| ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); |
| control.cancel(); |
| } else { |
| // Succeeds if invokeCallback is false (i.e. when called from notifyFinished). |
| ImeTracker.forLogging().onProgress(control.getStatsToken(), |
| ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); |
| } |
| if (DEBUG) { |
| Log.d(TAG, TextUtils.formatSimple( |
| "cancelAnimation of types: %d, animType: %d, host: %s", |
| control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle())); |
| } |
| @InsetsType int removedTypes = 0; |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| RunningAnimation runningAnimation = mRunningAnimations.get(i); |
| if (runningAnimation.runner == control) { |
| mRunningAnimations.remove(i); |
| removedTypes = control.getTypes(); |
| if (invokeCallback) { |
| dispatchAnimationEnd(runningAnimation.runner.getAnimation()); |
| } |
| break; |
| } |
| } |
| if (mRunningAnimations.isEmpty()) { |
| mHost.notifyAnimationRunningStateChanged(false); |
| } |
| onAnimationStateChanged(removedTypes, false /* running */); |
| } |
| |
| void onAnimationStateChanged(@InsetsType int types, boolean running) { |
| boolean insetsChanged = false; |
| for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { |
| final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); |
| if ((consumer.getType() & types) != 0) { |
| insetsChanged |= consumer.onAnimationStateChanged(running); |
| } |
| } |
| if (insetsChanged) { |
| notifyVisibilityChanged(); |
| } |
| } |
| |
| private void applyLocalVisibilityOverride() { |
| for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { |
| final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); |
| consumer.applyLocalVisibilityOverride(); |
| } |
| } |
| |
| @VisibleForTesting |
| public @NonNull InsetsSourceConsumer getSourceConsumer(int id, int type) { |
| InsetsSourceConsumer consumer = mSourceConsumers.get(id); |
| if (consumer != null) { |
| return consumer; |
| } |
| if (type == ime() && mImeSourceConsumer != null) { |
| // WindowInsets.Type.ime() should be only provided by one source. |
| mSourceConsumers.remove(mImeSourceConsumer.getId()); |
| consumer = mImeSourceConsumer; |
| consumer.setId(id); |
| } else { |
| consumer = mConsumerCreator.apply(this, id, type); |
| } |
| mSourceConsumers.put(id, consumer); |
| return consumer; |
| } |
| |
| @VisibleForTesting |
| public @NonNull InsetsSourceConsumer getImeSourceConsumer() { |
| return mImeSourceConsumer; |
| } |
| |
| void notifyVisibilityChanged() { |
| mHost.notifyInsetsChanged(); |
| } |
| |
| /** |
| * @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int) |
| */ |
| public void updateCompatSysUiVisibility() { |
| if (mCompatSysUiVisibilityStaled) { |
| mCompatSysUiVisibilityStaled = false; |
| mHost.updateCompatSysUiVisibility( |
| // Treat non-existing types as controllable types for compatibility. |
| mVisibleTypes, mRequestedVisibleTypes, mControllableTypes | ~mExistingTypes); |
| } |
| } |
| |
| /** |
| * Called when current window gains focus. |
| */ |
| public void onWindowFocusGained(boolean hasViewFocused) { |
| mImeSourceConsumer.onWindowFocusGained(hasViewFocused); |
| } |
| |
| /** |
| * Called when current window loses focus. |
| */ |
| public void onWindowFocusLost() { |
| mImeSourceConsumer.onWindowFocusLost(); |
| } |
| |
| @VisibleForTesting |
| public @AnimationType int getAnimationType(@InsetsType int type) { |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; |
| if (control.controlsType(type)) { |
| return mRunningAnimations.get(i).type; |
| } |
| } |
| return ANIMATION_TYPE_NONE; |
| } |
| |
| @VisibleForTesting(visibility = PACKAGE) |
| public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { |
| final @InsetsType int requestedVisibleTypes = |
| (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask); |
| if (mRequestedVisibleTypes != requestedVisibleTypes) { |
| mRequestedVisibleTypes = requestedVisibleTypes; |
| } |
| } |
| |
| /** |
| * Called when finishing setting requested visible types or finishing setting controls. |
| */ |
| private void reportRequestedVisibleTypes() { |
| if (mReportedRequestedVisibleTypes != mRequestedVisibleTypes) { |
| final @InsetsType int diff = mRequestedVisibleTypes ^ mReportedRequestedVisibleTypes; |
| if (WindowInsets.Type.hasCompatSystemBars(diff)) { |
| mCompatSysUiVisibilityStaled = true; |
| } |
| mReportedRequestedVisibleTypes = mRequestedVisibleTypes; |
| mHost.updateRequestedVisibleTypes(mReportedRequestedVisibleTypes); |
| } |
| updateCompatSysUiVisibility(); |
| } |
| |
| @VisibleForTesting |
| public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme, |
| @Nullable ImeTracker.Token statsToken) { |
| // TODO(b/166736352): We should only skip the animation of specific types, not all types. |
| boolean skipAnim = false; |
| if ((types & ime()) != 0) { |
| final InsetsSourceControl imeControl = mImeSourceConsumer.getControl(); |
| // Skip showing animation once that made by system for some reason. |
| // (e.g. starting window with IME snapshot) |
| if (imeControl != null) { |
| skipAnim = imeControl.getAndClearSkipAnimationOnce() && show |
| && mImeSourceConsumer.hasViewFocusWhenWindowFocusGain(); |
| } |
| } |
| applyAnimation(types, show, fromIme, skipAnim, statsToken); |
| } |
| |
| @VisibleForTesting |
| public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme, |
| boolean skipAnim, @Nullable ImeTracker.Token statsToken) { |
| if (types == 0) { |
| // nothing to animate. |
| if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate"); |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); |
| if (!fromIme) { |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); |
| } |
| return; |
| } |
| |
| boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks(); |
| final InternalAnimationControlListener listener = new InternalAnimationControlListener( |
| show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(), |
| skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP), |
| mLoggingListener, mJankContext); |
| |
| // We are about to playing the default animation (show/hide). Passing a null frame indicates |
| // the controlled types should be animated regardless of the frame. |
| controlAnimationUnchecked( |
| types, null /* cancellationSignal */, listener, null /* frame */, fromIme, |
| listener.getDurationMs(), listener.getInsetsInterpolator(), |
| show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, |
| show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN, |
| !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken); |
| } |
| |
| /** |
| * Cancel on-going animation to show/hide {@link InsetsType}. |
| */ |
| @VisibleForTesting |
| public void cancelExistingAnimations() { |
| cancelExistingControllers(all()); |
| } |
| |
| void dump(String prefix, PrintWriter pw) { |
| final String innerPrefix = prefix + " "; |
| pw.println(prefix + "InsetsController:"); |
| mState.dump(innerPrefix, pw); |
| pw.println(innerPrefix + "mIsPredictiveBackImeHideAnimInProgress=" |
| + mIsPredictiveBackImeHideAnimInProgress); |
| } |
| |
| void dumpDebug(ProtoOutputStream proto, long fieldId) { |
| final long token = proto.start(fieldId); |
| mState.dumpDebug(proto, STATE); |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; |
| runner.dumpDebug(proto, CONTROL); |
| } |
| proto.end(token); |
| } |
| |
| @VisibleForTesting |
| @Override |
| public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController> |
| void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types, |
| WindowInsetsAnimation animation, Bounds bounds) { |
| mHost.dispatchWindowInsetsAnimationPrepare(animation); |
| mHost.addOnPreDrawRunnable(() -> { |
| if (runner.isCancelled()) { |
| if (WARN) Log.w(TAG, "startAnimation canceled before preDraw"); |
| return; |
| } |
| Trace.asyncTraceBegin(TRACE_TAG_VIEW, |
| "InsetsAnimation: " + WindowInsets.Type.toString(types), types); |
| for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { |
| RunningAnimation runningAnimation = mRunningAnimations.get(i); |
| if (runningAnimation.runner == runner) { |
| runningAnimation.startDispatched = true; |
| } |
| } |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.pendingAnim", 0); |
| mHost.dispatchWindowInsetsAnimationStart(animation, bounds); |
| mStartingAnimation = true; |
| runner.setReadyDispatched(true); |
| listener.onReady(runner, types); |
| mStartingAnimation = false; |
| }); |
| } |
| |
| @VisibleForTesting |
| public void dispatchAnimationEnd(WindowInsetsAnimation animation) { |
| Trace.asyncTraceEnd(TRACE_TAG_VIEW, |
| "InsetsAnimation: " + WindowInsets.Type.toString(animation.getTypeMask()), |
| animation.getTypeMask()); |
| 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) { |
| mAppearanceControlled |= mask; |
| mHost.setSystemBarsAppearance(appearance, mask); |
| } |
| |
| @Override |
| public void setSystemBarsAppearanceFromResource(@Appearance int appearance, |
| @Appearance int mask) { |
| mAppearanceFromResource = (mAppearanceFromResource & ~mask) | (appearance & mask); |
| |
| // Don't change the flags which are already controlled by setSystemBarsAppearance. |
| mHost.setSystemBarsAppearance(appearance, mask & ~mAppearanceControlled); |
| } |
| |
| @Override |
| public @Appearance int getSystemBarsAppearance() { |
| // We only return the requested appearance, not the implied one. |
| return (mHost.getSystemBarsAppearance() & mAppearanceControlled) |
| | (mAppearanceFromResource & ~mAppearanceControlled); |
| } |
| |
| public @Appearance int getAppearanceControlled() { |
| return mAppearanceControlled; |
| } |
| |
| @Override |
| public void setImeCaptionBarInsetsHeight(int height) { |
| if (!ENABLE_HIDE_IME_CAPTION_BAR) { |
| return; |
| } |
| Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom); |
| InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR); |
| if (mImeCaptionBarInsetsHeight != height |
| || (source != null && !newFrame.equals(source.getFrame()))) { |
| mImeCaptionBarInsetsHeight = height; |
| if (mImeCaptionBarInsetsHeight != 0) { |
| mState.getOrCreateSource(ID_IME_CAPTION_BAR, captionBar()) |
| .setFrame(newFrame); |
| getSourceConsumer(ID_IME_CAPTION_BAR, captionBar()).setControl( |
| new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(), |
| null /* leash */, false /* initialVisible */, |
| new Point(), Insets.NONE), |
| new int[1], new int[1]); |
| } else { |
| mState.removeSource(ID_IME_CAPTION_BAR); |
| InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR); |
| if (sourceConsumer != null) { |
| sourceConsumer.setControl(null, new int[1], new int[1]); |
| } |
| } |
| mHost.notifyInsetsChanged(); |
| } |
| } |
| |
| @Override |
| public void setSystemBarsBehavior(@Behavior int behavior) { |
| mBehaviorControlled = true; |
| mHost.setSystemBarsBehavior(behavior); |
| } |
| |
| @Override |
| public @Behavior int getSystemBarsBehavior() { |
| if (!mBehaviorControlled) { |
| // We only return the requested behavior, not the implied one. |
| return BEHAVIOR_DEFAULT; |
| } |
| return mHost.getSystemBarsBehavior(); |
| } |
| |
| public boolean isBehaviorControlled() { |
| return mBehaviorControlled; |
| } |
| |
| @Override |
| public void setAnimationsDisabled(boolean disable) { |
| mAnimationsDisabled = disable; |
| } |
| |
| private @InsetsType int calculateControllableTypes() { |
| @InsetsType int result = 0; |
| for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { |
| InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); |
| InsetsSource source = mState.peekSource(consumer.getId()); |
| if (consumer.getControl() != null && source != null) { |
| result |= consumer.getType(); |
| } |
| } |
| return result & ~mState.calculateUncontrollableInsetsFromFrame(mFrame); |
| } |
| |
| /** |
| * @return The types that are now animating due to a listener invoking control/show/hide |
| */ |
| private @InsetsType int invokeControllableInsetsChangedListeners() { |
| mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); |
| 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); |
| } |
| |
| @Override |
| public void reportPerceptible(@InsetsType int types, boolean perceptible) { |
| final int size = mSourceConsumers.size(); |
| for (int i = 0; i < size; i++) { |
| final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); |
| if ((consumer.getType() & types) != 0) { |
| consumer.onPerceptible(perceptible); |
| } |
| } |
| } |
| |
| @VisibleForTesting(visibility = PACKAGE) |
| public Host getHost() { |
| return mHost; |
| } |
| } |