Refactor the design and improve the animations of Accessibility Floating Menu(9/n).

Actions:
1) Support the feature to avoid overlapping on the keyboard.
2) Fix the issue overlapping on primary action on the bottom.

Bug: 227715451
Bug: 252913008
Test: manual test
Change-Id: Ie3404276c56c9fb1e3d18d348cfc5a7b4815c905
This commit is contained in:
Peter Liang
2022-10-31 20:53:46 +08:00
parent 41752009bc
commit e1e1887ece
6 changed files with 198 additions and 16 deletions

View File

@@ -1341,6 +1341,7 @@
<dimen name="accessibility_floating_menu_large_width_height">56dp</dimen>
<dimen name="accessibility_floating_menu_large_single_radius">35dp</dimen>
<dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen>
<dimen name="accessibility_floating_menu_ime_shifting_space">48dp</dimen>
<dimen name="accessibility_floating_menu_message_container_horizontal_padding">15dp</dimen>
<dimen name="accessibility_floating_menu_message_text_vertical_padding">8dp</dimen>

View File

@@ -345,7 +345,7 @@ class MenuAnimationController {
}
private void constrainPositionAndUpdate(PointF position) {
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
final Rect draggableBounds = mMenuView.getMenuDraggableBoundsExcludeIme();
// Have the space gap margin between the top bound and the menu view, so actually the
// position y range needs to cut the margin.
position.offset(-draggableBounds.left, -draggableBounds.top);

View File

@@ -219,6 +219,14 @@ class MenuView extends FrameLayout implements
return mMenuViewAppearance.getMenuDraggableBounds();
}
Rect getMenuDraggableBoundsExcludeIme() {
return mMenuViewAppearance.getMenuDraggableBoundsExcludeIme();
}
int getMenuHeight() {
return mMenuViewAppearance.getMenuHeight();
}
void persistPositionAndUpdateEdge(Position percentagePosition) {
mMenuViewModel.updateMenuSavingPosition(percentagePosition);
mMenuViewAppearance.setPercentagePosition(percentagePosition);

View File

@@ -47,6 +47,9 @@ class MenuViewAppearance {
private final Resources mRes;
private final Position mPercentagePosition = new Position(/* percentageX= */
0f, /* percentageY= */ 0f);
private boolean mIsImeShowing;
// Avoid the menu view overlapping on the primary action button under the bottom as possible.
private int mImeShiftingSpace;
private int mTargetFeaturesSize;
private int mSizeType;
private int mMargin;
@@ -62,6 +65,7 @@ class MenuViewAppearance {
private int mStrokeColor;
private int mInset;
private int mElevation;
private float mImeTop;
private float[] mRadii;
private Drawable mBackgroundDrawable;
private String mContentDescription;
@@ -106,6 +110,8 @@ class MenuViewAppearance {
mStrokeColor = mRes.getColor(R.color.accessibility_floating_menu_stroke_dark);
mInset = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
mElevation = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
mImeShiftingSpace = mRes.getDimensionPixelSize(
R.dimen.accessibility_floating_menu_ime_shifting_space);
final Drawable drawable =
mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
@@ -131,29 +137,56 @@ class MenuViewAppearance {
mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
}
void onImeVisibilityChanged(boolean imeShowing, float imeTop) {
mIsImeShowing = imeShowing;
mImeTop = imeTop;
}
Rect getMenuDraggableBounds() {
return getMenuDraggableBoundsWith(/* includeIme= */ true);
}
Rect getMenuDraggableBoundsExcludeIme() {
return getMenuDraggableBoundsWith(/* includeIme= */ false);
}
private Rect getMenuDraggableBoundsWith(boolean includeIme) {
final int margin = getMenuMargin();
final Rect draggableBounds = getWindowAvailableBounds();
final Rect draggableBounds = new Rect(getWindowAvailableBounds());
// Initializes start position for mapping the translation of the menu view.
draggableBounds.offsetTo(/* newLeft= */ 0, /* newTop= */ 0);
draggableBounds.top += margin;
draggableBounds.right -= getMenuWidth();
draggableBounds.bottom -= Math.min(
getWindowAvailableBounds().height() - draggableBounds.top,
calculateActualMenuHeight() + margin);
if (includeIme && mIsImeShowing) {
final int imeHeight = (int) (draggableBounds.bottom - mImeTop);
draggableBounds.bottom -= (imeHeight + mImeShiftingSpace);
}
draggableBounds.bottom -= (calculateActualMenuHeight() + margin);
draggableBounds.bottom = Math.max(draggableBounds.top, draggableBounds.bottom);
return draggableBounds;
}
PointF getMenuPosition() {
final Rect draggableBounds = getMenuDraggableBounds();
final Rect draggableBounds = getMenuDraggableBoundsExcludeIme();
final float x = draggableBounds.left
+ draggableBounds.width() * mPercentagePosition.getPercentageX();
return new PointF(
draggableBounds.left
+ draggableBounds.width() * mPercentagePosition.getPercentageX(),
draggableBounds.top
+ draggableBounds.height() * mPercentagePosition.getPercentageY());
float y = draggableBounds.top
+ draggableBounds.height() * mPercentagePosition.getPercentageY();
// If the bottom of the menu view and overlap on the ime, its position y will be
// overridden with new y.
final float menuBottom = y + getMenuHeight() + mMargin;
if (mIsImeShowing && (menuBottom >= mImeTop)) {
y = Math.max(draggableBounds.top,
mImeTop - getMenuHeight() - mMargin - mImeShiftingSpace);
}
return new PointF(x, y);
}
String getContentDescription() {

View File

@@ -16,6 +16,10 @@
package com.android.systemui.accessibility.floatingmenu;
import static android.view.WindowInsets.Type.ime;
import static androidx.core.view.WindowInsetsCompat.Type;
import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE;
import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
@@ -26,13 +30,16 @@ import android.annotation.IntDef;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.PluralsMessageFormatter;
import android.view.MotionEvent;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -62,14 +69,17 @@ import java.util.Map;
class MenuViewLayer extends FrameLayout {
private static final int SHOW_MESSAGE_DELAY_MS = 3000;
private final WindowManager mWindowManager;
private final MenuView mMenuView;
private final MenuMessageView mMessageView;
private final DismissView mDismissView;
private final MenuViewAppearance mMenuViewAppearance;
private final MenuAnimationController mMenuAnimationController;
private final AccessibilityManager mAccessibilityManager;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final IAccessibilityFloatingMenu mFloatingMenu;
private final DismissAnimationController mDismissAnimationController;
private final Rect mImeInsetsRect = new Rect();
@IntDef({
LayerIndex.MENU_VIEW,
@@ -111,13 +121,13 @@ class MenuViewLayer extends FrameLayout {
AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) {
super(context);
mWindowManager = windowManager;
mAccessibilityManager = accessibilityManager;
mFloatingMenu = floatingMenu;
final MenuViewModel menuViewModel = new MenuViewModel(context);
final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context,
windowManager);
mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
mMenuView = new MenuView(context, menuViewModel, mMenuViewAppearance);
mMenuAnimationController = mMenuView.getMenuAnimationController();
mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
@@ -200,6 +210,7 @@ class MenuViewLayer extends FrameLayout {
super.onAttachedToWindow();
mMenuView.show();
setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
mMessageView.setUndoListener(view -> undo());
mContext.registerComponentCallbacks(mDismissAnimationController);
}
@@ -209,10 +220,35 @@ class MenuViewLayer extends FrameLayout {
super.onDetachedFromWindow();
mMenuView.hide();
setOnApplyWindowInsetsListener(null);
mHandler.removeCallbacksAndMessages(/* token= */ null);
mContext.unregisterComponentCallbacks(mDismissAnimationController);
}
private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final WindowInsets windowInsets = windowMetrics.getWindowInsets();
final Rect imeInsetsRect = windowInsets.getInsets(ime()).toRect();
if (!imeInsetsRect.equals(mImeInsetsRect)) {
final Rect windowBounds = new Rect(windowMetrics.getBounds());
final Rect systemBarsAndDisplayCutoutInsetsRect =
windowInsets.getInsetsIgnoringVisibility(
Type.systemBars() | Type.displayCutout()).toRect();
final float imeTop =
windowBounds.height() - systemBarsAndDisplayCutoutInsetsRect.top
- imeInsetsRect.bottom;
mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop);
mMenuView.onEdgeChanged();
mMenuView.onPositionChanged();
mImeInsetsRect.set(imeInsetsRect);
}
return insets;
}
private void hideMenuAndShowMessage() {
final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
SHOW_MESSAGE_DELAY_MS,

View File

@@ -18,12 +18,17 @@ package com.android.systemui.accessibility.floatingmenu;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.systemBars;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,13 +37,18 @@ import android.content.ComponentName;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.graphics.Insets;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
@@ -68,9 +78,20 @@ public class MenuViewLayerTest extends SysuiTestCase {
private static final ComponentName TEST_SELECT_TO_SPEAK_COMPONENT_NAME = new ComponentName(
SELECT_TO_SPEAK_PACKAGE_NAME, SELECT_TO_SPEAK_SERVICE_NAME);
private static final int DISPLAY_WINDOW_WIDTH = 1080;
private static final int DISPLAY_WINDOW_HEIGHT = 2340;
private static final int STATUS_BAR_HEIGHT = 75;
private static final int NAVIGATION_BAR_HEIGHT = 125;
private static final int IME_HEIGHT = 350;
private static final int IME_TOP =
DISPLAY_WINDOW_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT - IME_HEIGHT;
private MenuViewLayer mMenuViewLayer;
private String mLastAccessibilityButtonTargets;
private String mLastEnabledAccessibilityServices;
private WindowMetrics mWindowMetrics;
private MenuView mMenuView;
private MenuAnimationController mMenuAnimationController;
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@@ -78,14 +99,24 @@ public class MenuViewLayerTest extends SysuiTestCase {
@Mock
private IAccessibilityFloatingMenu mFloatingMenu;
@Mock
private WindowManager mStubWindowManager;
@Mock
private AccessibilityManager mStubAccessibilityManager;
@Before
public void setUp() throws Exception {
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mStubAccessibilityManager,
final Rect mDisplayBounds = new Rect();
mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
DISPLAY_WINDOW_HEIGHT);
mWindowMetrics = spy(new WindowMetrics(mDisplayBounds, fakeDisplayInsets()));
doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
mFloatingMenu);
mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
mMenuAnimationController = mMenuView.getMenuAnimationController();
mLastAccessibilityButtonTargets =
Settings.Secure.getStringForUser(mContext.getContentResolver(),
@@ -93,6 +124,12 @@ public class MenuViewLayerTest extends SysuiTestCase {
mLastEnabledAccessibilityServices =
Settings.Secure.getStringForUser(mContext.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
mMenuViewLayer.onAttachedToWindow();
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT);
}
@After
@@ -103,6 +140,8 @@ public class MenuViewLayerTest extends SysuiTestCase {
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
UserHandle.USER_CURRENT);
mMenuViewLayer.onDetachedFromWindow();
}
@Test
@@ -168,4 +207,69 @@ public class MenuViewLayerTest extends SysuiTestCase {
assertThat(value).isEqualTo("");
}
@Test
public void showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition() {
final float menuTop = STATUS_BAR_HEIGHT + 100;
mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
dispatchShowingImeInsets();
assertThat(mMenuView.getTranslationX()).isEqualTo(0);
assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
}
@Test
public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
final float menuTop = IME_TOP + 100;
mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
dispatchShowingImeInsets();
final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight();
assertThat(mMenuView.getTranslationX()).isEqualTo(0);
assertThat(menuBottom).isLessThan(IME_TOP);
}
@Test
public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
final float menuTop = IME_TOP + 200;
mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
dispatchShowingImeInsets();
dispatchHidingImeInsets();
assertThat(mMenuView.getTranslationX()).isEqualTo(0);
assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
}
private void dispatchShowingImeInsets() {
final WindowInsets fakeShowingImeInsets = fakeImeInsets(/* isImeVisible= */ true);
doReturn(fakeShowingImeInsets).when(mWindowMetrics).getWindowInsets();
mMenuViewLayer.dispatchApplyWindowInsets(fakeShowingImeInsets);
}
private void dispatchHidingImeInsets() {
final WindowInsets fakeHidingImeInsets = fakeImeInsets(/* isImeVisible= */ false);
doReturn(fakeHidingImeInsets).when(mWindowMetrics).getWindowInsets();
mMenuViewLayer.dispatchApplyWindowInsets(fakeHidingImeInsets);
}
private WindowInsets fakeDisplayInsets() {
return new WindowInsets.Builder()
.setVisible(systemBars() | displayCutout(), /* visible= */ true)
.setInsets(systemBars() | displayCutout(),
Insets.of(/* left= */ 0, STATUS_BAR_HEIGHT, /* right= */ 0,
NAVIGATION_BAR_HEIGHT))
.build();
}
private WindowInsets fakeImeInsets(boolean isImeVisible) {
final int bottom = isImeVisible ? (IME_HEIGHT + NAVIGATION_BAR_HEIGHT) : 0;
return new WindowInsets.Builder()
.setVisible(ime(), isImeVisible)
.setInsets(ime(),
Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, bottom))
.build();
}
}