Java程序  |  596行  |  22.44 KB

/*
 * 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<Insets> 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<WindowInsetsAnimationController, Insets> {
        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<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
    private final ViewRootImpl mViewRoot;

    private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
    private final ArrayList<InsetsAnimationControlImpl> mAnimationControls = new ArrayList<>();
    private final ArrayList<InsetsAnimationControlImpl> 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<Integer> 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<Integer> 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<Integer> internalTypes = mState.toInternalType(types);
        final SparseArray<InsetsSourceConsumer> consumers = new SparseArray<>();

        Pair<Integer, Boolean> 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<Integer, Boolean> collectConsumers(boolean fromIme,
            ArraySet<Integer> internalTypes, SparseArray<InsetsSourceConsumer> 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<InsetsSourceConsumer> consumers) {
        if (mPendingTypesToShow != 0) {
            typesReady |= mPendingTypesToShow;
            final ArraySet<Integer> 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<Integer> 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<Integer> 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;
        }
    }
}