diff options
| -rw-r--r-- | core/java/android/view/InsetsController.java | 150 | ||||
| -rw-r--r-- | core/java/android/view/InsetsSourceConsumer.java | 7 | ||||
| -rw-r--r-- | core/tests/coretests/Android.mk | 1 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/InsetsControllerTest.java | 70 |
4 files changed, 218 insertions, 10 deletions
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index dd6231d9c5b0..2142c36f8803 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -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,7 +164,10 @@ public class InsetsController implements WindowInsetsController { public void onControlsChanged(InsetsSourceControl[] activeControls) { if (activeControls != null) { for (InsetsSourceControl activeControl : activeControls) { - mTmpControlArray.put(activeControl.getType(), activeControl); + if (activeControl != null) { + // TODO(b/122982984): Figure out why it can be null. + mTmpControlArray.put(activeControl.getType(), activeControl); + } } } @@ -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); diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 145b09763676..7937cb69b80e 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -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; diff --git a/core/tests/coretests/Android.mk b/core/tests/coretests/Android.mk index 0fc3bd224fbf..8e8b07a9074b 100644 --- a/core/tests/coretests/Android.mk +++ b/core/tests/coretests/Android.mk @@ -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 \ diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index d44745121a22..8f2109676dfb 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -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()); + }); + } } |