Refactor the design and improve the animations of Accessibility Floating Menu(8/n).
Actions of this change: 1) Support the accessibility actions for menu view Bug: 227715451 Test: atest MenuItemAccessibilityDelegateTest Change-Id: Ib4a67fd2cee4ba21bcd7c557ca8dc53991a2d2be
This commit is contained in:
@@ -99,6 +99,30 @@ class MenuAnimationController {
|
||||
}
|
||||
}
|
||||
|
||||
void moveToTopLeftPosition() {
|
||||
mIsMovedToEdge = false;
|
||||
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
|
||||
moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top));
|
||||
}
|
||||
|
||||
void moveToTopRightPosition() {
|
||||
mIsMovedToEdge = false;
|
||||
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
|
||||
moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top));
|
||||
}
|
||||
|
||||
void moveToBottomLeftPosition() {
|
||||
mIsMovedToEdge = false;
|
||||
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
|
||||
moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom));
|
||||
}
|
||||
|
||||
void moveToBottomRightPosition() {
|
||||
mIsMovedToEdge = false;
|
||||
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
|
||||
moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom));
|
||||
}
|
||||
|
||||
void moveAndPersistPosition(PointF position) {
|
||||
moveToPosition(position);
|
||||
mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
|
||||
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* An accessibility item delegate for the individual items of the list view in the
|
||||
* {@link MenuView}.
|
||||
*/
|
||||
class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate {
|
||||
private final MenuAnimationController mAnimationController;
|
||||
|
||||
MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
|
||||
MenuAnimationController animationController) {
|
||||
super(recyclerViewDelegate);
|
||||
mAnimationController = animationController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
|
||||
final Resources res = host.getResources();
|
||||
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
|
||||
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
|
||||
res.getString(
|
||||
R.string.accessibility_floating_button_action_move_top_left));
|
||||
info.addAction(moveTopLeft);
|
||||
|
||||
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
|
||||
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
|
||||
R.id.action_move_top_right,
|
||||
res.getString(
|
||||
R.string.accessibility_floating_button_action_move_top_right));
|
||||
info.addAction(moveTopRight);
|
||||
|
||||
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
|
||||
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
|
||||
R.id.action_move_bottom_left,
|
||||
res.getString(
|
||||
R.string.accessibility_floating_button_action_move_bottom_left));
|
||||
info.addAction(moveBottomLeft);
|
||||
|
||||
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
|
||||
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
|
||||
R.id.action_move_bottom_right,
|
||||
res.getString(
|
||||
R.string.accessibility_floating_button_action_move_bottom_right));
|
||||
info.addAction(moveBottomRight);
|
||||
|
||||
final int moveEdgeId = mAnimationController.isMovedToEdge()
|
||||
? R.id.action_move_out_edge_and_show
|
||||
: R.id.action_move_to_edge_and_hide;
|
||||
final int moveEdgeTextResId = mAnimationController.isMovedToEdge()
|
||||
? R.string.accessibility_floating_button_action_move_out_edge_and_show
|
||||
: R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half;
|
||||
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
|
||||
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
|
||||
res.getString(moveEdgeTextResId));
|
||||
info.addAction(moveToOrOutEdge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (action == ACTION_ACCESSIBILITY_FOCUS) {
|
||||
mAnimationController.fadeInNowIfEnabled();
|
||||
}
|
||||
|
||||
if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
|
||||
mAnimationController.fadeOutIfEnabled();
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_top_left) {
|
||||
mAnimationController.moveToTopLeftPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_top_right) {
|
||||
mAnimationController.moveToTopRightPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_bottom_left) {
|
||||
mAnimationController.moveToBottomLeftPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_bottom_right) {
|
||||
mAnimationController.moveToBottomRightPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_to_edge_and_hide) {
|
||||
mAnimationController.moveToEdgeAndHide();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == R.id.action_move_out_edge_and_show) {
|
||||
mAnimationController.moveOutEdgeAndShow();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.performAccessibilityAction(host, action, args);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,12 @@ import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
|
||||
@@ -72,10 +75,15 @@ class MenuView extends FrameLayout implements
|
||||
mTargetFeaturesView = new RecyclerView(context);
|
||||
mTargetFeaturesView.setAdapter(mAdapter);
|
||||
mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context));
|
||||
final MenuListViewTouchHandler menuListViewTouchHandler =
|
||||
new MenuListViewTouchHandler(mMenuAnimationController);
|
||||
addOnItemTouchListenerToList(menuListViewTouchHandler);
|
||||
|
||||
mTargetFeaturesView.setAccessibilityDelegateCompat(
|
||||
new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) {
|
||||
@NonNull
|
||||
@Override
|
||||
public AccessibilityDelegateCompat getItemDelegate() {
|
||||
return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this,
|
||||
mMenuAnimationController);
|
||||
}
|
||||
});
|
||||
setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
|
||||
// Avoid drawing out of bounds of the parent view
|
||||
setClipToOutline(true);
|
||||
@@ -270,6 +278,7 @@ class MenuView extends FrameLayout implements
|
||||
void loadLayoutResources() {
|
||||
mMenuViewAppearance.update();
|
||||
|
||||
mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription());
|
||||
setBackground(mMenuViewAppearance.getMenuBackground());
|
||||
setElevation(mMenuViewAppearance.getMenuElevation());
|
||||
onItemSizeChanged();
|
||||
|
||||
@@ -64,6 +64,7 @@ class MenuViewAppearance {
|
||||
private int mElevation;
|
||||
private float[] mRadii;
|
||||
private Drawable mBackgroundDrawable;
|
||||
private String mContentDescription;
|
||||
|
||||
@IntDef({
|
||||
SMALL,
|
||||
@@ -108,6 +109,8 @@ class MenuViewAppearance {
|
||||
final Drawable drawable =
|
||||
mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
|
||||
mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
|
||||
mContentDescription = mRes.getString(
|
||||
com.android.internal.R.string.accessibility_select_shortcut_menu_title);
|
||||
}
|
||||
|
||||
void setSizeType(int sizeType) {
|
||||
@@ -157,6 +160,10 @@ class MenuViewAppearance {
|
||||
+ draggableBounds.height() * mPercentagePosition.getPercentageY());
|
||||
}
|
||||
|
||||
String getContentDescription() {
|
||||
return mContentDescription;
|
||||
}
|
||||
|
||||
Drawable getMenuBackground() {
|
||||
return mBackgroundDrawable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
|
||||
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
|
||||
|
||||
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 android.graphics.Rect;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.systemui.R;
|
||||
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.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Tests for {@link MenuItemAccessibilityDelegate}. */
|
||||
@SmallTest
|
||||
@TestableLooper.RunWithLooper
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
|
||||
@Rule
|
||||
public MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
||||
private RecyclerView mStubListView;
|
||||
private MenuView mMenuView;
|
||||
private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
|
||||
private MenuAnimationController mMenuAnimationController;
|
||||
private final Rect mDraggableBounds = new Rect(100, 200, 300, 400);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
|
||||
stubWindowManager);
|
||||
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
|
||||
|
||||
final int halfScreenHeight =
|
||||
stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2;
|
||||
mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
|
||||
mMenuView.setTranslationY(halfScreenHeight);
|
||||
|
||||
doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds();
|
||||
mStubListView = new RecyclerView(mContext);
|
||||
mMenuAnimationController = spy(new MenuAnimationController(mMenuView));
|
||||
mMenuItemAccessibilityDelegate =
|
||||
new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate(
|
||||
mStubListView), mMenuAnimationController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAccessibilityActionList_matchSize() {
|
||||
final AccessibilityNodeInfoCompat info =
|
||||
new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
|
||||
|
||||
mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
|
||||
|
||||
assertThat(info.getActionList().size()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveTopLeftAction_matchPosition() {
|
||||
final boolean moveTopLeftAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_top_left,
|
||||
null);
|
||||
|
||||
assertThat(moveTopLeftAction).isTrue();
|
||||
assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
|
||||
assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveTopRightAction_matchPosition() {
|
||||
final boolean moveTopRightAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_top_right, null);
|
||||
|
||||
assertThat(moveTopRightAction).isTrue();
|
||||
assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
|
||||
assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveBottomLeftAction_matchPosition() {
|
||||
final boolean moveBottomLeftAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_bottom_left, null);
|
||||
|
||||
assertThat(moveBottomLeftAction).isTrue();
|
||||
assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
|
||||
assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveBottomRightAction_matchPosition() {
|
||||
final boolean moveBottomRightAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_bottom_right, null);
|
||||
|
||||
assertThat(moveBottomRightAction).isTrue();
|
||||
assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
|
||||
assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveToEdgeAndHideAction_success() {
|
||||
final boolean moveToEdgeAndHideAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_to_edge_and_hide, null);
|
||||
|
||||
assertThat(moveToEdgeAndHideAction).isTrue();
|
||||
verify(mMenuAnimationController).moveToEdgeAndHide();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performMoveOutFromEdgeAction_success() {
|
||||
final boolean moveOutEdgeAndShowAction =
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
R.id.action_move_out_edge_and_show, null);
|
||||
|
||||
assertThat(moveOutEdgeAndShowAction).isTrue();
|
||||
verify(mMenuAnimationController).moveOutEdgeAndShow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performFocusAction_fadeIn() {
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
ACTION_ACCESSIBILITY_FOCUS, null);
|
||||
|
||||
verify(mMenuAnimationController).fadeInNowIfEnabled();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void performClearFocusAction_fadeOut() {
|
||||
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
|
||||
ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
|
||||
|
||||
verify(mMenuAnimationController).fadeOutIfEnabled();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user