From 5fec1068668556231a72a1775d5858e381c2f2d4 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Wed, 19 May 2021 18:52:30 +0800 Subject: [PATCH] Fix the accessibility floating menu overlap with the keyboard. Solution: Use WindowInset to get the ime height to avoid overlapping each other. Bug: 175364596 Test: atest AccessibilityFloatingMenuViewTest Change-Id: I01efda29d0bd91a580d2595ec238d1e459c60e9e --- .../AccessibilityFloatingMenuView.java | 43 +++++++++- .../AccessibilityFloatingMenuViewTest.java | 84 ++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index e85bd885c88a6..86a37ecbc4a96 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -18,6 +18,8 @@ package com.android.systemui.accessibility.floatingmenu; import static android.util.MathUtils.constrain; import static android.util.MathUtils.sq; +import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsets.Type.navigationBars; import static java.util.Objects.requireNonNull; @@ -29,6 +31,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -42,7 +45,9 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Animation; @@ -90,6 +95,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout private boolean mIsShowing; private boolean mIsDownInEnlargedTouchArea; private boolean mIsDragging = false; + private boolean mImeVisibility; @Alignment private int mAlignment = Alignment.RIGHT; @SizeType @@ -369,6 +375,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout mIsShowing = true; mWindowManager.addView(this, mCurrentLayoutParams); + + setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets)); setSystemGestureExclusion(); } @@ -379,6 +387,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout mIsShowing = false; mWindowManager.removeView(this); + + setOnApplyWindowInsetsListener(null); setSystemGestureExclusion(); } @@ -565,6 +575,16 @@ public class AccessibilityFloatingMenuView extends FrameLayout return mListView.dispatchTouchEvent(event); } + private WindowInsets onWindowInsetsApplied(WindowInsets insets) { + final boolean currentImeVisibility = insets.isVisible(ime()); + if (currentImeVisibility != mImeVisibility) { + mImeVisibility = currentImeVisibility; + updateLocationWith(mAlignment, mPercentageY); + } + + return insets; + } + private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) { return (side == Alignment.RIGHT && currentRawX > downX) || (side == Alignment.LEFT && downX > currentRawX); @@ -658,6 +678,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT); + params.receiveInsetsIgnoringZOrder = true; params.windowAnimations = android.R.style.Animation_Translucent; params.gravity = Gravity.START | Gravity.TOP; params.x = getMaxWindowX(); @@ -728,10 +749,30 @@ public class AccessibilityFloatingMenuView extends FrameLayout */ private void updateLocationWith(@Alignment int side, float percentageCurrentY) { mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX(); - mCurrentLayoutParams.y = (int) (percentageCurrentY * getMaxWindowY()); + final int currentLayoutY = (int) (percentageCurrentY * getMaxWindowY()); + mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval()); mWindowManager.updateViewLayout(this, mCurrentLayoutParams); } + /** + * Gets the moving interval to not overlap between the keyboard and menu view. + * + * @return the moving interval if they overlap each other, otherwise 0. + */ + private int getInterval() { + if (!mImeVisibility) { + return 0; + } + + final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + final Insets imeInsets = windowMetrics.getWindowInsets().getInsets( + ime() | navigationBars()); + final int imeY = mScreenHeight - imeInsets.bottom; + final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight(); + + return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0; + } + private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) { final float halfWidth = getLayoutWidth() / 2.0f; final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index 0de257ad424b5..65acba9bba859 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -18,6 +18,8 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.View.OVER_SCROLL_ALWAYS; import static android.view.View.OVER_SCROLL_NEVER; +import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsets.Type.navigationBars; import static com.google.common.truth.Truth.assertThat; @@ -31,9 +33,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -43,7 +47,9 @@ import android.testing.TestableLooper; import android.view.MotionEvent; import android.view.View; import android.view.ViewPropertyAnimator; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; @@ -79,12 +85,17 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { @Mock private ViewPropertyAnimator mAnimator; + @Mock + private WindowMetrics mWindowMetrics; + private MotionEvent mInterceptMotionEvent; private RecyclerView mListView; private Rect mAvailableBounds = new Rect(100, 200, 300, 400); + private int mScreenHeight; + private int mMenuWindowHeight; private int mMenuHalfWidth; private int mMenuHalfHeight; private int mScreenHalfWidth; @@ -117,12 +128,13 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { final int menuWidth = padding * 2 + iconWidthHeight; final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding; final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; - final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels; + mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels; mMenuHalfWidth = menuWidth / 2; mMenuHalfHeight = menuHeight / 2; mScreenHalfWidth = screenWidth / 2; - mScreenHalfHeight = screenHeight / 2; + mScreenHalfHeight = mScreenHeight / 2; mMaxWindowX = screenWidth - margin - menuWidth; + mMenuWindowHeight = menuHeight + margin * 2; } @Test @@ -464,12 +476,80 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER); } + @Test + public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() { + final int offset = 200; + + showMenuWithLatestStatus(); + final WindowInsets imeInset = fakeImeInsetWith(offset); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset); + final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y - offset; + mMenuView.dispatchApplyWindowInsets(imeInset); + + assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY); + } + + @Test + public void hideIme_onMenuViewShifted_menuViewMovedBack() { + final int offset = 200; + showMenuWithLatestStatus(); + final WindowInsets imeInset = fakeImeInsetWith(offset); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset); + final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y; + mMenuView.dispatchApplyWindowInsets(imeInset); + + mMenuView.dispatchApplyWindowInsets( + new WindowInsets.Builder().setVisible(ime(), false).build()); + + assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY); + } + + @Test + public void showMenuAndIme_withHigherIme_alignScreenTopEdge() { + final int offset = 99999; + + showMenuWithLatestStatus(); + final WindowInsets imeInset = fakeImeInsetWith(offset); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset); + mMenuView.dispatchApplyWindowInsets(imeInset); + + assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0); + } + @After public void tearDown() { mInterceptMotionEvent = null; mMotionEventHelper.recycleEvents(); } + private void showMenuWithLatestStatus() { + mMenuView.show(); + mMenuView.onTargetsChanged(mTargets); + mMenuView.setSizeType(0); + mMenuView.setShapeType(0); + } + + /** + * Based on the current menu status, fake the ime inset component {@link WindowInsets} used + * for testing. + * + * @param offset is used for the y-axis position of ime higher than the y-axis position of menu. + * @return the ime inset + */ + private WindowInsets fakeImeInsetWith(int offset) { + // Ensure the keyboard has overlapped on the menu view. + final int fakeImeHeight = + mScreenHeight - (mMenuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset; + + return new WindowInsets.Builder() + .setVisible(ime() | navigationBars(), true) + .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight)) + .build(); + } + private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView { TestAccessibilityFloatingMenu(Context context, RecyclerView listView) { super(context, listView);