Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 22f2b4cc authored by Tarandeep Singh's avatar Tarandeep Singh Committed by Jorim Jaggi
Browse files

Animate Insets controller (IME transitions 2/n)

Add animations to InsetsController.show()/hide()

Bug: 118599175
Bug: 118118435
Test: atest InsetsControllerTest
Test: atest InsetSourceConsumerTest
Change-Id: Ife224de9762361e60ca4d11c0e39ad0376cf1e35
parent bdd89dbe
Loading
Loading
Loading
Loading
+145 −5
Original line number Diff line number Diff line
@@ -16,17 +16,22 @@

package android.view;

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.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Log;
import android.util.Property;
import android.util.SparseArray;
import android.view.InsetsState.InternalInsetType;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type.InsetType;
import android.view.InsetsState.InternalInsetType;

import com.android.internal.annotations.VisibleForTesting;

@@ -39,6 +44,41 @@ import java.util.ArrayList;
 */
public class InsetsController implements WindowInsetsController {

    // TODO: Use animation scaling and more optimal duration.
    private static final int ANIMATION_DURATION_MS = 400;
    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();
@@ -58,6 +98,8 @@ public class InsetsController implements WindowInsetsController {

    private final Rect mLastLegacyContentInsets = new Rect();
    private final Rect mLastLegacyStableInsets = new Rect();
    private ObjectAnimator mAnimator;
    private @AnimationDirection int mAnimationDirection;

    public InsetsController(ViewRootImpl viewRoot) {
        mViewRoot = viewRoot;
@@ -122,9 +164,12 @@ public class InsetsController implements WindowInsetsController {
    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--) {
@@ -146,18 +191,40 @@ public class InsetsController implements WindowInsetsController {

    @Override
    public void show(@InsetType int types) {
        int typesReady = 0;
        final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
        for (int i = internalTypes.size() - 1; i >= 0; i--) {
            getSourceConsumer(internalTypes.valueAt(i)).show();
            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_SHOW) {
                // no-op: already shown or animating in.
                // 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 */);
    }

    @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--) {
            getSourceConsumer(internalTypes.valueAt(i)).hide();
            InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i));
            if (mAnimationDirection == DIRECTION_SHOW) {
                cancelExistingAnimation();
            } else if (!consumer.isVisible() || mAnimationDirection == DIRECTION_HIDE) {
                // no-op: already hidden or animating out.
                continue;
            }
            typesReady |= InsetsState.toPublicType(consumer.getType());
        }
        applyAnimation(typesReady, false /* show */);
    }

    @Override
@@ -226,6 +293,79 @@ public class InsetsController implements WindowInsetsController {
        }
    }

    private void applyAnimation(@InsetType final int types, boolean show) {
        if (types == 0) {
            // nothing to animate.
            return;
        }
        WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() {
            @Override
            public void onReady(WindowInsetsAnimationController controller, int types) {
                mAnimator = ObjectAnimator.ofObject(
                        controller,
                        new InsetsProperty(),
                        sEvaluator,
                        show ? controller.getHiddenStateInsets() : controller.getShownStateInsets(),
                        show ? controller.getShownStateInsets() : controller.getHiddenStateInsets()
                );
                mAnimator.setDuration(ANIMATION_DURATION_MS);
                mAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationCancel(Animator animation) {
                        onAnimationFinish();
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        onAnimationFinish();
                    }
                });
                mAnimator.start();
            }

            @Override
            public void onCancelled() {}

            private void onAnimationFinish() {
                mAnimationDirection = DIRECTION_NONE;
                if (show) {
                    showOnAnimationEnd(types);
                } else {
                    hideOnAnimationEnd(types);
                }
            }
        };
        // TODO: Instead of clearing this here, properly wire up
        // InsetsAnimationControlImpl.finish() to remove this from mAnimationControls.
        mAnimationControls.clear();
        controlWindowInsetsAnimation(types, listener);
    }

    private void hideOnAnimationEnd(@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 showOnAnimationEnd(@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() {
        mAnimationDirection = DIRECTION_NONE;
        if (mAnimator != null) {
            mAnimator.cancel();
        }
    }

    void dump(String prefix, PrintWriter pw) {
        pw.println(prefix); pw.println("InsetsController:");
        mState.dump(prefix + "  ", pw);
+6 −1
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@
package android.view;

import android.annotation.Nullable;
import android.view.SurfaceControl.Transaction;
import android.view.InsetsState.InternalInsetType;
import android.view.SurfaceControl.Transaction;

import com.android.internal.annotations.VisibleForTesting;

@@ -89,6 +89,11 @@ public class InsetsSourceConsumer {
        return true;
    }

    @VisibleForTesting
    public boolean isVisible() {
        return mVisible;
    }

    private void setVisible(boolean visible) {
        if (mVisible == visible) {
            return;
+1 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
LOCAL_JAVA_LIBRARIES := \
    android.test.runner \
    telephony-common \
    testables \
    org.apache.http.legacy \
    android.test.base \
    android.test.mock \
+66 −4
Original line number Diff line number Diff line
@@ -16,15 +16,25 @@

package android.view;

import static android.view.InsetsState.TYPE_IME;
import static android.view.InsetsState.TYPE_NAVIGATION_BAR;
import static android.view.InsetsState.TYPE_TOP_BAR;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Mockito.mock;

import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.view.WindowInsets.Type;
import android.view.WindowManager.BadTokenException;
import android.view.WindowManager.LayoutParams;
import android.widget.TextView;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;

@@ -37,8 +47,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class InsetsControllerTest {

    private InsetsController mController = new InsetsController(mock(ViewRootImpl.class));

    private InsetsController mController;
    private SurfaceSession mSession = new SurfaceSession();
    private SurfaceControl mLeash;

@@ -47,6 +56,24 @@ public class InsetsControllerTest {
        mLeash = new SurfaceControl.Builder(mSession)
                .setName("testSurface")
                .build();
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            Context context = InstrumentationRegistry.getTargetContext();
            // cannot mock ViewRootImpl since it's final.
            ViewRootImpl viewRootImpl = new ViewRootImpl(context, context.getDisplay());
            try {
                viewRootImpl.setView(new TextView(context), new LayoutParams(), null);
            } catch (BadTokenException e) {
                // activity isn't running, we will ignore BadTokenException.
            }
            mController = new InsetsController(viewRootImpl);
            final Rect rect = new Rect(5, 5, 5, 5);
            mController.calculateInsets(
                    false,
                    false,
                    new DisplayCutout(
                            Insets.of(10, 10, 10, 10), rect, rect, rect, rect),
                    rect, rect);
        });
    }

    @Test
@@ -64,4 +91,39 @@ public class InsetsControllerTest {
        mController.onControlsChanged(new InsetsSourceControl[0]);
        assertNull(mController.getSourceConsumer(TYPE_TOP_BAR).getControl());
    }

    @Test
    public void testAnimationEndState() {
        final InsetsSourceControl navBar = new InsetsSourceControl(TYPE_NAVIGATION_BAR, mLeash);
        final InsetsSourceControl topBar = new InsetsSourceControl(TYPE_TOP_BAR, mLeash);
        final InsetsSourceControl ime = new InsetsSourceControl(TYPE_IME, mLeash);

        InsetsSourceControl[] controls = new InsetsSourceControl[3];
        controls[0] = navBar;
        controls[1] = topBar;
        controls[2] = ime;
        mController.onControlsChanged(controls);
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            mController.show(Type.all());
            // quickly jump to final state by cancelling it.
            mController.cancelExistingAnimation();
            assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible());
            assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible());
            assertTrue(mController.getSourceConsumer(ime.getType()).isVisible());

            mController.hide(Type.all());
            mController.cancelExistingAnimation();
            assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible());
            assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible());
            assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());

            mController.show(Type.ime());
            mController.cancelExistingAnimation();
            assertTrue(mController.getSourceConsumer(ime.getType()).isVisible());

            mController.hide(Type.ime());
            mController.cancelExistingAnimation();
            assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
        });
    }
}