Merge "Fix the accessibility floating menu overlap with the keyboard." into sc-dev
This commit is contained in:
@@ -18,6 +18,8 @@ package com.android.systemui.accessibility.floatingmenu;
|
|||||||
|
|
||||||
import static android.util.MathUtils.constrain;
|
import static android.util.MathUtils.constrain;
|
||||||
import static android.util.MathUtils.sq;
|
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;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ import android.content.Context;
|
|||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Insets;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
@@ -42,7 +45,9 @@ import android.view.Gravity;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.view.WindowMetrics;
|
||||||
import android.view.accessibility.AccessibilityNodeInfo;
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
@@ -90,6 +95,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
private boolean mIsShowing;
|
private boolean mIsShowing;
|
||||||
private boolean mIsDownInEnlargedTouchArea;
|
private boolean mIsDownInEnlargedTouchArea;
|
||||||
private boolean mIsDragging = false;
|
private boolean mIsDragging = false;
|
||||||
|
private boolean mImeVisibility;
|
||||||
@Alignment
|
@Alignment
|
||||||
private int mAlignment = Alignment.RIGHT;
|
private int mAlignment = Alignment.RIGHT;
|
||||||
@SizeType
|
@SizeType
|
||||||
@@ -369,6 +375,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
|
|
||||||
mIsShowing = true;
|
mIsShowing = true;
|
||||||
mWindowManager.addView(this, mCurrentLayoutParams);
|
mWindowManager.addView(this, mCurrentLayoutParams);
|
||||||
|
|
||||||
|
setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
|
||||||
setSystemGestureExclusion();
|
setSystemGestureExclusion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +387,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
|
|
||||||
mIsShowing = false;
|
mIsShowing = false;
|
||||||
mWindowManager.removeView(this);
|
mWindowManager.removeView(this);
|
||||||
|
|
||||||
|
setOnApplyWindowInsetsListener(null);
|
||||||
setSystemGestureExclusion();
|
setSystemGestureExclusion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +575,16 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
return mListView.dispatchTouchEvent(event);
|
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) {
|
private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
|
||||||
return (side == Alignment.RIGHT && currentRawX > downX)
|
return (side == Alignment.RIGHT && currentRawX > downX)
|
||||||
|| (side == Alignment.LEFT && downX > currentRawX);
|
|| (side == Alignment.LEFT && downX > currentRawX);
|
||||||
@@ -658,6 +678,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||||
PixelFormat.TRANSLUCENT);
|
PixelFormat.TRANSLUCENT);
|
||||||
|
params.receiveInsetsIgnoringZOrder = true;
|
||||||
params.windowAnimations = android.R.style.Animation_Translucent;
|
params.windowAnimations = android.R.style.Animation_Translucent;
|
||||||
params.gravity = Gravity.START | Gravity.TOP;
|
params.gravity = Gravity.START | Gravity.TOP;
|
||||||
params.x = getMaxWindowX();
|
params.x = getMaxWindowX();
|
||||||
@@ -728,10 +749,30 @@ public class AccessibilityFloatingMenuView extends FrameLayout
|
|||||||
*/
|
*/
|
||||||
private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
|
private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
|
||||||
mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
|
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);
|
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) {
|
private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
|
||||||
final float halfWidth = getLayoutWidth() / 2.0f;
|
final float halfWidth = getLayoutWidth() / 2.0f;
|
||||||
final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
|
final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
|
||||||
|
|||||||
@@ -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_ALWAYS;
|
||||||
import static android.view.View.OVER_SCROLL_NEVER;
|
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;
|
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.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Insets;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
import android.graphics.drawable.GradientDrawable;
|
||||||
@@ -43,7 +47,9 @@ import android.testing.TestableLooper;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewPropertyAnimator;
|
import android.view.ViewPropertyAnimator;
|
||||||
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.view.WindowMetrics;
|
||||||
import android.view.accessibility.AccessibilityNodeInfo;
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -79,12 +85,17 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
|
|||||||
@Mock
|
@Mock
|
||||||
private ViewPropertyAnimator mAnimator;
|
private ViewPropertyAnimator mAnimator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private WindowMetrics mWindowMetrics;
|
||||||
|
|
||||||
private MotionEvent mInterceptMotionEvent;
|
private MotionEvent mInterceptMotionEvent;
|
||||||
|
|
||||||
private RecyclerView mListView;
|
private RecyclerView mListView;
|
||||||
|
|
||||||
private Rect mAvailableBounds = new Rect(100, 200, 300, 400);
|
private Rect mAvailableBounds = new Rect(100, 200, 300, 400);
|
||||||
|
|
||||||
|
private int mScreenHeight;
|
||||||
|
private int mMenuWindowHeight;
|
||||||
private int mMenuHalfWidth;
|
private int mMenuHalfWidth;
|
||||||
private int mMenuHalfHeight;
|
private int mMenuHalfHeight;
|
||||||
private int mScreenHalfWidth;
|
private int mScreenHalfWidth;
|
||||||
@@ -117,12 +128,13 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
|
|||||||
final int menuWidth = padding * 2 + iconWidthHeight;
|
final int menuWidth = padding * 2 + iconWidthHeight;
|
||||||
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
|
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
|
||||||
final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
|
final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
|
||||||
final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
|
mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
|
||||||
mMenuHalfWidth = menuWidth / 2;
|
mMenuHalfWidth = menuWidth / 2;
|
||||||
mMenuHalfHeight = menuHeight / 2;
|
mMenuHalfHeight = menuHeight / 2;
|
||||||
mScreenHalfWidth = screenWidth / 2;
|
mScreenHalfWidth = screenWidth / 2;
|
||||||
mScreenHalfHeight = screenHeight / 2;
|
mScreenHalfHeight = mScreenHeight / 2;
|
||||||
mMaxWindowX = screenWidth - margin - menuWidth;
|
mMaxWindowX = screenWidth - margin - menuWidth;
|
||||||
|
mMenuWindowHeight = menuHeight + margin * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -464,12 +476,80 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
|
|||||||
assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER);
|
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
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
mInterceptMotionEvent = null;
|
mInterceptMotionEvent = null;
|
||||||
mMotionEventHelper.recycleEvents();
|
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 {
|
private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
|
||||||
TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
|
TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
|
||||||
super(context, listView);
|
super(context, listView);
|
||||||
|
|||||||
Reference in New Issue
Block a user