summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/InsetsController.java150
-rw-r--r--core/java/android/view/InsetsSourceConsumer.java7
-rw-r--r--core/tests/coretests/Android.mk1
-rw-r--r--core/tests/coretests/src/android/view/InsetsControllerTest.java70
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());
+ });
+ }
}