Merge "Disable user animations on insets whose visible frame is empty (refined)" into rvc-dev am: 77190b77f6 am: 4c3dac8019 am: 27a5c4bbf6
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11908304 Change-Id: I63d2fbcf7d0ad32601775c79afdc3edfa28df98b
This commit is contained in:
@@ -471,6 +471,10 @@ public class InputMethodService extends AbstractInputMethodService {
|
||||
|
||||
final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
|
||||
onComputeInsets(mTmpInsets);
|
||||
if (!mViewsCreated) {
|
||||
// The IME views are not ready, keep visible insets untouched.
|
||||
mTmpInsets.visibleTopInsets = 0;
|
||||
}
|
||||
if (isExtractViewShown()) {
|
||||
// In true fullscreen mode, we just say the window isn't covering
|
||||
// any content so we don't impact whatever is behind.
|
||||
|
||||
@@ -514,6 +514,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
||||
/** Set of inset types for which an animation was started since last resetting this field */
|
||||
private @InsetsType int mLastStartedAnimTypes;
|
||||
|
||||
/** Set of inset types which cannot be controlled by the user animation */
|
||||
private @InsetsType int mDisabledUserAnimationInsetsTypes;
|
||||
|
||||
private Runnable mInvokeControllableInsetsChangedListeners =
|
||||
this::invokeControllableInsetsChangedListeners;
|
||||
|
||||
public InsetsController(Host host) {
|
||||
this(host, (controller, type) -> {
|
||||
if (type == ITYPE_IME) {
|
||||
@@ -628,22 +634,57 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
||||
|
||||
private void updateState(InsetsState newState) {
|
||||
mState.setDisplayFrame(newState.getDisplayFrame());
|
||||
for (int i = 0; i < InsetsState.SIZE; i++) {
|
||||
InsetsSource source = newState.peekSource(i);
|
||||
if (source == null) continue;;
|
||||
getSourceConsumer(source.getType()).updateSource(source);
|
||||
}
|
||||
for (int i = 0; i < InsetsState.SIZE; i++) {
|
||||
InsetsSource source = mState.peekSource(i);
|
||||
@InsetsType int disabledUserAnimationTypes = 0;
|
||||
@InsetsType int[] cancelledUserAnimationTypes = {0};
|
||||
for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
|
||||
InsetsSource source = newState.peekSource(type);
|
||||
if (source == null) continue;
|
||||
if (newState.peekSource(source.getType()) == null) {
|
||||
mState.removeSource(source.getType());
|
||||
@AnimationType int animationType = getAnimationType(type);
|
||||
if (!source.isUserControllable()) {
|
||||
@InsetsType int insetsType = toPublicType(type);
|
||||
// The user animation is not allowed when visible frame is empty.
|
||||
disabledUserAnimationTypes |= insetsType;
|
||||
if (animationType == ANIMATION_TYPE_USER) {
|
||||
// Existing user animation needs to be cancelled.
|
||||
animationType = ANIMATION_TYPE_NONE;
|
||||
cancelledUserAnimationTypes[0] |= insetsType;
|
||||
}
|
||||
}
|
||||
getSourceConsumer(type).updateSource(source, animationType);
|
||||
}
|
||||
for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
|
||||
InsetsSource source = mState.peekSource(type);
|
||||
if (source == null) continue;
|
||||
if (newState.peekSource(type) == null) {
|
||||
mState.removeSource(type);
|
||||
}
|
||||
}
|
||||
if (mCaptionInsetsHeight != 0) {
|
||||
mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top,
|
||||
mFrame.right, mFrame.top + mCaptionInsetsHeight));
|
||||
}
|
||||
|
||||
updateDisabledUserAnimationTypes(disabledUserAnimationTypes);
|
||||
|
||||
if (cancelledUserAnimationTypes[0] != 0) {
|
||||
mHandler.post(() -> show(cancelledUserAnimationTypes[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) {
|
||||
@InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes;
|
||||
if (diff != 0) {
|
||||
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
|
||||
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
|
||||
if (consumer.getControl() != null
|
||||
&& (toPublicType(consumer.getType()) & diff) != 0) {
|
||||
mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
|
||||
mHandler.post(mInvokeControllableInsetsChangedListeners);
|
||||
break;
|
||||
}
|
||||
}
|
||||
mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean captionInsetsUnchanged() {
|
||||
@@ -847,6 +888,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
||||
+ " while an existing " + Type.toString(mTypesBeingCancelled)
|
||||
+ " is being cancelled.");
|
||||
}
|
||||
if (animationType == ANIMATION_TYPE_USER) {
|
||||
final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes;
|
||||
if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes);
|
||||
types &= ~mDisabledUserAnimationInsetsTypes;
|
||||
|
||||
if (fromIme && (disabledTypes & ime()) != 0
|
||||
&& !mState.getSource(ITYPE_IME).isVisible()) {
|
||||
// We've requested IMM to show IME, but the IME is not controllable. We need to
|
||||
// cancel the request.
|
||||
getSourceConsumer(ITYPE_IME).hide(true, animationType);
|
||||
}
|
||||
}
|
||||
if (types == 0) {
|
||||
// nothing to animate.
|
||||
listener.onCancelled(null);
|
||||
@@ -1320,7 +1373,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
||||
@InsetsType int result = 0;
|
||||
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
|
||||
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
|
||||
if (consumer.getControl() != null) {
|
||||
InsetsSource source = mState.peekSource(consumer.mType);
|
||||
if (consumer.getControl() != null && source != null && source.isUserControllable()) {
|
||||
result |= toPublicType(consumer.mType);
|
||||
}
|
||||
}
|
||||
@@ -1331,6 +1385,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
||||
* @return The types that are now animating due to a listener invoking control/show/hide
|
||||
*/
|
||||
private @InsetsType int invokeControllableInsetsChangedListeners() {
|
||||
mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
|
||||
mLastStartedAnimTypes = 0;
|
||||
@InsetsType int types = calculateControllableTypes();
|
||||
int size = mControllableInsetsChangedListeners.size();
|
||||
|
||||
@@ -92,6 +92,11 @@ public class InsetsSource implements Parcelable {
|
||||
return mVisible;
|
||||
}
|
||||
|
||||
boolean isUserControllable() {
|
||||
// If mVisibleFrame is null, it will be the same area as mFrame.
|
||||
return mVisibleFrame == null || !mVisibleFrame.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the insets this source will cause to a client window.
|
||||
*
|
||||
|
||||
@@ -18,8 +18,8 @@ package android.view;
|
||||
|
||||
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
|
||||
import static android.view.InsetsController.AnimationType;
|
||||
import static android.view.InsetsState.getDefaultVisibility;
|
||||
import static android.view.InsetsController.DEBUG;
|
||||
import static android.view.InsetsState.getDefaultVisibility;
|
||||
import static android.view.InsetsState.toPublicType;
|
||||
|
||||
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
|
||||
@@ -284,9 +284,9 @@ public class InsetsSourceConsumer {
|
||||
}
|
||||
|
||||
@VisibleForTesting(visibility = PACKAGE)
|
||||
public void updateSource(InsetsSource newSource) {
|
||||
public void updateSource(InsetsSource newSource, @AnimationType int animationType) {
|
||||
InsetsSource source = mState.peekSource(mType);
|
||||
if (source == null || mController.getAnimationType(mType) == ANIMATION_TYPE_NONE
|
||||
if (source == null || animationType == ANIMATION_TYPE_NONE
|
||||
|| source.getFrame().equals(newSource.getFrame())) {
|
||||
mPendingFrame = null;
|
||||
mPendingVisibleFrame = null;
|
||||
@@ -295,7 +295,7 @@ public class InsetsSourceConsumer {
|
||||
}
|
||||
|
||||
// Frame is changing while animating. Keep note of the new frame but keep existing frame
|
||||
// until animaition is finished.
|
||||
// until animation is finished.
|
||||
newSource = new InsetsSource(newSource);
|
||||
mPendingFrame = new Rect(newSource.getFrame());
|
||||
mPendingVisibleFrame = newSource.getVisibleFrame() != null
|
||||
|
||||
@@ -26,13 +26,11 @@ import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
@@ -135,37 +133,29 @@ public class InsetsSourceConsumerTest {
|
||||
InsetsSourceConsumer consumer = new InsetsSourceConsumer(
|
||||
ITYPE_IME, state, null, controller);
|
||||
|
||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
||||
|
||||
InsetsSource source = new InsetsSource(ITYPE_IME);
|
||||
source.setFrame(0, 1, 2, 3);
|
||||
consumer.updateSource(new InsetsSource(source));
|
||||
|
||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER);
|
||||
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_NONE);
|
||||
|
||||
// While we're animating, updates are delayed
|
||||
source.setFrame(4, 5, 6, 7);
|
||||
consumer.updateSource(new InsetsSource(source));
|
||||
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||
assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ITYPE_IME).getFrame());
|
||||
|
||||
// Finish the animation, now the pending frame should be applied
|
||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
||||
assertTrue(consumer.notifyAnimationFinished());
|
||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||
|
||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER);
|
||||
|
||||
// Animating again, updates are delayed
|
||||
source.setFrame(8, 9, 10, 11);
|
||||
consumer.updateSource(new InsetsSource(source));
|
||||
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||
|
||||
// Updating with the current frame triggers a different code path, verify this clears
|
||||
// the pending 8, 9, 10, 11 frame:
|
||||
source.setFrame(4, 5, 6, 7);
|
||||
consumer.updateSource(new InsetsSource(source));
|
||||
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||
|
||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
||||
assertFalse(consumer.notifyAnimationFinished());
|
||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.systemui.globalactions;
|
||||
|
||||
import static android.view.WindowInsets.Type.ime;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
@@ -39,6 +40,7 @@ import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -52,6 +54,11 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
||||
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(
|
||||
TestActivity.class, false, false);
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
executeShellCommand("input keyevent HOME");
|
||||
}
|
||||
|
||||
/**
|
||||
* This test verifies that GlobalActions, which is frequently used to capture bugreports,
|
||||
* doesn't interfere with the IME, i.e. soft-keyboard state.
|
||||
@@ -68,6 +75,9 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
||||
waitUntil("Ime is not visible", activity::isImeVisible);
|
||||
}
|
||||
|
||||
// In some cases, IME is not controllable. e.g., floating IME or fullscreen IME.
|
||||
final boolean activityControlledIme = activity.mControlsIme;
|
||||
|
||||
executeShellCommand("input keyevent --longpress POWER");
|
||||
|
||||
waitUntil("activity loses focus", () -> !activity.mHasFocus);
|
||||
@@ -77,9 +87,9 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
||||
|
||||
runAssertionOnMainThread(() -> {
|
||||
assertTrue("IME should remain visible behind GlobalActions, but didn't",
|
||||
activity.mControlsIme);
|
||||
assertTrue("App behind GlobalActions should remain in control of IME, but didn't",
|
||||
activity.mImeVisible);
|
||||
assertEquals("App behind GlobalActions should remain in control of IME, but didn't",
|
||||
activityControlledIme, activity.mControlsIme);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,7 +191,7 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
||||
}
|
||||
|
||||
boolean isImeVisible() {
|
||||
return mHasFocus && mControlsIme && mImeVisible;
|
||||
return mHasFocus && mImeVisible;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user