From ae8863ee7737cf54a2c4ae720a54c7190f076e44 Mon Sep 17 00:00:00 2001 From: Peter Liang Date: Tue, 27 Sep 2022 00:00:44 +0800 Subject: [PATCH 1/2] Support the feature of Drag to remove for the Accessibility Floating Menu(1/n). Goals: Based on the animation implementation and improvements, provide the capability of dismissing the accessibility floating menu view on the home screen. Actions: 1) Add and interact between the magnetized object and magnetic target. Bug: 237716018 Test: atest MenuTouchHandlerTest MenuViewLayerTest Change-Id: I9428c3a3e4e26b9fa4595bc2b6a3a7526c54cef1 --- .../DismissAnimationController.java | 175 ++++++++++++++++++ .../floatingmenu/MenuAnimationController.java | 11 ++ .../MenuListViewTouchHandler.java | 25 ++- .../accessibility/floatingmenu/MenuView.java | 7 +- .../floatingmenu/MenuViewLayer.java | 74 +++++++- .../floatingmenu/MenuViewLayerController.java | 2 +- .../DismissAnimationControllerTest.java | 67 +++++++ .../MenuAnimationControllerTest.java | 22 ++- .../MenuListViewTouchHandlerTest.java | 33 +++- .../floatingmenu/MenuViewLayerTest.java | 21 ++- 10 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java new file mode 100644 index 0000000000000..f56a15427452a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.ComponentCallbacks; +import android.content.res.Configuration; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; + +import com.android.systemui.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +/** + * Controls the interaction between {@link MagnetizedObject} and + * {@link MagnetizedObject.MagneticTarget}. + */ +class DismissAnimationController implements ComponentCallbacks { + private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; + private static final float ANIMATING_MAX_ALPHA = 0.7f; + + private final DismissView mDismissView; + private final MenuView mMenuView; + private final ValueAnimator mDismissAnimator; + private final MagnetizedObject mMagnetizedObject; + private float mMinDismissSize; + private float mSizePercent; + + DismissAnimationController(DismissView dismissView, MenuView menuView) { + mDismissView = dismissView; + mDismissView.setPivotX(dismissView.getWidth() / 2.0f); + mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + mMenuView = menuView; + + updateResources(); + + mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); + mDismissAnimator.addUpdateListener(dismissAnimation -> { + final float animatedValue = (float) dismissAnimation.getAnimatedValue(); + final float scaleValue = Math.max(animatedValue, mSizePercent); + dismissView.getCircle().setScaleX(scaleValue); + dismissView.getCircle().setScaleY(scaleValue); + + menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + }); + + mDismissAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + super.onAnimationEnd(animation, isReverse); + + if (isReverse) { + mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); + mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); + mMenuView.setAlpha(COMPLETELY_OPAQUE); + } + } + }); + + mMagnetizedObject = + new MagnetizedObject(mMenuView.getContext(), mMenuView, + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_X), + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_Y)) { + @Override + public void getLocationOnScreen(MenuView underlyingObject, int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + + @Override + public float getHeight(MenuView underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public float getWidth(MenuView underlyingObject) { + return underlyingObject.getWidth(); + } + }; + + final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget( + dismissView.getCircle(), (int) mMinDismissSize); + mMagnetizedObject.addTarget(magneticTarget); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + updateResources(); + } + + @Override + public void onLowMemory() { + // Do nothing + } + + void showDismissView(boolean show) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } + } + + void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { + mMagnetizedObject.setMagnetListener(magnetListener); + } + + void maybeConsumeDownMotionEvent(MotionEvent event) { + mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeMoveMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeUpMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + void animateDismissMenu(boolean scaleUp) { + if (scaleUp) { + mDismissAnimator.start(); + } else { + mDismissAnimator.reverse(); + } + } + + private void updateResources() { + final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_size); + mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_small); + mSizePercent = mMinDismissSize / maxDismissSize; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index d6d0399035054..e5313adf94d49 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -47,6 +47,8 @@ class MenuAnimationController { private static final float MIN_PERCENT = 0.0f; private static final float MAX_PERCENT = 1.0f; private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float SCALE_SHRINK = 0.0f; private static final float FLING_FRICTION_SCALAR = 1.9f; private static final float DEFAULT_FRICTION = 4.2f; private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; @@ -297,6 +299,15 @@ class MenuAnimationController { mMenuView.onDraggingStart(); } + void startShrinkAnimation(Runnable endAction) { + mMenuView.animate() + .scaleX(SCALE_SHRINK) + .scaleY(SCALE_SHRINK) + .alpha(COMPLETELY_TRANSPARENT) + .translationY(mMenuView.getTranslationY()) + .withEndAction(endAction).start(); + } + private void onSpringAnimationEnd(PointF position) { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java index 3146c9f0d2af4..bc3cf0a6bab03 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -38,9 +38,12 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { private final PointF mMenuTranslationDown = new PointF(); private boolean mIsDragging = false; private float mTouchSlop; + private final DismissAnimationController mDismissAnimationController; - MenuListViewTouchHandler(MenuAnimationController menuAnimationController) { + MenuListViewTouchHandler(MenuAnimationController menuAnimationController, + DismissAnimationController dismissAnimationController) { mMenuAnimationController = menuAnimationController; + mDismissAnimationController = dismissAnimationController; } @Override @@ -61,6 +64,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY()); mMenuAnimationController.cancelAnimations(); + mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent); break; case MotionEvent.ACTION_MOVE: if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) { @@ -69,8 +73,13 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuAnimationController.onDraggingStart(); } - mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); - mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy); + mDismissAnimationController.showDismissView(/* show= */ true); + + if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) { + mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); + mMenuAnimationController.moveToPositionYIfNeeded( + mMenuTranslationDown.y + dy); + } } break; case MotionEvent.ACTION_UP: @@ -79,10 +88,18 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { final float endX = mMenuTranslationDown.x + dx; mIsDragging = false; - if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + mDismissAnimationController.showDismissView(/* show= */ false); + mMenuAnimationController.fadeOutIfEnabled(); + + return true; + } + + if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) { mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); mMenuAnimationController.flingMenuThenSpringToEdge(endX, mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + mDismissAnimationController.showDismissView(/* show= */ false); } // Avoid triggering the listener of the item. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 15d139cf15da5..2e8570cb52607 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -42,7 +42,7 @@ import java.util.Collections; import java.util.List; /** - * The menu view displays the accessibility features. + * The container view displays the accessibility features. */ @SuppressLint("ViewConstructor") class MenuView extends FrameLayout implements @@ -70,7 +70,6 @@ class MenuView extends FrameLayout implements mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; mMenuAnimationController = new MenuAnimationController(this); - mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); @@ -112,6 +111,10 @@ class MenuView extends FrameLayout implements mTargetFeaturesView.addOnItemTouchListener(listener); } + MenuAnimationController getMenuAnimationController() { + return mMenuAnimationController; + } + @SuppressLint("NotifyDataSetChanged") private void onItemSizeChanged() { mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 5252519e9faf4..26bf5e354164c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -19,12 +19,20 @@ package com.android.systemui.accessibility.floatingmenu; import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Configuration; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; import android.view.MotionEvent; import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -34,24 +42,83 @@ import java.lang.annotation.RetentionPolicy; @SuppressLint("ViewConstructor") class MenuViewLayer extends FrameLayout { private final MenuView mMenuView; + private final DismissView mDismissView; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final IAccessibilityFloatingMenu mFloatingMenu; + private final DismissAnimationController mDismissAnimationController; @IntDef({ - LayerIndex.MENU_VIEW + LayerIndex.MENU_VIEW, + LayerIndex.DISMISS_VIEW }) @Retention(RetentionPolicy.SOURCE) @interface LayerIndex { int MENU_VIEW = 0; + int DISMISS_VIEW = 1; } - MenuViewLayer(@NonNull Context context, WindowManager windowManager) { + @VisibleForTesting + final Runnable mDismissMenuAction = new Runnable() { + @Override + public void run() { + Settings.Secure.putString(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ ""); + mFloatingMenu.hide(); + } + }; + + MenuViewLayer(@NonNull Context context, WindowManager windowManager, + IAccessibilityFloatingMenu floatingMenu) { super(context); + mFloatingMenu = floatingMenu; + final MenuViewModel menuViewModel = new MenuViewModel(context); final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); + final MenuAnimationController menuAnimationController = + mMenuView.getMenuAnimationController(); + + mDismissView = new DismissView(context); + mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); + mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velocityX, float velocityY, boolean wasFlungOut) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + menuAnimationController.startShrinkAnimation(() -> { + mMenuView.hide(); + + mHandler.post(mDismissMenuAction); + }); + + mDismissView.hide(); + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + }); + + final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler( + menuAnimationController, mDismissAnimationController); + mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler); addView(mMenuView, LayerIndex.MENU_VIEW); + addView(mDismissView, LayerIndex.DISMISS_VIEW); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDismissView.updateResources(); } @Override @@ -68,6 +135,7 @@ class MenuViewLayer extends FrameLayout { super.onAttachedToWindow(); mMenuView.show(); + mContext.registerComponentCallbacks(mDismissAnimationController); } @Override @@ -75,5 +143,7 @@ class MenuViewLayer extends FrameLayout { super.onDetachedFromWindow(); mMenuView.hide(); + mHandler.removeCallbacksAndMessages(/* token= */ null); + mContext.unregisterComponentCallbacks(mDismissAnimationController); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index d2093c200ca2e..c185546e34fa9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -34,7 +34,7 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { MenuViewLayerController(Context context, WindowManager windowManager) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager); + mMenuViewLayer = new MenuViewLayer(context, windowManager, this); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java new file mode 100644 index 0000000000000..8ef65dcb2c3a1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.wm.shell.bubbles.DismissView; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link DismissAnimationController}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class DismissAnimationControllerTest extends SysuiTestCase { + private DismissAnimationController mDismissAnimationController; + private DismissView mDismissView; + + @Before + public void setUp() throws Exception { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, + stubMenuViewAppearance); + mDismissView = new DismissView(mContext); + mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView); + } + + @Test + public void showDismissView_success() { + mDismissAnimationController.showDismissView(true); + + verify(mDismissView).show(); + } + + @Test + public void hideDismissView_success() { + mDismissAnimationController.showDismissView(false); + + verify(mDismissView).hide(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index dbf291c49ee51..31a7361b73388 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -18,9 +18,16 @@ package com.android.systemui.accessibility.floatingmenu; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import android.graphics.PointF; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import android.view.ViewPropertyAnimator; import android.view.WindowManager; import androidx.test.filters.SmallTest; @@ -36,6 +43,8 @@ import org.junit.runner.RunWith; @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class MenuAnimationControllerTest extends SysuiTestCase { + + private ViewPropertyAnimator mViewPropertyAnimator; private MenuView mMenuView; private MenuAnimationController mMenuAnimationController; @@ -45,7 +54,11 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); - mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mViewPropertyAnimator = spy(mMenuView.animate()); + doReturn(mViewPropertyAnimator).when(mMenuView).animate(); + mMenuAnimationController = new MenuAnimationController(mMenuView); } @@ -58,4 +71,11 @@ public class MenuAnimationControllerTest extends SysuiTestCase { assertThat(mMenuView.getTranslationX()).isEqualTo(50); assertThat(mMenuView.getTranslationY()).isEqualTo(60); } + + @Test + public void startShrinkAnimation_verifyAnimationEndAction() { + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE)); + + verify(mViewPropertyAnimator).withEndAction(any(Runnable.class)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index c5b9a294fc341..4acb394bee95d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -21,6 +21,8 @@ import static android.view.View.OVER_SCROLL_NEVER; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; +import com.android.wm.shell.bubbles.DismissView; import org.junit.After; import org.junit.Before; @@ -57,7 +60,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private MenuView mStubMenuView; private MenuListViewTouchHandler mTouchHandler; private MenuAnimationController mMenuAnimationController; + private DismissAnimationController mDismissAnimationController; private RecyclerView mStubListView; + private DismissView mDismissView; @Before public void setUp() throws Exception { @@ -69,7 +74,11 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { mStubMenuView.setTranslationX(0); mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView)); - mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController); + mDismissView = spy(new DismissView(mContext)); + mDismissAnimationController = + spy(new DismissAnimationController(mDismissView, mStubMenuView)); + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, + mDismissAnimationController); final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); mStubListView = (RecyclerView) mStubMenuView.getChildAt(0); mStubListView.setAdapter(stubAdapter); @@ -88,7 +97,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test - public void onActionMoveEvent_shouldMoveToPosition() { + public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() { + doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent( + any(MotionEvent.class)); final int offset = 100; final MotionEvent stubDownEvent = mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, @@ -107,6 +118,24 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { assertThat(mStubMenuView.getTranslationY()).isEqualTo(offset); } + @Test + public void onActionMoveEvent_shouldShowDismissView() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + verify(mDismissView).show(); + } + @Test public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { final int offset = 100; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 23c6ef1338b3f..8162035878b82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -23,6 +23,8 @@ import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.Laye import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -33,8 +35,12 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Tests for {@link MenuViewLayer}. */ @RunWith(AndroidTestingRunner.class) @@ -43,10 +49,16 @@ import org.junit.runner.RunWith; public class MenuViewLayerTest extends SysuiTestCase { private MenuViewLayer mMenuViewLayer; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private IAccessibilityFloatingMenu mFloatingMenu; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager); + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mFloatingMenu); } @Test @@ -64,4 +76,11 @@ public class MenuViewLayerTest extends SysuiTestCase { assertThat(menuView.getVisibility()).isEqualTo(GONE); } + + @Test + public void tiggerDismissMenuAction_hideFloatingMenu() { + mMenuViewLayer.mDismissMenuAction.run(); + + verify(mFloatingMenu).hide(); + } } From e8bff1ea10b381c2a000a516ed962cdcbfd0e60a Mon Sep 17 00:00:00 2001 From: Peter Liang Date: Fri, 14 Oct 2022 23:28:37 +0800 Subject: [PATCH 2/2] Support the feature of Drag to remove for the Accessibility Floating Menu(2/n). Action: Show the message view with the action when removing the menu view to prompt if users undo their operation. Bug: 237716018 Bug: 253114877 Test: atest MenuAnimationControllerTest Change-Id: I1f676efc47a5632d9e0097b2d10030769cc4b43d --- ...essibility_floating_message_background.xml | 22 +++ packages/SystemUI/res/values-night/colors.xml | 2 + packages/SystemUI/res/values/colors.xml | 2 + packages/SystemUI/res/values/dimens.xml | 8 + packages/SystemUI/res/values/strings.xml | 9 + .../AccessibilityFloatingMenuController.java | 7 +- .../floatingmenu/MenuAnimationController.java | 14 ++ .../floatingmenu/MenuMessageView.java | 162 ++++++++++++++++++ .../accessibility/floatingmenu/MenuView.java | 28 ++- .../floatingmenu/MenuViewLayer.java | 89 ++++++++-- .../floatingmenu/MenuViewLayerController.java | 6 +- ...cessibilityFloatingMenuControllerTest.java | 7 +- .../MenuAnimationControllerTest.java | 9 + .../MenuViewLayerControllerTest.java | 7 +- .../floatingmenu/MenuViewLayerTest.java | 6 +- 15 files changed, 357 insertions(+), 21 deletions(-) create mode 100644 packages/SystemUI/res/drawable/accessibility_floating_message_background.xml create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java diff --git a/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml new file mode 100644 index 0000000000000..de83df4e625c2 --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index dc2bee56373c3..16152f80308af 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -99,6 +99,8 @@ #B3000000 + @*android:color/background_material_dark + @*android:color/primary_text_default_material_dark @color/material_dynamic_secondary20 diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 9e8bef06270b6..55b59b63c2f9e 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -219,6 +219,8 @@ #CCFFFFFF #26FFFFFF + @*android:color/background_material_light + @*android:color/primary_text_default_material_light #33FFFFFF diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 66f0e7543469b..f02f29a4f7414 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1336,6 +1336,14 @@ 35dp 35dp + 15dp + 8dp + 8dp + 5dp + 14sp + 312dp + 48dp + 8dp 16dp -2dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d4d8843acdead..dee8bae3c4899 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2188,6 +2188,15 @@ Tap to open accessibility features. Customize or replace this button in Settings.\n\nView settings Move button to the edge to hide it temporarily + + Undo + + + {count, plural, + =1 {{label} shortcut removed} + other {# shortcuts removed} + } + Move top left diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index ea334b27fa09b..777d10c7acfd0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -28,6 +28,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.MainThread; @@ -56,6 +57,7 @@ public class AccessibilityFloatingMenuController implements private Context mContext; private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; + private final AccessibilityManager mAccessibilityManager; private final FeatureFlags mFeatureFlags; @VisibleForTesting IAccessibilityFloatingMenu mFloatingMenu; @@ -96,6 +98,7 @@ public class AccessibilityFloatingMenuController implements public AccessibilityFloatingMenuController(Context context, WindowManager windowManager, DisplayManager displayManager, + AccessibilityManager accessibilityManager, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, AccessibilityButtonModeObserver accessibilityButtonModeObserver, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -103,6 +106,7 @@ public class AccessibilityFloatingMenuController implements mContext = context; mWindowManager = windowManager; mDisplayManager = displayManager; + mAccessibilityManager = accessibilityManager; mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver; mAccessibilityButtonModeObserver = accessibilityButtonModeObserver; mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -180,7 +184,8 @@ public class AccessibilityFloatingMenuController implements final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY); mFloatingMenu = new MenuViewLayerController( mContext.createWindowContext(defaultDisplay, - TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager); + TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager, + mAccessibilityManager); } else { mFloatingMenu = new AccessibilityFloatingMenu(mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index e5313adf94d49..eb252323907d4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -49,6 +49,7 @@ class MenuAnimationController { private static final float COMPLETELY_OPAQUE = 1.0f; private static final float COMPLETELY_TRANSPARENT = 0.0f; private static final float SCALE_SHRINK = 0.0f; + private static final float SCALE_GROW = 1.0f; private static final float FLING_FRICTION_SCALAR = 1.9f; private static final float DEFAULT_FRICTION = 4.2f; private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; @@ -300,6 +301,8 @@ class MenuAnimationController { } void startShrinkAnimation(Runnable endAction) { + mMenuView.animate().cancel(); + mMenuView.animate() .scaleX(SCALE_SHRINK) .scaleY(SCALE_SHRINK) @@ -308,6 +311,17 @@ class MenuAnimationController { .withEndAction(endAction).start(); } + void startGrowAnimation() { + mMenuView.animate().cancel(); + + mMenuView.animate() + .scaleX(SCALE_GROW) + .scaleY(SCALE_GROW) + .alpha(COMPLETELY_OPAQUE) + .translationY(mMenuView.getTranslationY()) + .start(); + } + private void onSpringAnimationEnd(PointF position) { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java new file mode 100644 index 0000000000000..9875ad06f1ed5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static android.util.TypedValue.COMPLEX_UNIT_PX; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.settingslib.Utils; +import com.android.systemui.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The message view with the action prompt to whether to undo operation for users when removing + * the {@link MenuView}. + */ +class MenuMessageView extends LinearLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { + private final TextView mTextView; + private final Button mUndoButton; + + @IntDef({ + Index.TEXT_VIEW, + Index.UNDO_BUTTON + }) + @Retention(RetentionPolicy.SOURCE) + @interface Index { + int TEXT_VIEW = 0; + int UNDO_BUTTON = 1; + } + + MenuMessageView(Context context) { + super(context); + + setVisibility(GONE); + + mTextView = new TextView(context); + mUndoButton = new Button(context); + + addView(mTextView, Index.TEXT_VIEW, + new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1)); + addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateResources(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(WRAP_CONTENT, + WRAP_CONTENT); + containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + setLayoutParams(containerParams); + setGravity(Gravity.CENTER_VERTICAL); + + mUndoButton.setBackground(null); + + updateResources(); + + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + + if (getVisibility() == VISIBLE) { + final int x = (int) getX(); + final int y = (int) getY(); + inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight())); + } + } + + /** + * Registers a listener to be invoked when this undo action button is clicked. It should be + * called after {@link View#onAttachedToWindow()}. + * + * @param listener The listener that will run + */ + void setUndoListener(OnClickListener listener) { + mUndoButton.setOnClickListener(listener); + } + + private void updateResources() { + final Resources res = getResources(); + + final int containerPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_container_horizontal_padding); + final int margin = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_margin); + final FrameLayout.LayoutParams containerParams = + (FrameLayout.LayoutParams) getLayoutParams(); + containerParams.setMargins(margin, margin, margin, margin); + setLayoutParams(containerParams); + setBackground(res.getDrawable(R.drawable.accessibility_floating_message_background)); + setPadding(containerPadding, /* top= */ 0, containerPadding, /* bottom= */ 0); + setMinimumWidth( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_width)); + setMinimumHeight( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_height)); + setElevation( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_elevation)); + + final int textPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_vertical_padding); + final int textColor = res.getColor(R.color.accessibility_floating_menu_message_text); + final int textSize = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_size); + mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding); + mTextView.setTextSize(COMPLEX_UNIT_PX, textSize); + mTextView.setTextColor(textColor); + + final ColorStateList colorAccent = Utils.getColorAccent(getContext()); + mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo)); + mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize); + mUndoButton.setTextColor(colorAccent); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 2e8570cb52607..6a14af52fbaff 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -64,6 +64,8 @@ class MenuView extends FrameLayout implements this::onTargetFeaturesChanged; private final MenuViewAppearance mMenuViewAppearance; + private OnTargetFeaturesChangeListener mFeaturesChangeListener; + MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { super(context); @@ -95,7 +97,9 @@ class MenuView extends FrameLayout implements @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(mBoundsInParent); + if (getVisibility() == VISIBLE) { + inoutInfo.touchableRegion.union(mBoundsInParent); + } } @Override @@ -107,6 +111,10 @@ class MenuView extends FrameLayout implements mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); } + void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) { + mFeaturesChangeListener = listener; + } + void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { mTargetFeaturesView.addOnItemTouchListener(listener); } @@ -142,7 +150,7 @@ class MenuView extends FrameLayout implements onEdgeChanged(); } - private void onEdgeChanged() { + void onEdgeChanged() { final int[] insets = mMenuViewAppearance.getMenuInsets(); getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], insets[3]); @@ -196,6 +204,9 @@ class MenuView extends FrameLayout implements onEdgeChanged(); onPositionChanged(); + if (mFeaturesChangeListener != null) { + mFeaturesChangeListener.onChange(newTargetFeatures); + } mMenuAnimationController.fadeOutIfEnabled(); } @@ -302,4 +313,17 @@ class MenuView extends FrameLayout implements final ViewGroup parentView = (ViewGroup) getParent(); parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); } + + /** + * Interface definition for the {@link AccessibilityTarget} list changes. + */ + interface OnTargetFeaturesChangeListener { + /** + * Called when the list of accessibility target features was updated. This will be + * invoked when the end of {@code onTargetFeaturesChanged}. + * + * @param newTargetFeatures the list related to the current accessibility features. + */ + void onChange(List newTargetFeatures); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 26bf5e354164c..3e620a26f42c5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility.floatingmenu; +import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; + import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; @@ -23,38 +25,57 @@ import android.content.res.Configuration; import android.os.Handler; import android.os.Looper; import android.provider.Settings; +import android.util.PluralsMessageFormatter; import android.view.MotionEvent; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.systemui.R; import com.android.wm.shell.bubbles.DismissView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** - * The basic interactions with the child view {@link MenuView}. + * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and + * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the + * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu + * message view would be shown and allowed users to undo it. */ @SuppressLint("ViewConstructor") class MenuViewLayer extends FrameLayout { + private static final int SHOW_MESSAGE_DELAY_MS = 3000; + private final MenuView mMenuView; + private final MenuMessageView mMessageView; private final DismissView mDismissView; + private final MenuAnimationController mMenuAnimationController; + private final AccessibilityManager mAccessibilityManager; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final IAccessibilityFloatingMenu mFloatingMenu; private final DismissAnimationController mDismissAnimationController; @IntDef({ LayerIndex.MENU_VIEW, - LayerIndex.DISMISS_VIEW + LayerIndex.DISMISS_VIEW, + LayerIndex.MESSAGE_VIEW, }) @Retention(RetentionPolicy.SOURCE) @interface LayerIndex { int MENU_VIEW = 0; int DISMISS_VIEW = 1; + int MESSAGE_VIEW = 2; } @VisibleForTesting @@ -68,17 +89,17 @@ class MenuViewLayer extends FrameLayout { }; MenuViewLayer(@NonNull Context context, WindowManager windowManager, - IAccessibilityFloatingMenu floatingMenu) { + AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) { super(context); + mAccessibilityManager = accessibilityManager; mFloatingMenu = floatingMenu; final MenuViewModel menuViewModel = new MenuViewModel(context); final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); - final MenuAnimationController menuAnimationController = - mMenuView.getMenuAnimationController(); + mMenuAnimationController = mMenuView.getMenuAnimationController(); mDismissView = new DismissView(context); mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); @@ -96,23 +117,36 @@ class MenuViewLayer extends FrameLayout { @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - menuAnimationController.startShrinkAnimation(() -> { - mMenuView.hide(); - - mHandler.post(mDismissMenuAction); - }); - + hideMenuAndShowMessage(); mDismissView.hide(); mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); } }); final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler( - menuAnimationController, mDismissAnimationController); + mMenuAnimationController, mDismissAnimationController); mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler); + mMessageView = new MenuMessageView(context); + + mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> { + if (newTargetFeatures.size() < 1) { + return; + } + + // During the undo action period, the pending action will be canceled and undo back + // to the previous state if users did any action related to the accessibility features. + if (mMessageView.getVisibility() == VISIBLE) { + undo(); + } + + final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW); + messageText.setText(getMessageText(newTargetFeatures)); + }); + addView(mMenuView, LayerIndex.MENU_VIEW); addView(mDismissView, LayerIndex.DISMISS_VIEW); + addView(mMessageView, LayerIndex.MESSAGE_VIEW); } @Override @@ -121,6 +155,17 @@ class MenuViewLayer extends FrameLayout { mDismissView.updateResources(); } + private String getMessageText(List newTargetFeatures) { + Preconditions.checkArgument(newTargetFeatures.size() > 0, + "The list should at least have one feature."); + + final Map arguments = new HashMap<>(); + arguments.put("count", newTargetFeatures.size()); + arguments.put("label", newTargetFeatures.get(0).getLabel()); + return PluralsMessageFormatter.format(getResources(), arguments, + R.string.accessibility_floating_button_undo_message_text); + } + @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) { @@ -135,6 +180,7 @@ class MenuViewLayer extends FrameLayout { super.onAttachedToWindow(); mMenuView.show(); + mMessageView.setUndoListener(view -> undo()); mContext.registerComponentCallbacks(mDismissAnimationController); } @@ -146,4 +192,23 @@ class MenuViewLayer extends FrameLayout { mHandler.removeCallbacksAndMessages(/* token= */ null); mContext.unregisterComponentCallbacks(mDismissAnimationController); } + + private void hideMenuAndShowMessage() { + final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( + SHOW_MESSAGE_DELAY_MS, + AccessibilityManager.FLAG_CONTENT_TEXT + | AccessibilityManager.FLAG_CONTENT_CONTROLS); + mHandler.postDelayed(mDismissMenuAction, delayTime); + mMessageView.setVisibility(VISIBLE); + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); + } + + private void undo() { + mHandler.removeCallbacksAndMessages(/* token= */ null); + mMessageView.setVisibility(GONE); + mMenuView.onEdgeChanged(); + mMenuView.onPositionChanged(); + mMenuView.setVisibility(VISIBLE); + mMenuAnimationController.startGrowAnimation(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index c185546e34fa9..b1a64eda46ff7 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; /** * Controls the {@link MenuViewLayer} whether to be attached to the window via the interface @@ -32,9 +33,10 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { private final MenuViewLayer mMenuViewLayer; private boolean mIsShowing; - MenuViewLayerController(Context context, WindowManager windowManager) { + MenuViewLayerController(Context context, WindowManager windowManager, + AccessibilityManager accessibilityManager) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager, this); + mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java index 19a6c66652ddd..77d38c58e6857 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java @@ -35,6 +35,7 @@ import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -68,6 +69,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); private Context mContextWrapper; + private AccessibilityManager mAccessibilityManager; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private AccessibilityFloatingMenuController mController; private AccessibilityButtonTargetsObserver mTargetsObserver; @@ -87,6 +89,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { } }; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(), Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(), @@ -348,8 +351,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); final AccessibilityFloatingMenuController controller = new AccessibilityFloatingMenuController(mContextWrapper, windowManager, - displayManager, mTargetsObserver, mModeObserver, mKeyguardUpdateMonitor, - featureFlags); + displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver, + mKeyguardUpdateMonitor, featureFlags); controller.init(); return controller; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 31a7361b73388..d0bd4f7026ebc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -78,4 +78,13 @@ public class MenuAnimationControllerTest extends SysuiTestCase { verify(mViewPropertyAnimator).withEndAction(any(Runnable.class)); } + + @Test + public void startGrowAnimation_menuCompletelyOpaque() { + mMenuAnimationController.startShrinkAnimation(null); + + mMenuAnimationController.startGrowAnimation(); + + assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index 8c8d6aca7cd7f..dd7ce0e06c326 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java @@ -34,6 +34,7 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -58,6 +59,9 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { @Mock private WindowManager mWindowManager; + @Mock + private AccessibilityManager mAccessibilityManager; + @Mock private WindowMetrics mWindowMetrics; @@ -72,7 +76,8 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340)); when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets()); - mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager); + mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager, + mAccessibilityManager); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 8162035878b82..d20eeafde09c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -29,6 +29,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -58,7 +59,10 @@ public class MenuViewLayerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mFloatingMenu); + final AccessibilityManager stubAccessibilityManager = mContext.getSystemService( + AccessibilityManager.class); + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, stubAccessibilityManager, + mFloatingMenu); } @Test