/* * 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.TYPE_IME; import static android.view.InsetsState.toPublicType; import static android.view.WindowInsets.Type.all; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TypeEvaluator; import android.annotation.IntDef; import android.annotation.NonNull; import android.graphics.Insets; import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.Property; import android.util.SparseArray; import android.view.InsetsSourceConsumer.ShowResult; import android.view.InsetsState.InternalInsetType; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetType; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.util.ArrayList; /** * Implements {@link WindowInsetsController} on the client. * @hide */ public class InsetsController implements WindowInsetsController { private static final int ANIMATION_DURATION_SHOW_MS = 275; private static final int ANIMATION_DURATION_HIDE_MS = 340; private static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); private static final int DIRECTION_NONE = 0; private static final int DIRECTION_SHOW = 1; private static final int DIRECTION_HIDE = 2; @IntDef ({DIRECTION_NONE, DIRECTION_SHOW, DIRECTION_HIDE}) private @interface AnimationDirection{} /** * Translation animation evaluator. */ private static TypeEvaluator sEvaluator = (fraction, startValue, endValue) -> Insets.of( 0, (int) (startValue.top + fraction * (endValue.top - startValue.top)), 0, (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom))); /** * Linear animation property */ private static class InsetsProperty extends Property { InsetsProperty() { super(Insets.class, "Insets"); } @Override public Insets get(WindowInsetsAnimationController object) { return object.getCurrentInsets(); } @Override public void set(WindowInsetsAnimationController object, Insets value) { object.changeInsets(value); } } private final String TAG = "InsetsControllerImpl"; private final InsetsState mState = new InsetsState(); private final InsetsState mTmpState = new InsetsState(); private final Rect mFrame = new Rect(); private final SparseArray mSourceConsumers = new SparseArray<>(); private final ViewRootImpl mViewRoot; private final SparseArray mTmpControlArray = new SparseArray<>(); private final ArrayList mAnimationControls = new ArrayList<>(); private final ArrayList mTmpFinishedControls = new ArrayList<>(); private WindowInsets mLastInsets; private boolean mAnimCallbackScheduled; private final Runnable mAnimCallback; private final Rect mLastLegacyContentInsets = new Rect(); private final Rect mLastLegacyStableInsets = new Rect(); private @AnimationDirection int mAnimationDirection; private int mPendingTypesToShow; private int mLastLegacySoftInputMode; public InsetsController(ViewRootImpl viewRoot) { mViewRoot = viewRoot; mAnimCallback = () -> { mAnimCallbackScheduled = false; if (mAnimationControls.isEmpty()) { return; } mTmpFinishedControls.clear(); InsetsState state = new InsetsState(mState, true /* copySources */); for (int i = mAnimationControls.size() - 1; i >= 0; i--) { InsetsAnimationControlImpl control = mAnimationControls.get(i); if (mAnimationControls.get(i).applyChangeInsets(state)) { mTmpFinishedControls.add(control); } } WindowInsets insets = state.calculateInsets(mFrame, mLastInsets.isRound(), mLastInsets.shouldAlwaysConsumeSystemBars(), mLastInsets.getDisplayCutout(), mLastLegacyContentInsets, mLastLegacyStableInsets, mLastLegacySoftInputMode, null /* typeSideMap */); mViewRoot.mView.dispatchWindowInsetsAnimationProgress(insets); for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) { dispatchAnimationFinished(mTmpFinishedControls.get(i).getAnimation()); } }; } @VisibleForTesting public void onFrameChanged(Rect frame) { if (mFrame.equals(frame)) { return; } mViewRoot.notifyInsetsChanged(); mFrame.set(frame); } public InsetsState getState() { return mState; } boolean onStateChanged(InsetsState state) { if (mState.equals(state)) { return false; } mState.set(state); mTmpState.set(state, true /* copySources */); applyLocalVisibilityOverride(); mViewRoot.notifyInsetsChanged(); if (!mState.equals(mTmpState)) { sendStateToWindowManager(); } return true; } /** * @see InsetsState#calculateInsets */ @VisibleForTesting public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars, DisplayCutout cutout, Rect legacyContentInsets, Rect legacyStableInsets, int legacySoftInputMode) { mLastLegacyContentInsets.set(legacyContentInsets); mLastLegacyStableInsets.set(legacyStableInsets); mLastLegacySoftInputMode = legacySoftInputMode; mLastInsets = mState.calculateInsets(mFrame, isScreenRound, alwaysConsumeSystemBars, cutout, legacyContentInsets, legacyStableInsets, legacySoftInputMode, null /* typeSideMap */); return mLastInsets; } /** * 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); } } } // 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); } // Ensure to create source consumers if not available yet. for (int i = mTmpControlArray.size() - 1; i >= 0; i--) { final InsetsSourceControl control = mTmpControlArray.valueAt(i); getSourceConsumer(control.getType()).setControl(control); } mTmpControlArray.clear(); } @Override public void show(@InsetType int types) { show(types, false /* fromIme */); } private void show(@InsetType int types, boolean fromIme) { // 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--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (mAnimationDirection == DIRECTION_HIDE) { // Only one animator (with multiple InsetType) can run at a time. // previous one should be cancelled for simplicity. cancelExistingAnimation(); } else if (consumer.isVisible() && (mAnimationDirection == DIRECTION_NONE || mAnimationDirection == DIRECTION_HIDE)) { // no-op: already shown or animating in (because window visibility is // applied before starting animation). // TODO: When we have more than one types: handle specific case when // show animation is going on, but the current type is not becoming visible. continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } applyAnimation(typesReady, true /* show */, fromIme); } @Override public void hide(@InsetType int types) { int typesReady = 0; final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (mAnimationDirection == DIRECTION_SHOW) { cancelExistingAnimation(); } else if (!consumer.isVisible() && (mAnimationDirection == DIRECTION_NONE || mAnimationDirection == DIRECTION_HIDE)) { // no-op: already hidden or animating out. continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } applyAnimation(typesReady, false /* show */, false /* fromIme */); } @Override public void controlWindowInsetsAnimation(@InsetType int types, WindowInsetsAnimationControlListener listener) { controlWindowInsetsAnimation(types, listener, false /* fromIme */); } private void controlWindowInsetsAnimation(@InsetType int types, WindowInsetsAnimationControlListener listener, boolean fromIme) { // 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. if (!mState.getDisplayFrame().equals(mFrame)) { listener.onCancelled(); return; } controlAnimationUnchecked(types, listener, mFrame, fromIme); } private void controlAnimationUnchecked(@InsetType int types, WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme) { if (types == 0) { // nothing to animate. return; } cancelExistingControllers(types); final ArraySet internalTypes = mState.toInternalType(types); final SparseArray consumers = new SparseArray<>(); Pair typesReadyPair = collectConsumers(fromIme, internalTypes, consumers); int typesReady = typesReadyPair.first; boolean isReady = typesReadyPair.second; if (!isReady) { // IME isn't ready, all requested types would be shown once IME is ready. mPendingTypesToShow = typesReady; // TODO: listener for pending types. return; } // pending types from previous request. typesReady = collectPendingConsumers(typesReady, consumers); if (typesReady == 0) { listener.onCancelled(); return; } final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, frame, mState, listener, typesReady, () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this); mAnimationControls.add(controller); } /** * @return Pair of (types ready to animate, is ready to animate). */ private Pair collectConsumers(boolean fromIme, ArraySet internalTypes, SparseArray consumers) { int typesReady = 0; boolean isReady = true; for (int i = internalTypes.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (consumer.getControl() != null) { if (!consumer.isVisible()) { // Show request switch(consumer.requestShow(fromIme)) { case ShowResult.SHOW_IMMEDIATELY: typesReady |= InsetsState.toPublicType(consumer.getType()); break; case ShowResult.SHOW_DELAYED: isReady = false; break; case ShowResult.SHOW_FAILED: // IME cannot be shown (since it didn't have focus), proceed // with animation of other types. if (mPendingTypesToShow != 0) { // remove IME from pending because view no longer has focus. mPendingTypesToShow &= ~InsetsState.toPublicType(TYPE_IME); } break; } } else { // Hide request // TODO: Move notifyHidden() to beginning of the hide animation // (when visibility actually changes using hideDirectly()). consumer.notifyHidden(); typesReady |= InsetsState.toPublicType(consumer.getType()); } consumers.put(consumer.getType(), consumer); } else { // TODO: Let calling app know it's not possible, or wait // TODO: Remove it from types } } return new Pair<>(typesReady, isReady); } private int collectPendingConsumers(@InsetType int typesReady, SparseArray consumers) { if (mPendingTypesToShow != 0) { typesReady |= mPendingTypesToShow; final ArraySet internalTypes = mState.toInternalType(mPendingTypesToShow); for (int i = internalTypes.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); consumers.put(consumer.getType(), consumer); } mPendingTypesToShow = 0; } return typesReady; } private void cancelExistingControllers(@InsetType int types) { for (int i = mAnimationControls.size() - 1; i >= 0; i--) { InsetsAnimationControlImpl control = mAnimationControls.get(i); if ((control.getTypes() & types) != 0) { cancelAnimation(control); } } } @VisibleForTesting public void notifyFinished(InsetsAnimationControlImpl controller, int shownTypes) { mAnimationControls.remove(controller); hideDirectly(controller.getTypes() & ~shownTypes); showDirectly(controller.getTypes() & shownTypes); } void notifyControlRevoked(InsetsSourceConsumer consumer) { for (int i = mAnimationControls.size() - 1; i >= 0; i--) { InsetsAnimationControlImpl control = mAnimationControls.get(i); if ((control.getTypes() & toPublicType(consumer.getType())) != 0) { cancelAnimation(control); } } } private void cancelAnimation(InsetsAnimationControlImpl control) { control.onCancelled(); mAnimationControls.remove(control); } 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(@InternalInsetType int type) { InsetsSourceConsumer controller = mSourceConsumers.get(type); if (controller != null) { return controller; } controller = createConsumerOfType(type); mSourceConsumers.put(type, controller); return controller; } @VisibleForTesting public void notifyVisibilityChanged() { mViewRoot.notifyInsetsChanged(); sendStateToWindowManager(); } /** * Called when current window gains focus. */ public void onWindowFocusGained() { getSourceConsumer(TYPE_IME).onWindowFocusGained(); } /** * Called when current window loses focus. */ public void onWindowFocusLost() { getSourceConsumer(TYPE_IME).onWindowFocusLost(); } ViewRootImpl getViewRoot() { return mViewRoot; } /** * 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); } } private InsetsSourceConsumer createConsumerOfType(int type) { if (type == TYPE_IME) { return new ImeInsetsSourceConsumer(mState, Transaction::new, this); } else { return new InsetsSourceConsumer(type, mState, Transaction::new, this); } } /** * Sends the local visibility state back to window manager. */ private void sendStateToWindowManager() { InsetsState tmpState = new InsetsState(); for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); if (consumer.getControl() != null) { tmpState.addSource(mState.getSource(consumer.getType())); } } // TODO: Put this on a dispatcher thread. try { mViewRoot.mWindowSession.insetsModified(mViewRoot.mWindow, tmpState); } catch (RemoteException e) { Log.e(TAG, "Failed to call insetsModified", e); } } private void applyAnimation(@InsetType final int types, boolean show, boolean fromIme) { if (types == 0) { // nothing to animate. return; } WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() { private WindowInsetsAnimationController mController; private ObjectAnimator mAnimator; @Override public void onReady(WindowInsetsAnimationController controller, int types) { mController = controller; if (show) { showDirectly(types); } else { hideDirectly(types); } mAnimator = ObjectAnimator.ofObject( controller, new InsetsProperty(), sEvaluator, show ? controller.getHiddenStateInsets() : controller.getShownStateInsets(), show ? controller.getShownStateInsets() : controller.getHiddenStateInsets() ); mAnimator.setDuration(show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS); mAnimator.setInterpolator(INTERPOLATOR); mAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { onAnimationFinish(); } }); mAnimator.start(); } @Override public void onCancelled() { mAnimator.cancel(); } private void onAnimationFinish() { mAnimationDirection = DIRECTION_NONE; mController.finish(show ? types : 0); } }; // Show/hide animations always need to be relative to the display frame, in order that shown // and hidden state insets are correct. controlAnimationUnchecked(types, listener, mState.getDisplayFrame(), fromIme); } private void hideDirectly(@InsetType int types) { final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).hide(); } } private void showDirectly(@InsetType int types) { final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).show(); } } /** * Cancel on-going animation to show/hide {@link InsetType}. */ @VisibleForTesting public void cancelExistingAnimation() { cancelExistingControllers(all()); } void dump(String prefix, PrintWriter pw) { pw.println(prefix); pw.println("InsetsController:"); mState.dump(prefix + " ", pw); } @VisibleForTesting public void dispatchAnimationStarted(WindowInsetsAnimationListener.InsetsAnimation animation) { mViewRoot.mView.dispatchWindowInsetsAnimationStarted(animation); } @VisibleForTesting public void dispatchAnimationFinished(WindowInsetsAnimationListener.InsetsAnimation animation) { mViewRoot.mView.dispatchWindowInsetsAnimationFinished(animation); } @VisibleForTesting public void scheduleApplyChangeInsets() { if (!mAnimCallbackScheduled) { mViewRoot.mChoreographer.postCallback(Choreographer.CALLBACK_INSETS_ANIMATION, mAnimCallback, null /* token*/); mAnimCallbackScheduled = true; } } }