From cd10a1bc49d10401135b27570bda0d240dda48de Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Thu, 10 Dec 2020 01:00:37 +0800 Subject: [PATCH 1/5] Add new floating action menu for Accessibility targets. (1/n) Goals: - Trigger actions of accessibility features would be grouped into the floating menu. Patch action: 1. Use current Accessibility Target functions to get the corresponding data. 2. The position of the floating menu is at the bottom-right corner of the screen initially. 3. Support dark theme. Bug: 173958541 Test: atest AccessibilityFloatingMenuTest AccessibilityFloatingMenuViewTest Change-Id: I28e729520d4eb8ecb0bcc41d4de9d1499380e7f5 Merged-In: I28e729520d4eb8ecb0bcc41d4de9d1499380e7f5 (cherry picked from commit 2be49c9be939f25c7d4e7b117340a67228fca1e6) --- ...accessibility_floating_menu_background.xml | 32 +++ .../accessibility_floating_menu_item.xml | 36 ++++ packages/SystemUI/res/values-night/colors.xml | 2 + packages/SystemUI/res/values/colors.xml | 4 + packages/SystemUI/res/values/dimens.xml | 10 + .../AccessibilityFloatingMenu.java | 88 ++++++++ .../AccessibilityFloatingMenuView.java | 195 ++++++++++++++++++ .../AccessibilityTargetAdapter.java | 82 ++++++++ .../IAccessibilityFloatingMenu.java | 38 ++++ .../AccessibilityFloatingMenuTest.java | 90 ++++++++ .../AccessibilityFloatingMenuViewTest.java | 134 ++++++++++++ 11 files changed, 711 insertions(+) create mode 100644 packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml create mode 100644 packages/SystemUI/res/layout/accessibility_floating_menu_item.xml create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IAccessibilityFloatingMenu.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java diff --git a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml new file mode 100644 index 0000000000000..675d36f83093d --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml b/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml new file mode 100644 index 0000000000000..376b29d2f369f --- /dev/null +++ b/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 37ec576be4be3..d571f2f174978 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -102,4 +102,6 @@ #81C995 #FCAD70 + + #B3000000 diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 55365bd3c6924..0076c51326738 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -274,4 +274,8 @@ #1E8E3E #E8710A + + + #CCFFFFFF + #26FFFFFF diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6e270a75a6b90..033e606d77d84 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1356,6 +1356,16 @@ 20dp 6dp + + 5dp + 1dp + -2dp + 16dp + 6dp + 36dp + 25dp + 20dp + 44dp 22dp diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java new file mode 100644 index 0000000000000..f5356c1e48eb6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2021 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.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; + +import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Contains logic for an accessibility floating menu view. + */ +public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { + private final Context mContext; + private final AccessibilityFloatingMenuView mMenuView; + + private final ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); + } + }; + + public AccessibilityFloatingMenu(Context context) { + mContext = context; + mMenuView = new AccessibilityFloatingMenuView(context); + } + + @VisibleForTesting + AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) { + mContext = context; + mMenuView = menuView; + } + + @Override + public boolean isShowing() { + return mMenuView.isShowing(); + } + + @Override + public void show() { + if (isShowing()) { + return; + } + + mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); + mMenuView.show(); + + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), + /* notifyForDescendants */ false, mContentObserver, + UserHandle.USER_CURRENT); + } + + @Override + public void hide() { + if (!isShowing()) { + return; + } + + mMenuView.hide(); + + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java new file mode 100644 index 0000000000000..b393a892e9b77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2021 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.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.DimenRes; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Accessibility floating menu is used for the actions of accessibility features, it's also the + * action set. + * + *

The number of items would depend on strings key + * {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}. + */ +public class AccessibilityFloatingMenuView extends FrameLayout { + private static final float DEFAULT_LOCATION_Y_PERCENTAGE = 0.8f; + private static final int INDEX_MENU_ITEM = 0; + private boolean mIsShowing; + private RecyclerView mListView; + private final WindowManager.LayoutParams mLayoutParams; + private final WindowManager mWindowManager; + private final AccessibilityTargetAdapter mAdapter; + private final List mTargets = new ArrayList<>(); + + public AccessibilityFloatingMenuView(Context context) { + super(context); + + mWindowManager = context.getSystemService(WindowManager.class); + mLayoutParams = createDefaultLayoutParams(); + mAdapter = new AccessibilityTargetAdapter(mTargets); + + initListView(context, mAdapter); + updateStroke(); + } + + void show() { + if (isShowing()) { + return; + } + + mIsShowing = true; + mWindowManager.addView(this, mLayoutParams); + } + + void hide() { + if (!isShowing()) { + return; + } + + mIsShowing = false; + mWindowManager.removeView(this); + } + + boolean isShowing() { + return mIsShowing; + } + + void onTargetsChanged(List newTargets) { + mTargets.clear(); + mTargets.addAll(newTargets); + mAdapter.notifyDataSetChanged(); + + final boolean hasMoreItems = mAdapter.getItemCount() > 1; + final int resId = hasMoreItems + ? R.dimen.accessibility_floating_menu_multiple_radius + : R.dimen.accessibility_floating_menu_single_radius; + setRadius(resId); + } + + private void setRadius(@DimenRes int radiusResId) { + final float radius = getResources().getDimension(radiusResId); + final float[] radii = new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + getMenuGradientDrawable().setCornerRadii(radii); + } + + private void initListView(Context context, AccessibilityTargetAdapter adapter) { + final Resources res = context.getResources(); + final int margin = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); + final int elevation = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); + final Drawable listViewBackground = + context.getDrawable(R.drawable.accessibility_floating_menu_background); + + mListView = new RecyclerView(context); + final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + final LayoutParams layoutParams = + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.setMarginsRelative(margin, margin, /* end= */ 0, margin); + mListView.setLayoutParams(layoutParams); + mListView.setElevation(elevation); + mListView.setBackground(listViewBackground); + mListView.setAdapter(adapter); + mListView.setLayoutManager(layoutManager); + + addView(mListView); + } + + private WindowManager.LayoutParams createDefaultLayoutParams() { + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + final DisplayMetrics dm = getResources().getDisplayMetrics(); + params.gravity = Gravity.START | Gravity.TOP; + params.x = dm.widthPixels; + params.y = (int) (dm.heightPixels * DEFAULT_LOCATION_Y_PERCENTAGE); + + return params; + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateLocation(); + updateColor(); + updateStroke(); + } + + private LayerDrawable getMenuLayerDrawable() { + return (LayerDrawable) mListView.getBackground(); + } + + private GradientDrawable getMenuGradientDrawable() { + return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM); + } + + private void updateLocation() { + final DisplayMetrics dm = getResources().getDisplayMetrics(); + mLayoutParams.x = dm.widthPixels; + mLayoutParams.y = (int) (dm.heightPixels * DEFAULT_LOCATION_Y_PERCENTAGE); + mWindowManager.updateViewLayout(this, mLayoutParams); + } + + private void updateColor() { + final int menuColorResId = R.color.accessibility_floating_menu_background; + getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId)); + } + + private void updateStroke() { + final Resources res = getResources(); + final boolean isNightMode = + (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + + final int inset = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); + final int insetRight = isNightMode ? inset : 0; + getMenuLayerDrawable().setLayerInset(INDEX_MENU_ITEM, 0, 0, insetRight, 0); + + final int width = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width); + final int strokeWidth = isNightMode ? width : 0; + final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark); + getMenuGradientDrawable().setStroke(strokeWidth, strokeColor); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java new file mode 100644 index 0000000000000..8b586c54034d8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 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.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.Adapter; + +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.R; +import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder; + +import java.util.List; + +/** + * An adapter which shows the set of accessibility targets that can be performed. + */ +public class AccessibilityTargetAdapter extends Adapter { + + private final List mTargets; + public AccessibilityTargetAdapter(List targets) { + mTargets = targets; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) { + final View root = LayoutInflater.from(parent.getContext()).inflate( + R.layout.accessibility_floating_menu_item, parent, + /* attachToRoot= */ false); + + return new ViewHolder(root); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.mIconView.setBackground(mTargets.get(position).getIcon()); + + final boolean isFirstItem = (position == 0); + final boolean isLastItem = (position == (getItemCount() - 1)); + final int padding = holder.itemView.getPaddingStart(); + final int paddingTop = isFirstItem ? padding : 0; + final int paddingBottom = isLastItem ? padding : 0; + holder.itemView.setPaddingRelative(padding, paddingTop, padding, paddingBottom); + holder.itemView.setOnClickListener((v) -> mTargets.get(position).onSelected()); + holder.mDivider.setVisibility(isLastItem ? View.GONE : View.VISIBLE); + } + + @Override + public int getItemCount() { + return mTargets.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final View mIconView; + final View mDivider; + + ViewHolder(View itemView) { + super(itemView); + mIconView = itemView.findViewById(R.id.icon_view); + mDivider = itemView.findViewById(R.id.transparent_divider); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IAccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IAccessibilityFloatingMenu.java new file mode 100644 index 0000000000000..62f02a0444855 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IAccessibilityFloatingMenu.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 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; + +/** + * Interface for managing the accessibility targets menu component. + */ +public interface IAccessibilityFloatingMenu { + + /** + * Checks if the menu was shown. + */ + boolean isShowing(); + + /** + * Shows the accessibility targets menu. + */ + void show(); + + /** + * Hides the accessibility targets menu. + */ + void hide(); +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java new file mode 100644 index 0000000000000..0489871578cfe --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 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 com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; + +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.accessibility.AccessibilityManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link AccessibilityFloatingMenu}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class AccessibilityFloatingMenuTest extends SysuiTestCase { + + @Mock + private AccessibilityManager mAccessibilityManager; + + private AccessibilityFloatingMenuView mMenuView; + private AccessibilityFloatingMenu mMenu; + + @Before + public void initMenu() { + MockitoAnnotations.initMocks(this); + + final List assignedTargets = new ArrayList<>(); + mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); + assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME); + doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets( + anyInt()); + + mMenuView = new AccessibilityFloatingMenuView(mContext); + mMenu = new AccessibilityFloatingMenu(mContext, mMenuView); + } + + @Test + public void showMenuView_success() { + mMenu.show(); + + assertThat(mMenuView.isShowing()).isTrue(); + } + + @Test + public void hideMenuView_success() { + mMenu.show(); + mMenu.hide(); + + assertThat(mMenuView.isShowing()).isFalse(); + } + + @After + public void tearDown() { + mMenu.hide(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java new file mode 100644 index 0000000000000..583a68bcbbacf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link AccessibilityFloatingMenuView}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { + private AccessibilityFloatingMenuView mMenuView; + + @Mock + private WindowManager mWindowManager; + + private final List mTargets = new ArrayList<>(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + final WindowManager wm = mContext.getSystemService(WindowManager.class); + doAnswer(invocation -> wm.getMaximumWindowMetrics()).when( + mWindowManager).getMaximumWindowMetrics(); + mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + + mTargets.add(mock(AccessibilityTarget.class)); + mMenuView = new AccessibilityFloatingMenuView(mContext); + } + + @Test + public void initListView_success() { + assertThat(mMenuView.getChildCount()).isEqualTo(1); + } + + @Test + public void showMenuView_success() { + mMenuView.show(); + + assertThat(mMenuView.isShowing()).isTrue(); + verify(mWindowManager).addView(eq(mMenuView), any(WindowManager.LayoutParams.class)); + } + + @Test + public void showMenuView_showTwice_addViewOnce() { + mMenuView.show(); + mMenuView.show(); + + assertThat(mMenuView.isShowing()).isTrue(); + verify(mWindowManager, times(1)).addView(eq(mMenuView), + any(WindowManager.LayoutParams.class)); + } + + @Test + public void hideMenuView_success() { + mMenuView.show(); + mMenuView.hide(); + + assertThat(mMenuView.isShowing()).isFalse(); + verify(mWindowManager).removeView(eq(mMenuView)); + } + + @Test + public void hideMenuView_hideTwice_removeViewOnce() { + mMenuView.show(); + mMenuView.hide(); + mMenuView.hide(); + + assertThat(mMenuView.isShowing()).isFalse(); + verify(mWindowManager, times(1)).removeView(eq(mMenuView)); + } + + @Test + public void updateListViewRadius_singleTarget_matchResult() { + final float radius = + getContext().getResources().getDimension( + R.dimen.accessibility_floating_menu_single_radius); + final float[] expectedRadii = + new float[] {radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + + mMenuView.onTargetsChanged(mTargets); + final View view = mMenuView.getChildAt(0); + final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground(); + final GradientDrawable gradientDrawable = + (GradientDrawable) layerDrawable.getDrawable(0); + final float[] actualRadii = gradientDrawable.getCornerRadii(); + + assertThat(actualRadii).isEqualTo(expectedRadii); + } +} From 1b38ff82bcec4d8dc7ba7159f9ed76762f53aa18 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Tue, 19 Jan 2021 14:39:01 +0800 Subject: [PATCH 2/5] Add new floating action menu for Accessibility targets. (2/n) Patch action: 1. Update the style depends on the settings icon and size key. 2. Under half circle shape mode, the menu will enlarge touch area. Cherry picked from commit c26ac8c638f5cf7235d9fb18b374479d93fa99e3 Bug: 173958541 Test: atest AccessibilityFloatingMenuTest AccessibilityFloatingMenuViewTest AccessibilityTargetAdapterTest Change-Id: Iec53ef71d7e0abe5c7619d4e5f67862ca56dfd1f Merged-In: Iec53ef71d7e0abe5c7619d4e5f67862ca56dfd1f --- ...accessibility_floating_menu_background.xml | 4 +- .../accessibility_floating_menu_item.xml | 7 +- packages/SystemUI/res/values/dimens.xml | 9 +- .../AccessibilityFloatingMenu.java | 61 +++++- .../AccessibilityFloatingMenuView.java | 204 +++++++++++++++--- .../AccessibilityTargetAdapter.java | 88 +++++++- .../AccessibilityFloatingMenuTest.java | 5 + .../AccessibilityFloatingMenuViewTest.java | 54 ++++- .../AccessibilityTargetAdapterTest.java | 85 ++++++++ 9 files changed, 457 insertions(+), 60 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java diff --git a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml index 675d36f83093d..ef67e512d95d4 100644 --- a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml +++ b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml @@ -21,9 +21,9 @@ diff --git a/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml b/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml index 376b29d2f369f..f7357b27af5f0 100644 --- a/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml +++ b/packages/SystemUI/res/layout/accessibility_floating_menu_item.xml @@ -19,14 +19,15 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/accessibility_floating_menu_padding" + android:paddingStart="@dimen/accessibility_floating_menu_padding" + android:paddingEnd="@dimen/accessibility_floating_menu_padding" android:orientation="vertical" android:gravity="center"> + android:layout_width="@dimen/accessibility_floating_menu_small_width_height" + android:layout_height="@dimen/accessibility_floating_menu_small_width_height"/> -2dp 16dp 6dp - 36dp - 25dp - 20dp + 36dp + 25dp + 20dp + 56dp + 33dp + 35dp 44dp diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java index f5356c1e48eb6..b25a76a59ba82 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -16,9 +16,13 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; +import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType; +import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType; import android.content.Context; import android.database.ContentObserver; @@ -44,6 +48,22 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { } }; + private final ContentObserver mSizeContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + mMenuView.setSizeType(getSizeType(mContext)); + } + }; + + private final ContentObserver mShapeContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + mMenuView.setShapeType(getShapeType(mContext)); + } + }; + public AccessibilityFloatingMenu(Context context) { mContext = context; mMenuView = new AccessibilityFloatingMenuView(context); @@ -66,13 +86,12 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { return; } - mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); mMenuView.show(); + mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); + mMenuView.setSizeType(getSizeType(mContext)); + mMenuView.setShapeType(getShapeType(mContext)); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), - /* notifyForDescendants */ false, mContentObserver, - UserHandle.USER_CURRENT); + registerContentObservers(); } @Override @@ -83,6 +102,38 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { mMenuView.hide(); + unregisterContentObservers(); + } + + private static int getSizeType(Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL); + } + + private static int getShapeType(Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE, + ShapeType.CIRCLE); + } + + private void registerContentObservers() { + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), + /* notifyForDescendants */ false, mContentObserver, + UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE), + /* notifyForDescendants */ false, mSizeContentObserver, + UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE), + /* notifyForDescendants */ false, mShapeContentObserver, + UserHandle.USER_CURRENT); + } + + private void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mContentObserver); + mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver); + mContext.getContentResolver().unregisterContentObserver(mShapeContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index b393a892e9b77..9f147ee936172 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -16,15 +16,18 @@ package com.android.systemui.accessibility.floatingmenu; +import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.util.DisplayMetrics; import android.view.Gravity; +import android.view.MotionEvent; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; @@ -34,8 +37,11 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -50,21 +56,59 @@ public class AccessibilityFloatingMenuView extends FrameLayout { private static final float DEFAULT_LOCATION_Y_PERCENTAGE = 0.8f; private static final int INDEX_MENU_ITEM = 0; private boolean mIsShowing; - private RecyclerView mListView; + @SizeType + private int mSizeType = SizeType.SMALL; + private int mMargin; + private int mPadding; + private int mScreenHeight; + private int mScreenWidth; + private int mIconWidth; + private int mIconHeight; + private final RecyclerView mListView; + private final AccessibilityTargetAdapter mAdapter; private final WindowManager.LayoutParams mLayoutParams; private final WindowManager mWindowManager; - private final AccessibilityTargetAdapter mAdapter; private final List mTargets = new ArrayList<>(); + @IntDef({ + SizeType.SMALL, + SizeType.LARGE + }) + @Retention(RetentionPolicy.SOURCE) + @interface SizeType { + int SMALL = 0; + int LARGE = 1; + } + + @IntDef({ + ShapeType.CIRCLE, + ShapeType.HALF_CIRCLE + }) + @Retention(RetentionPolicy.SOURCE) + @interface ShapeType { + int CIRCLE = 0; + int HALF_CIRCLE = 1; + } + public AccessibilityFloatingMenuView(Context context) { + this(context, new RecyclerView(context)); + } + + @VisibleForTesting + AccessibilityFloatingMenuView(Context context, + RecyclerView listView) { super(context); + mListView = listView; mWindowManager = context.getSystemService(WindowManager.class); mLayoutParams = createDefaultLayoutParams(); mAdapter = new AccessibilityTargetAdapter(mTargets); - initListView(context, mAdapter); - updateStroke(); + updateDimensions(); + initListView(); + + final int uiMode = context.getResources().getConfiguration().uiMode; + updateStrokeWith(uiMode); } void show() { @@ -94,41 +138,100 @@ public class AccessibilityFloatingMenuView extends FrameLayout { mTargets.addAll(newTargets); mAdapter.notifyDataSetChanged(); - final boolean hasMoreItems = mAdapter.getItemCount() > 1; - final int resId = hasMoreItems - ? R.dimen.accessibility_floating_menu_multiple_radius - : R.dimen.accessibility_floating_menu_single_radius; - setRadius(resId); + updateRadiusWith(mSizeType, mTargets.size()); } - private void setRadius(@DimenRes int radiusResId) { - final float radius = getResources().getDimension(radiusResId); + void setSizeType(@SizeType int newSizeType) { + mSizeType = newSizeType; + + updateIconSizeWith(newSizeType); + updateRadiusWith(newSizeType, mTargets.size()); + + // When the icon sized changed, the menu size and location will be impacted. + updateLocation(); + } + + void setShapeType(@ShapeType int newShapeType) { + final boolean isCircleShape = + newShapeType == ShapeType.CIRCLE; + final float offset = + isCircleShape + ? 0 + : getLayoutWidth() / 2.0f; + mListView.animate().translationX(offset); + + setOnTouchListener( + isCircleShape + ? null + : (view, event) -> onTouched(event)); + } + + private boolean onTouched(MotionEvent event) { + final int currentX = (int) event.getX(); + final int currentY = (int) event.getY(); + + final int menuHalfWidth = getLayoutWidth() / 2; + final Rect touchDelegateBounds = + new Rect(mMargin, mMargin, mMargin + menuHalfWidth, mMargin + getLayoutHeight()); + if (touchDelegateBounds.contains(currentX, currentY)) { + // In order to correspond to the correct item of list view. + event.setLocation(currentX - mMargin, currentY - mMargin); + return mListView.dispatchTouchEvent(event); + } + + return false; + } + + private void setRadius(float radius) { final float[] radii = new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; getMenuGradientDrawable().setCornerRadii(radii); } - private void initListView(Context context, AccessibilityTargetAdapter adapter) { - final Resources res = context.getResources(); - final int margin = + private void updateDimensions() { + final Resources res = getResources(); + final DisplayMetrics dm = res.getDisplayMetrics(); + mScreenWidth = dm.widthPixels; + mScreenHeight = dm.heightPixels; + mMargin = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); - final int elevation = - res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); - final Drawable listViewBackground = - context.getDrawable(R.drawable.accessibility_floating_menu_background); + mPadding = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding); + } - mListView = new RecyclerView(context); - final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + private void updateIconSizeWith(@SizeType int sizeType) { + final Resources res = getResources(); + final int iconResId = + sizeType == SizeType.SMALL + ? R.dimen.accessibility_floating_menu_small_width_height + : R.dimen.accessibility_floating_menu_large_width_height; + mIconWidth = res.getDimensionPixelSize(iconResId); + mIconHeight = mIconWidth; + + mAdapter.setIconWidthHeight(mIconWidth); + mAdapter.notifyDataSetChanged(); + } + + private void initListView() { + final Drawable listViewBackground = + getContext().getDrawable(R.drawable.accessibility_floating_menu_background); + final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + mListView.setBackground(listViewBackground); + mListView.setAdapter(mAdapter); + mListView.setLayoutManager(layoutManager); + updateListView(); + + addView(mListView); + } + + private void updateListView() { + final int elevation = + getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); final LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - layoutParams.setMarginsRelative(margin, margin, /* end= */ 0, margin); + layoutParams.setMarginsRelative(mMargin, mMargin, /* end= */ 0, mMargin); mListView.setLayoutParams(layoutParams); mListView.setElevation(elevation); - mListView.setBackground(listViewBackground); - mListView.setAdapter(adapter); - mListView.setLayoutManager(layoutManager); - - addView(mListView); } private WindowManager.LayoutParams createDefaultLayoutParams() { @@ -150,9 +253,12 @@ public class AccessibilityFloatingMenuView extends FrameLayout { protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - updateLocation(); + updateDimensions(); + updateListView(); + updateIconSizeWith(mSizeType); updateColor(); - updateStroke(); + updateStrokeWith(newConfig.uiMode); + updateLocation(); } private LayerDrawable getMenuLayerDrawable() { @@ -163,10 +269,12 @@ public class AccessibilityFloatingMenuView extends FrameLayout { return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM); } + /** + * Updates the floating menu to be fixed at the side of the screen. + */ private void updateLocation() { - final DisplayMetrics dm = getResources().getDisplayMetrics(); - mLayoutParams.x = dm.widthPixels; - mLayoutParams.y = (int) (dm.heightPixels * DEFAULT_LOCATION_Y_PERCENTAGE); + mLayoutParams.x = mScreenWidth - mMargin - getLayoutWidth(); + mLayoutParams.y = (int) (mScreenHeight * DEFAULT_LOCATION_Y_PERCENTAGE); mWindowManager.updateViewLayout(this, mLayoutParams); } @@ -175,10 +283,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout { getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId)); } - private void updateStroke() { + private void updateStrokeWith(int uiMode) { final Resources res = getResources(); final boolean isNightMode = - (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; final int inset = @@ -192,4 +300,34 @@ public class AccessibilityFloatingMenuView extends FrameLayout { final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark); getMenuGradientDrawable().setStroke(strokeWidth, strokeColor); } + + private void updateRadiusWith(@SizeType int sizeType, int itemCount) { + setRadius(getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount))); + } + + private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) { + return sizeType == SizeType.SMALL + ? getSmallSizeResIdWith(itemCount) + : getLargeSizeResIdWith(itemCount); + } + + private int getSmallSizeResIdWith(int itemCount) { + return itemCount > 1 + ? R.dimen.accessibility_floating_menu_small_multiple_radius + : R.dimen.accessibility_floating_menu_small_single_radius; + } + + private int getLargeSizeResIdWith(int itemCount) { + return itemCount > 1 + ? R.dimen.accessibility_floating_menu_large_multiple_radius + : R.dimen.accessibility_floating_menu_large_single_radius; + } + + private int getLayoutWidth() { + return mPadding * 2 + mIconWidth; + } + + private int getLayoutHeight() { + return (mPadding + mIconHeight) * mTargets.size() + mPadding; + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java index 8b586c54034d8..bb4038e92ff43 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java @@ -16,10 +16,17 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.view.View.GONE; + +import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.FIRST_ITEM; +import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.LAST_ITEM; +import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.REGULAR_ITEM; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; @@ -28,40 +35,70 @@ import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.R; import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** * An adapter which shows the set of accessibility targets that can be performed. */ public class AccessibilityTargetAdapter extends Adapter { - + private int mIconWidthHeight; private final List mTargets; + + @IntDef({ + FIRST_ITEM, + REGULAR_ITEM, + LAST_ITEM + }) + @Retention(RetentionPolicy.SOURCE) + @interface ItemType { + int FIRST_ITEM = 0; + int REGULAR_ITEM = 1; + int LAST_ITEM = 2; + } + public AccessibilityTargetAdapter(List targets) { mTargets = targets; } @NonNull @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) { + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @ItemType int itemType) { final View root = LayoutInflater.from(parent.getContext()).inflate( R.layout.accessibility_floating_menu_item, parent, /* attachToRoot= */ false); + if (itemType == FIRST_ITEM) { + return new TopViewHolder(root); + } + + if (itemType == LAST_ITEM) { + return new BottomViewHolder(root); + } + return new ViewHolder(root); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.mIconView.setBackground(mTargets.get(position).getIcon()); - - final boolean isFirstItem = (position == 0); - final boolean isLastItem = (position == (getItemCount() - 1)); - final int padding = holder.itemView.getPaddingStart(); - final int paddingTop = isFirstItem ? padding : 0; - final int paddingBottom = isLastItem ? padding : 0; - holder.itemView.setPaddingRelative(padding, paddingTop, padding, paddingBottom); + holder.updateIconWidthHeight(mIconWidthHeight); holder.itemView.setOnClickListener((v) -> mTargets.get(position).onSelected()); - holder.mDivider.setVisibility(isLastItem ? View.GONE : View.VISIBLE); + } + + @ItemType + @Override + public int getItemViewType(int position) { + if (position == 0) { + return FIRST_ITEM; + } + + if (position == (getItemCount() - 1)) { + return LAST_ITEM; + } + + return REGULAR_ITEM; } @Override @@ -69,6 +106,10 @@ public class AccessibilityTargetAdapter extends Adapter { return mTargets.size(); } + public void setIconWidthHeight(int iconWidthHeight) { + mIconWidthHeight = iconWidthHeight; + } + static class ViewHolder extends RecyclerView.ViewHolder { final View mIconView; final View mDivider; @@ -78,5 +119,32 @@ public class AccessibilityTargetAdapter extends Adapter { mIconView = itemView.findViewById(R.id.icon_view); mDivider = itemView.findViewById(R.id.transparent_divider); } + + void updateIconWidthHeight(int newValue) { + final ViewGroup.LayoutParams layoutParams = mIconView.getLayoutParams(); + if (layoutParams.width == newValue) { + return; + } + layoutParams.width = newValue; + layoutParams.height = newValue; + mIconView.setLayoutParams(layoutParams); + } + } + + static class TopViewHolder extends ViewHolder { + TopViewHolder(View itemView) { + super(itemView); + final int padding = itemView.getPaddingStart(); + itemView.setPaddingRelative(padding, padding, padding, 0); + } + } + + static class BottomViewHolder extends ViewHolder { + BottomViewHolder(View itemView) { + super(itemView); + mDivider.setVisibility(GONE); + final int padding = itemView.getPaddingStart(); + itemView.setPaddingRelative(padding, 0, padding, padding); + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java index 0489871578cfe..337d97e1dc0bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java @@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import android.content.Context; import android.testing.AndroidTestingRunner; @@ -30,6 +31,7 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; +import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.SysuiTestCase; import org.junit.After; @@ -58,6 +60,9 @@ public class AccessibilityFloatingMenuTest extends SysuiTestCase { public void initMenu() { MockitoAnnotations.initMocks(this); + final List mTargets = new ArrayList<>(); + mTargets.add(mock(AccessibilityTarget.class)); + final List assignedTargets = new ArrayList<>(); mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index 583a68bcbbacf..9ff697cc9957a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -16,23 +16,29 @@ package com.android.systemui.accessibility.floatingmenu; - import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.view.View; +import android.view.ViewPropertyAnimator; import android.view.WindowManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; @@ -51,12 +57,18 @@ import java.util.List; /** Tests for {@link AccessibilityFloatingMenuView}. */ @SmallTest @RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { private AccessibilityFloatingMenuView mMenuView; @Mock private WindowManager mWindowManager; + @Mock + private ViewPropertyAnimator mAnimator; + + private RecyclerView mListView; + private final List mTargets = new ArrayList<>(); @Before @@ -69,6 +81,7 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mTargets.add(mock(AccessibilityTarget.class)); + mListView = spy(new RecyclerView(mContext)); mMenuView = new AccessibilityFloatingMenuView(mContext); } @@ -117,10 +130,10 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { @Test public void updateListViewRadius_singleTarget_matchResult() { final float radius = - getContext().getResources().getDimension( - R.dimen.accessibility_floating_menu_single_radius); + getContext().getResources().getDimensionPixelSize( + R.dimen.accessibility_floating_menu_small_single_radius); final float[] expectedRadii = - new float[] {radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; mMenuView.onTargetsChanged(mTargets); final View view = mMenuView.getChildAt(0); @@ -131,4 +144,37 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { assertThat(actualRadii).isEqualTo(expectedRadii); } + + @Test + public void setSizeType_largeSize_matchResult() { + final int shapeType = 2; + final float radius = getContext().getResources().getDimensionPixelSize( + R.dimen.accessibility_floating_menu_large_single_radius); + final float[] expectedRadii = + new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + final Drawable listViewBackground = + mContext.getDrawable(R.drawable.accessibility_floating_menu_background); + mListView = spy(new RecyclerView(mContext)); + mListView.setBackground(listViewBackground); + + mMenuView = new AccessibilityFloatingMenuView(mContext, mListView); + mMenuView.setSizeType(shapeType); + final LayerDrawable layerDrawable = + (LayerDrawable) mListView.getBackground(); + final GradientDrawable gradientDrawable = + (GradientDrawable) layerDrawable.getDrawable(0); + + assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii); + } + + @Test + public void setShapeType_halfCircle_translationX() { + final int shapeType = 2; + doReturn(mAnimator).when(mListView).animate(); + + mMenuView = new AccessibilityFloatingMenuView(mContext, mListView); + mMenuView.setShapeType(shapeType); + + verify(mAnimator).translationX(anyFloat()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java new file mode 100644 index 0000000000000..899625eee7d9b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2021 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.graphics.drawable.Drawable; +import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link AccessibilityTargetAdapter}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AccessibilityTargetAdapterTest extends SysuiTestCase { + @Mock + private AccessibilityTarget mAccessibilityTarget; + + @Mock + private Drawable mIcon; + + @Mock + private Drawable.ConstantState mConstantState; + + private ViewHolder mViewHolder; + private AccessibilityTargetAdapter mAdapter; + private final List mTargets = new ArrayList<>(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTargets.add(mAccessibilityTarget); + mAdapter = new AccessibilityTargetAdapter(mTargets); + + final View root = LayoutInflater.from(mContext).inflate( + R.layout.accessibility_floating_menu_item, null); + mViewHolder = new ViewHolder(root); + when(mAccessibilityTarget.getIcon()).thenReturn(mIcon); + when(mIcon.getConstantState()).thenReturn(mConstantState); + } + + @Test + public void onBindViewHolder_setIconWidthHeight_matchResult() { + final int iconWidthHeight = 50; + mAdapter.setIconWidthHeight(iconWidthHeight); + + mAdapter.onBindViewHolder(mViewHolder, 0); + final int actualIconWith = mViewHolder.mIconView.getLayoutParams().width; + + assertThat(actualIconWith).isEqualTo(iconWidthHeight); + } +} From 596c9fab0aeb4a071598156567a8e5ee2f494989 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Tue, 19 Jan 2021 19:13:27 +0800 Subject: [PATCH 3/5] Add new floating action menu for Accessibility targets. (3/n) Patch action: Obsevers the settings key and update the corresponding status. Cherry picked from commit ff5466d31ad1c384ace586646dd092a82af19a93 Bug: 173958541 Test: atest AccessibilityFloatingMenuTest AccessibilityFloatingMenuViewTest AccessibilityTargetAdapterTest Change-Id: Ic3a499f48613519313802298ee09b1e06abf1b1d Merged-In: Ic3a499f48613519313802298ee09b1e06abf1b1d --- .../AccessibilityFloatingMenu.java | 36 +++++++ .../AccessibilityFloatingMenuView.java | 99 ++++++++++++++++++- .../AccessibilityFloatingMenuViewTest.java | 19 +++- 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java index b25a76a59ba82..b71e135fe644d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -16,7 +16,9 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; @@ -37,6 +39,8 @@ import com.android.internal.annotations.VisibleForTesting; * Contains logic for an accessibility floating menu view. */ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { + private static final int DEFAULT_FADE_EFFECT_ENABLED = 1; + private static final float DEFAULT_OPACITY_VALUE = 0.55f; private final Context mContext; private final AccessibilityFloatingMenuView mMenuView; @@ -64,6 +68,15 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { } }; + private final ContentObserver mFadeOutContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext), + getOpacityValue(mContext)); + } + }; + public AccessibilityFloatingMenu(Context context) { mContext = context; mMenuView = new AccessibilityFloatingMenuView(context); @@ -88,6 +101,8 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { mMenuView.show(); mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); + mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext), + getOpacityValue(mContext)); mMenuView.setSizeType(getSizeType(mContext)); mMenuView.setShapeType(getShapeType(mContext)); @@ -105,6 +120,18 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { unregisterContentObservers(); } + private static boolean isFadeEffectEnabled(Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, + DEFAULT_FADE_EFFECT_ENABLED) == /* enable */ 1; + } + + private static float getOpacityValue(Context context) { + return Settings.Secure.getFloat( + context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY, + DEFAULT_OPACITY_VALUE); + } + private static int getSizeType(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL); @@ -129,11 +156,20 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE), /* notifyForDescendants */ false, mShapeContentObserver, UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED), + /* notifyForDescendants */ false, mFadeOutContentObserver, + UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY), + /* notifyForDescendants */ false, mFadeOutContentObserver, + UserHandle.USER_CURRENT); } private void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mContentObserver); mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver); mContext.getContentResolver().unregisterContentObserver(mShapeContentObserver); + mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index 9f147ee936172..3aed1d857cd16 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.floatingmenu; +import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; @@ -25,6 +26,8 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; +import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.MotionEvent; @@ -33,6 +36,7 @@ import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.DimenRes; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -52,9 +56,14 @@ import java.util.List; *

The number of items would depend on strings key * {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}. */ -public class AccessibilityFloatingMenuView extends FrameLayout { +public class AccessibilityFloatingMenuView extends FrameLayout + implements RecyclerView.OnItemTouchListener { private static final float DEFAULT_LOCATION_Y_PERCENTAGE = 0.8f; private static final int INDEX_MENU_ITEM = 0; + private static final int FADE_OUT_DURATION_MS = 1000; + private static final int FADE_EFFECT_DURATION_MS = 3000; + + private boolean mIsFadeEffectEnabled; private boolean mIsShowing; @SizeType private int mSizeType = SizeType.SMALL; @@ -66,6 +75,9 @@ public class AccessibilityFloatingMenuView extends FrameLayout { private int mIconHeight; private final RecyclerView mListView; private final AccessibilityTargetAdapter mAdapter; + private float mFadeOutValue; + private final ValueAnimator mFadeOutAnimator; + private final Handler mUiHandler; private final WindowManager.LayoutParams mLayoutParams; private final WindowManager mWindowManager; private final List mTargets = new ArrayList<>(); @@ -103,6 +115,12 @@ public class AccessibilityFloatingMenuView extends FrameLayout { mWindowManager = context.getSystemService(WindowManager.class); mLayoutParams = createDefaultLayoutParams(); mAdapter = new AccessibilityTargetAdapter(mTargets); + mUiHandler = createUiHandler(); + + mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue); + mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS); + mFadeOutAnimator.addUpdateListener( + (animation) -> setAlpha((float) animation.getAnimatedValue())); updateDimensions(); initListView(); @@ -111,6 +129,36 @@ public class AccessibilityFloatingMenuView extends FrameLayout { updateStrokeWith(uiMode); } + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent event) { + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + fadeIn(); + break; + case MotionEvent.ACTION_MOVE: + // Do nothing + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + fadeOut(); + break; + default: // Do nothing + } + return false; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) { + // Do Nothing + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean b) { + // Do Nothing + } + void show() { if (isShowing()) { return; @@ -134,14 +182,20 @@ public class AccessibilityFloatingMenuView extends FrameLayout { } void onTargetsChanged(List newTargets) { + fadeIn(); + mTargets.clear(); mTargets.addAll(newTargets); mAdapter.notifyDataSetChanged(); updateRadiusWith(mSizeType, mTargets.size()); + + fadeOut(); } void setSizeType(@SizeType int newSizeType) { + fadeIn(); + mSizeType = newSizeType; updateIconSizeWith(newSizeType); @@ -149,9 +203,13 @@ public class AccessibilityFloatingMenuView extends FrameLayout { // When the icon sized changed, the menu size and location will be impacted. updateLocation(); + + fadeOut(); } void setShapeType(@ShapeType int newShapeType) { + fadeIn(); + final boolean isCircleShape = newShapeType == ShapeType.CIRCLE; final float offset = @@ -164,6 +222,36 @@ public class AccessibilityFloatingMenuView extends FrameLayout { isCircleShape ? null : (view, event) -> onTouched(event)); + + fadeOut(); + } + + void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) { + mIsFadeEffectEnabled = isFadeEffectEnabled; + mFadeOutValue = newOpacityValue; + + mFadeOutAnimator.cancel(); + mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue); + setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f); + } + + @VisibleForTesting + void fadeIn() { + if (!mIsFadeEffectEnabled) { + return; + } + + mUiHandler.removeCallbacksAndMessages(null); + mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f)); + } + + @VisibleForTesting + void fadeOut() { + if (!mIsFadeEffectEnabled) { + return; + } + + mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS); } private boolean onTouched(MotionEvent event) { @@ -187,6 +275,14 @@ public class AccessibilityFloatingMenuView extends FrameLayout { getMenuGradientDrawable().setCornerRadii(radii); } + private Handler createUiHandler() { + final Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalArgumentException("looper must not be null"); + } + return new Handler(looper); + } + private void updateDimensions() { final Resources res = getResources(); final DisplayMetrics dm = res.getDisplayMetrics(); @@ -218,6 +314,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout { mListView.setBackground(listViewBackground); mListView.setAdapter(mAdapter); mListView.setLayoutManager(layoutManager); + mListView.addOnItemTouchListener(this); updateListView(); addView(mListView); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index 9ff697cc9957a..124a749e2be12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -82,7 +82,7 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { mTargets.add(mock(AccessibilityTarget.class)); mListView = spy(new RecyclerView(mContext)); - mMenuView = new AccessibilityFloatingMenuView(mContext); + mMenuView = spy(new AccessibilityFloatingMenuView(mContext)); } @Test @@ -177,4 +177,21 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { verify(mAnimator).translationX(anyFloat()); } + + @Test + public void onTargetsChanged_fadeInOut() { + mMenuView.onTargetsChanged(mTargets); + + verify(mMenuView).fadeIn(); + verify(mMenuView).fadeOut(); + } + + @Test + public void setSizeType_fadeInOut() { + final int smallSize = 0; + mMenuView.setSizeType(smallSize); + + verify(mMenuView).fadeIn(); + verify(mMenuView).fadeOut(); + } } From 2f5db393669d3b35e3632c1b87bffce01dcb8fc0 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Fri, 26 Feb 2021 19:26:15 +0800 Subject: [PATCH 4/5] Add new floating action menu for Accessibility targets. (4/n) Patch action: 1. Drag and drop anywhere on the screen. 2. Stick on the closest left side or right side of the screen when dropping. 3. Apply the different styles. - Half oval style when dropping. - Oval style when dragging. Cherry picked from commit adad00ef6fd8342615e2c9121a74186ec8d256e4 Bug: 173958541 Test: atest AccessibilityFloatingMenuTest AccessibilityFloatingMenuViewTest AccessibilityTargetAdapterTest Change-Id: I5f207b86b9ca4342f1414230c4d7524c59f48953 Merged-In: I5f207b86b9ca4342f1414230c4d7524c59f48953 --- ...accessibility_floating_menu_background.xml | 25 +- .../AccessibilityFloatingMenu.java | 2 +- .../AccessibilityFloatingMenuView.java | 361 +++++++++++++++--- .../InstantInsetLayerDrawable.java | 37 ++ .../accessibility/MotionEventHelper.java | 6 +- .../AccessibilityFloatingMenuViewTest.java | 157 +++++++- 6 files changed, 498 insertions(+), 90 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/InstantInsetLayerDrawable.java diff --git a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml index ef67e512d95d4..5148668328da3 100644 --- a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml +++ b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml @@ -15,18 +15,13 @@ limitations under the License. --> - - - - - - - - - \ No newline at end of file + + + + diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java index b71e135fe644d..c5f35e2d1a8a5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -140,7 +140,7 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { private static int getShapeType(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE, - ShapeType.CIRCLE); + ShapeType.OVAL); } private void registerContentObservers() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index 3aed1d857cd16..9ae6034c4d6ff 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -16,6 +16,11 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.util.MathUtils.constrain; +import static android.util.MathUtils.sq; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; @@ -31,8 +36,10 @@ import android.os.Looper; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.MotionEvent; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import androidx.annotation.DimenRes; @@ -47,6 +54,7 @@ import com.android.systemui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -58,27 +66,49 @@ import java.util.List; */ public class AccessibilityFloatingMenuView extends FrameLayout implements RecyclerView.OnItemTouchListener { - private static final float DEFAULT_LOCATION_Y_PERCENTAGE = 0.8f; private static final int INDEX_MENU_ITEM = 0; private static final int FADE_OUT_DURATION_MS = 1000; private static final int FADE_EFFECT_DURATION_MS = 3000; + private static final int SNAP_TO_LOCATION_DURATION_MS = 150; + private static final int MIN_WINDOW_X = 0; + private static final int MIN_WINDOW_Y = 0; + private static final float LOCATION_Y_PERCENTAGE = 0.8f; private boolean mIsFadeEffectEnabled; private boolean mIsShowing; + private boolean mIsDownInEnlargedTouchArea; + private boolean mIsDragging = false; + @Alignment + private int mAlignment = Alignment.RIGHT; @SizeType private int mSizeType = SizeType.SMALL; + @ShapeType + private int mShapeType = ShapeType.OVAL; + @RadiusType + private int mRadiusType = RadiusType.LEFT_HALF_OVAL; private int mMargin; private int mPadding; private int mScreenHeight; private int mScreenWidth; private int mIconWidth; private int mIconHeight; + private int mInset; + private int mDownX; + private int mDownY; + private int mRelativeToPointerDownX; + private int mRelativeToPointerDownY; + private float mRadius; + private float mPercentageY = LOCATION_Y_PERCENTAGE; + private float mSquareScaledTouchSlop; private final RecyclerView mListView; private final AccessibilityTargetAdapter mAdapter; private float mFadeOutValue; private final ValueAnimator mFadeOutAnimator; + @VisibleForTesting + final ValueAnimator mDragAnimator; private final Handler mUiHandler; - private final WindowManager.LayoutParams mLayoutParams; + @VisibleForTesting + final WindowManager.LayoutParams mCurrentLayoutParams; private final WindowManager mWindowManager; private final List mTargets = new ArrayList<>(); @@ -93,13 +123,35 @@ public class AccessibilityFloatingMenuView extends FrameLayout } @IntDef({ - ShapeType.CIRCLE, - ShapeType.HALF_CIRCLE + ShapeType.OVAL, + ShapeType.HALF_OVAL }) @Retention(RetentionPolicy.SOURCE) @interface ShapeType { - int CIRCLE = 0; - int HALF_CIRCLE = 1; + int OVAL = 0; + int HALF_OVAL = 1; + } + + @IntDef({ + RadiusType.LEFT_HALF_OVAL, + RadiusType.OVAL, + RadiusType.RIGHT_HALF_OVAL + }) + @Retention(RetentionPolicy.SOURCE) + @interface RadiusType { + int LEFT_HALF_OVAL = 0; + int OVAL = 1; + int RIGHT_HALF_OVAL = 2; + } + + @IntDef({ + Alignment.LEFT, + Alignment.RIGHT + }) + @Retention(RetentionPolicy.SOURCE) + @interface Alignment { + int LEFT = 0; + int RIGHT = 1; } public AccessibilityFloatingMenuView(Context context) { @@ -113,7 +165,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout mListView = listView; mWindowManager = context.getSystemService(WindowManager.class); - mLayoutParams = createDefaultLayoutParams(); + mCurrentLayoutParams = createDefaultLayoutParams(); mAdapter = new AccessibilityTargetAdapter(mTargets); mUiHandler = createUiHandler(); @@ -122,30 +174,89 @@ public class AccessibilityFloatingMenuView extends FrameLayout mFadeOutAnimator.addUpdateListener( (animation) -> setAlpha((float) animation.getAnimatedValue())); + mDragAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mDragAnimator.setDuration(SNAP_TO_LOCATION_DURATION_MS); + mDragAnimator.setInterpolator(new OvershootInterpolator()); + mDragAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAlignment = calculateCurrentAlignment(); + mPercentageY = calculateCurrentPercentageY(); + + updateLocationWith(mAlignment, mPercentageY); + updateMarginsWith(mAlignment); + updateOffsetWith(mShapeType, mAlignment); + + updateInsetWith(getResources().getConfiguration().uiMode, mAlignment); + + mRadiusType = (mAlignment == Alignment.RIGHT) + ? RadiusType.LEFT_HALF_OVAL + : RadiusType.RIGHT_HALF_OVAL; + updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); + + fadeOut(); + } + }); + updateDimensions(); initListView(); - - final int uiMode = context.getResources().getConfiguration().uiMode; - updateStrokeWith(uiMode); + updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment); } @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { + final int currentRawX = (int) event.getRawX(); + final int currentRawY = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: fadeIn(); + + mDownX = currentRawX; + mDownY = currentRawY; + mRelativeToPointerDownX = mCurrentLayoutParams.x - mDownX; + mRelativeToPointerDownY = mCurrentLayoutParams.y - mDownY; + mListView.animate().translationX(0); break; case MotionEvent.ACTION_MOVE: - // Do nothing + if (mIsDragging + || hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) { + if (!mIsDragging) { + mIsDragging = true; + setRadius(mRadius, RadiusType.OVAL); + setInset(0, 0); + } + + final int newWindowX = currentRawX + mRelativeToPointerDownX; + final int newWindowY = currentRawY + mRelativeToPointerDownY; + mCurrentLayoutParams.x = constrain(newWindowX, MIN_WINDOW_X, getMaxWindowX()); + mCurrentLayoutParams.y = constrain(newWindowY, MIN_WINDOW_Y, getMaxWindowY()); + mWindowManager.updateViewLayout(this, mCurrentLayoutParams); + } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: + if (mIsDragging) { + mIsDragging = false; + + final int maxX = getMaxWindowX(); + final int endX = mCurrentLayoutParams.x > ((MIN_WINDOW_X + maxX) / 2) + ? maxX : MIN_WINDOW_X; + final int endY = mCurrentLayoutParams.y; + snapToLocation(endX, endY); + + // Avoid triggering the listener of the item. + return true; + } + updateOffsetWith(mShapeType, mAlignment); + fadeOut(); break; default: // Do nothing } + + // not consume all the events here because keeping the scroll behavior of list view. return false; } @@ -165,7 +276,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout } mIsShowing = true; - mWindowManager.addView(this, mLayoutParams); + mWindowManager.addView(this, mCurrentLayoutParams); + setSystemGestureExclusion(); } void hide() { @@ -175,6 +287,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout mIsShowing = false; mWindowManager.removeView(this); + setSystemGestureExclusion(); } boolean isShowing() { @@ -188,7 +301,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout mTargets.addAll(newTargets); mAdapter.notifyDataSetChanged(); - updateRadiusWith(mSizeType, mTargets.size()); + updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); + setSystemGestureExclusion(); fadeOut(); } @@ -199,10 +313,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout mSizeType = newSizeType; updateIconSizeWith(newSizeType); - updateRadiusWith(newSizeType, mTargets.size()); + updateRadiusWith(newSizeType, mRadiusType, mTargets.size()); // When the icon sized changed, the menu size and location will be impacted. - updateLocation(); + updateLocationWith(mAlignment, mPercentageY); + setSystemGestureExclusion(); fadeOut(); } @@ -210,16 +325,12 @@ public class AccessibilityFloatingMenuView extends FrameLayout void setShapeType(@ShapeType int newShapeType) { fadeIn(); - final boolean isCircleShape = - newShapeType == ShapeType.CIRCLE; - final float offset = - isCircleShape - ? 0 - : getLayoutWidth() / 2.0f; - mListView.animate().translationX(offset); + mShapeType = newShapeType; + + updateOffsetWith(newShapeType, mAlignment); setOnTouchListener( - isCircleShape + newShapeType == ShapeType.OVAL ? null : (view, event) -> onTouched(event)); @@ -241,6 +352,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout return; } + mFadeOutAnimator.cancel(); mUiHandler.removeCallbacksAndMessages(null); mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f)); } @@ -255,24 +367,50 @@ public class AccessibilityFloatingMenuView extends FrameLayout } private boolean onTouched(MotionEvent event) { + final int action = event.getAction(); final int currentX = (int) event.getX(); final int currentY = (int) event.getY(); final int menuHalfWidth = getLayoutWidth() / 2; final Rect touchDelegateBounds = new Rect(mMargin, mMargin, mMargin + menuHalfWidth, mMargin + getLayoutHeight()); - if (touchDelegateBounds.contains(currentX, currentY)) { - // In order to correspond to the correct item of list view. - event.setLocation(currentX - mMargin, currentY - mMargin); - return mListView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN + && touchDelegateBounds.contains(currentX, currentY)) { + mIsDownInEnlargedTouchArea = true; } - return false; + if (!mIsDownInEnlargedTouchArea) { + return false; + } + + if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL) { + mIsDownInEnlargedTouchArea = false; + } + + // In order to correspond to the correct item of list view. + event.setLocation(currentX - mMargin, currentY - mMargin); + return mListView.dispatchTouchEvent(event); } - private void setRadius(float radius) { - final float[] radii = new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; - getMenuGradientDrawable().setCornerRadii(radii); + private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) { + return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop; + } + + private void setRadius(float radius, @RadiusType int type) { + getMenuGradientDrawable().setCornerRadii(createRadii(radius, type)); + } + + private float[] createRadii(float radius, @RadiusType int type) { + if (type == RadiusType.LEFT_HALF_OVAL) { + return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + } + + if (type == RadiusType.RIGHT_HALF_OVAL) { + return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}; + } + + return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; } private Handler createUiHandler() { @@ -292,6 +430,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); mPadding = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding); + mInset = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); + + mSquareScaledTouchSlop = + sq(ViewConfiguration.get(getContext()).getScaledTouchSlop()); } private void updateIconSizeWith(@SizeType int sizeType) { @@ -308,13 +451,20 @@ public class AccessibilityFloatingMenuView extends FrameLayout } private void initListView() { - final Drawable listViewBackground = + final Drawable background = getContext().getDrawable(R.drawable.accessibility_floating_menu_background); final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); - mListView.setBackground(listViewBackground); + final LayoutParams layoutParams = + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mListView.setLayoutParams(layoutParams); + final InstantInsetLayerDrawable layerDrawable = + new InstantInsetLayerDrawable(new Drawable[]{background}); + mListView.setBackground(layerDrawable); mListView.setAdapter(mAdapter); mListView.setLayoutManager(layoutManager); mListView.addOnItemTouchListener(this); + mListView.animate().setInterpolator(new OvershootInterpolator()); updateListView(); addView(mListView); @@ -323,12 +473,9 @@ public class AccessibilityFloatingMenuView extends FrameLayout private void updateListView() { final int elevation = getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); - final LayoutParams layoutParams = - new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - layoutParams.setMarginsRelative(mMargin, mMargin, /* end= */ 0, mMargin); - mListView.setLayoutParams(layoutParams); mListView.setElevation(elevation); + + updateMarginsWith(mAlignment); } private WindowManager.LayoutParams createDefaultLayoutParams() { @@ -338,10 +485,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); - final DisplayMetrics dm = getResources().getDisplayMetrics(); + params.windowAnimations = android.R.style.Animation_Translucent; params.gravity = Gravity.START | Gravity.TOP; - params.x = dm.widthPixels; - params.y = (int) (dm.heightPixels * DEFAULT_LOCATION_Y_PERCENTAGE); + params.x = getMaxWindowX(); + params.y = (int) (getMaxWindowY() * mPercentageY); return params; } @@ -354,12 +501,37 @@ public class AccessibilityFloatingMenuView extends FrameLayout updateListView(); updateIconSizeWith(mSizeType); updateColor(); - updateStrokeWith(newConfig.uiMode); - updateLocation(); + updateStrokeWith(newConfig.uiMode, mAlignment); + updateLocationWith(mAlignment, mPercentageY); } - private LayerDrawable getMenuLayerDrawable() { - return (LayerDrawable) mListView.getBackground(); + private void snapToLocation(int endX, int endY) { + mDragAnimator.cancel(); + mDragAnimator.removeAllUpdateListeners(); + mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY)); + mDragAnimator.start(); + } + + private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) { + float value = (float) animator.getAnimatedValue(); + final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX)); + final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY)); + + mCurrentLayoutParams.x = newX; + mCurrentLayoutParams.y = newY; + mWindowManager.updateViewLayout(this, mCurrentLayoutParams); + } + + private int getMaxWindowX() { + return mScreenWidth - mMargin - getLayoutWidth(); + } + + private int getMaxWindowY() { + return mScreenHeight - getWindowHeight(); + } + + private InstantInsetLayerDrawable getMenuLayerDrawable() { + return (InstantInsetLayerDrawable) mListView.getBackground(); } private GradientDrawable getMenuGradientDrawable() { @@ -369,10 +541,30 @@ public class AccessibilityFloatingMenuView extends FrameLayout /** * Updates the floating menu to be fixed at the side of the screen. */ - private void updateLocation() { - mLayoutParams.x = mScreenWidth - mMargin - getLayoutWidth(); - mLayoutParams.y = (int) (mScreenHeight * DEFAULT_LOCATION_Y_PERCENTAGE); - mWindowManager.updateViewLayout(this, mLayoutParams); + private void updateLocationWith(@Alignment int side, float percentageCurrentY) { + mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : MIN_WINDOW_X; + mCurrentLayoutParams.y = (int) (percentageCurrentY * getMaxWindowY()); + mWindowManager.updateViewLayout(this, mCurrentLayoutParams); + } + + private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) { + final float halfWidth = getLayoutWidth() / 2.0f; + final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth; + mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset); + } + + private void updateMarginsWith(@Alignment int side) { + final LayoutParams layoutParams = (LayoutParams) mListView.getLayoutParams(); + final int marginLeft = (side == Alignment.LEFT) ? 0 : mMargin; + final int marginRight = (side == Alignment.RIGHT) ? 0 : mMargin; + + if (marginLeft == layoutParams.leftMargin + && marginRight == layoutParams.rightMargin) { + return; + } + + layoutParams.setMargins(marginLeft, mMargin, marginRight, mMargin); + mListView.setLayoutParams(layoutParams); } private void updateColor() { @@ -380,17 +572,13 @@ public class AccessibilityFloatingMenuView extends FrameLayout getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId)); } - private void updateStrokeWith(int uiMode) { - final Resources res = getResources(); + private void updateStrokeWith(int uiMode, @Alignment int side) { + updateInsetWith(uiMode, side); + final boolean isNightMode = (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - - final int inset = - res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); - final int insetRight = isNightMode ? inset : 0; - getMenuLayerDrawable().setLayerInset(INDEX_MENU_ITEM, 0, 0, insetRight, 0); - + final Resources res = getResources(); final int width = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width); final int strokeWidth = isNightMode ? width : 0; @@ -398,8 +586,43 @@ public class AccessibilityFloatingMenuView extends FrameLayout getMenuGradientDrawable().setStroke(strokeWidth, strokeColor); } - private void updateRadiusWith(@SizeType int sizeType, int itemCount) { - setRadius(getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount))); + private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType, + int itemCount) { + mRadius = + getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount)); + setRadius(mRadius, radiusType); + } + + private void updateInsetWith(int uiMode, @Alignment int side) { + final boolean isNightMode = + (uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + + final int layerInset = isNightMode ? mInset : 0; + final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0; + final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0; + setInset(insetLeft, insetRight); + } + + private void setInset(int left, int right) { + final LayerDrawable layerDrawable = getMenuLayerDrawable(); + if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left + && layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) { + return; + } + + layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0); + } + + @Alignment + private int calculateCurrentAlignment() { + return mCurrentLayoutParams.x >= ((MIN_WINDOW_X + getMaxWindowX()) / 2) + ? Alignment.RIGHT + : Alignment.LEFT; + } + + private float calculateCurrentPercentageY() { + return mCurrentLayoutParams.y / (float) getMaxWindowY(); } private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) { @@ -425,6 +648,24 @@ public class AccessibilityFloatingMenuView extends FrameLayout } private int getLayoutHeight() { - return (mPadding + mIconHeight) * mTargets.size() + mPadding; + return Math.min(mScreenHeight - mMargin * 2, + (mPadding + mIconHeight) * mTargets.size() + mPadding); + } + + private int getWindowWidth() { + return mMargin + getLayoutWidth(); + } + + private int getWindowHeight() { + return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight()); + } + + private void setSystemGestureExclusion() { + final Rect excludeZone = + new Rect(0, 0, getWindowWidth(), getWindowHeight()); + post(() -> setSystemGestureExclusionRects( + mIsShowing + ? Collections.singletonList(excludeZone) + : Collections.emptyList())); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/InstantInsetLayerDrawable.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/InstantInsetLayerDrawable.java new file mode 100644 index 0000000000000..6c021a6f3c7ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/InstantInsetLayerDrawable.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 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.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; + +/** + * A drawable that forces to update the bounds {@link #onBoundsChange(Rect)} immediately after + * {@link #setLayerInset} dynamically. + */ +public class InstantInsetLayerDrawable extends LayerDrawable { + public InstantInsetLayerDrawable(Drawable[] layers) { + super(layers); + } + + @Override + public void setLayerInset(int index, int l, int t, int r, int b) { + super.setLayerInset(index, l, t, r, b); + onBoundsChange(getBounds()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java index 92dad9bdb1204..550e77d63c3b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java @@ -23,11 +23,11 @@ import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.List; -class MotionEventHelper { +public class MotionEventHelper { @GuardedBy("this") private final List mMotionEvents = new ArrayList<>(); - void recycleEvents() { + public void recycleEvents() { for (MotionEvent event:mMotionEvents) { event.recycle(); } @@ -36,7 +36,7 @@ class MotionEventHelper { } } - MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, + public MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, float y) { MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0); synchronized (this) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index 124a749e2be12..b177e00f14f8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -23,31 +23,38 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.MotionEvent; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.MotionEventHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -67,8 +74,17 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { @Mock private ViewPropertyAnimator mAnimator; + private MotionEvent mInterceptMotionEvent; + private RecyclerView mListView; + private int mMenuHalfWidth; + private int mMenuHalfHeight; + private int mScreenHalfWidth; + private int mScreenHalfHeight; + private int mMaxWindowX; + + private final MotionEventHelper mMotionEventHelper = new MotionEventHelper(); private final List mTargets = new ArrayList<>(); @Before @@ -81,8 +97,25 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mTargets.add(mock(AccessibilityTarget.class)); - mListView = spy(new RecyclerView(mContext)); - mMenuView = spy(new AccessibilityFloatingMenuView(mContext)); + mListView = new RecyclerView(mContext); + mMenuView = new AccessibilityFloatingMenuView(mContext, mListView); + + final Resources res = mContext.getResources(); + final int margin = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); + final int padding = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding); + final int iconWidthHeight = + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height); + final int menuWidth = padding * 2 + iconWidthHeight; + final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding; + final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; + final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels; + mMenuHalfWidth = menuWidth / 2; + mMenuHalfHeight = menuHeight / 2; + mScreenHalfWidth = screenWidth / 2; + mScreenHalfHeight = screenHeight / 2; + mMaxWindowX = screenWidth - margin - menuWidth; } @Test @@ -169,29 +202,131 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { @Test public void setShapeType_halfCircle_translationX() { + final RecyclerView listView = spy(new RecyclerView(mContext)); + final AccessibilityFloatingMenuView menuView = + new AccessibilityFloatingMenuView(mContext, listView); final int shapeType = 2; - doReturn(mAnimator).when(mListView).animate(); + doReturn(mAnimator).when(listView).animate(); - mMenuView = new AccessibilityFloatingMenuView(mContext, mListView); - mMenuView.setShapeType(shapeType); + menuView.setShapeType(shapeType); verify(mAnimator).translationX(anyFloat()); } @Test public void onTargetsChanged_fadeInOut() { - mMenuView.onTargetsChanged(mTargets); + final AccessibilityFloatingMenuView menuView = spy(mMenuView); + final InOrder inOrderMenuView = inOrder(menuView); - verify(mMenuView).fadeIn(); - verify(mMenuView).fadeOut(); + menuView.onTargetsChanged(mTargets); + + inOrderMenuView.verify(menuView).fadeIn(); + inOrderMenuView.verify(menuView).fadeOut(); } @Test public void setSizeType_fadeInOut() { + final AccessibilityFloatingMenuView menuView = spy(mMenuView); + final InOrder inOrderMenuView = inOrder(menuView); final int smallSize = 0; - mMenuView.setSizeType(smallSize); + menuView.setSizeType(smallSize); - verify(mMenuView).fadeIn(); - verify(mMenuView).fadeOut(); + inOrderMenuView.verify(menuView).fadeIn(); + inOrderMenuView.verify(menuView).fadeOut(); + } + + @Test + public void tapOnAndDragMenu_interceptUpEvent() { + final RecyclerView listView = new RecyclerView(mContext); + final TestAccessibilityFloatingMenu menuView = + new TestAccessibilityFloatingMenu(mContext, listView); + + menuView.show(); + menuView.onTargetsChanged(mTargets); + menuView.setSizeType(0); + menuView.setShapeType(0); + final int currentWindowX = mMenuView.mCurrentLayoutParams.x; + final int currentWindowY = mMenuView.mCurrentLayoutParams.y; + final MotionEvent downEvent = + mMotionEventHelper.obtainMotionEvent(0, 1, + MotionEvent.ACTION_DOWN, + currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth, + currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight); + final MotionEvent moveEvent = + mMotionEventHelper.obtainMotionEvent(2, 3, + MotionEvent.ACTION_MOVE, + /* screenCenterX */mScreenHalfWidth + - /* offsetXToScreenLeftHalfRegion */ 10, + /* screenCenterY */ mScreenHalfHeight); + final MotionEvent upEvent = + mMotionEventHelper.obtainMotionEvent(4, 5, + MotionEvent.ACTION_UP, + /* screenCenterX */ mScreenHalfWidth + - /* offsetXToScreenLeftHalfRegion */ 10, + /* screenCenterY */ mScreenHalfHeight); + listView.dispatchTouchEvent(downEvent); + listView.dispatchTouchEvent(moveEvent); + listView.dispatchTouchEvent(upEvent); + + assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP); + } + + @Test + public void tapOnAndDragMenu_matchLocation() { + mMenuView.show(); + mMenuView.onTargetsChanged(mTargets); + mMenuView.setSizeType(0); + mMenuView.setShapeType(0); + final int currentWindowX = mMenuView.mCurrentLayoutParams.x; + final int currentWindowY = mMenuView.mCurrentLayoutParams.y; + final MotionEvent downEvent = + mMotionEventHelper.obtainMotionEvent(0, 1, + MotionEvent.ACTION_DOWN, + currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth, + currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight); + final MotionEvent moveEvent = + mMotionEventHelper.obtainMotionEvent(2, 3, + MotionEvent.ACTION_MOVE, + /* screenCenterX */mScreenHalfWidth + + /* offsetXToScreenRightHalfRegion */ 10, + /* screenCenterY */ mScreenHalfHeight); + final MotionEvent upEvent = + mMotionEventHelper.obtainMotionEvent(4, 5, + MotionEvent.ACTION_UP, + /* screenCenterX */ mScreenHalfWidth + + /* offsetXToScreenRightHalfRegion */ 10, + /* screenCenterY */ mScreenHalfHeight); + mListView.dispatchTouchEvent(downEvent); + mListView.dispatchTouchEvent(moveEvent); + mListView.dispatchTouchEvent(upEvent); + mMenuView.mDragAnimator.end(); + + assertThat(mMenuView.mCurrentLayoutParams.x).isEqualTo(mMaxWindowX); + assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo( + /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight); + } + + @After + public void tearDown() { + mInterceptMotionEvent = null; + mMotionEventHelper.recycleEvents(); + } + + private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView { + TestAccessibilityFloatingMenu(Context context, RecyclerView listView) { + super(context, listView); + } + + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent event) { + final boolean intercept = super.onInterceptTouchEvent(recyclerView, event); + + if (intercept) { + mInterceptMotionEvent = event; + } + + return intercept; + } } } From 1b682704fdaa5fcb603badffb9f8e0395895f0b3 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Wed, 10 Mar 2021 20:53:59 +0800 Subject: [PATCH 5/5] Add new floating action menu for Accessibility targets. (5/n) Action: AccessibilityFloatingMenu has two shape types, one is oval style, the other is half oval style. - Half oval shape when dragging. - Oval shape when dropping. The patch will provide the capability of switch shape type by dragging into the screen edge or out. Cherry picked from commit 0a2e8f9a5648008ba125f8e7ecb37bb674beab34 Bug: 173958541 Test: atest AccessibilityFloatingMenuTest AccessibilityFloatingMenuViewTest AccessibilityTargetAdapterTest Change-Id: Ib86713cd6a48e102cb9088beb6353391caeadc83 Merged-In: Ib86713cd6a48e102cb9088beb6353391caeadc83 --- .../AccessibilityFloatingMenu.java | 20 +++-------- .../AccessibilityFloatingMenuView.java | 25 ++++++++++++-- .../AccessibilityFloatingMenuViewTest.java | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java index c5f35e2d1a8a5..7b4ce61d2cfe1 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -43,9 +43,10 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { private static final float DEFAULT_OPACITY_VALUE = 0.55f; private final Context mContext; private final AccessibilityFloatingMenuView mMenuView; + private final Handler mHandler = new Handler(Looper.getMainLooper()); private final ContentObserver mContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { + new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); @@ -53,23 +54,15 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { }; private final ContentObserver mSizeContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { + new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.setSizeType(getSizeType(mContext)); } }; - private final ContentObserver mShapeContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { - @Override - public void onChange(boolean selfChange) { - mMenuView.setShapeType(getShapeType(mContext)); - } - }; - private final ContentObserver mFadeOutContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { + new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext), @@ -152,10 +145,6 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE), /* notifyForDescendants */ false, mSizeContentObserver, UserHandle.USER_CURRENT); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE), - /* notifyForDescendants */ false, mShapeContentObserver, - UserHandle.USER_CURRENT); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED), /* notifyForDescendants */ false, mFadeOutContentObserver, @@ -169,7 +158,6 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { private void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mContentObserver); mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver); - mContext.getContentResolver().unregisterContentObserver(mShapeContentObserver); mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index 9ae6034c4d6ff..ab05c2a273ad4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -82,8 +82,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout private int mAlignment = Alignment.RIGHT; @SizeType private int mSizeType = SizeType.SMALL; + @VisibleForTesting @ShapeType - private int mShapeType = ShapeType.OVAL; + int mShapeType = ShapeType.OVAL; + private int mTemporaryShapeType; @RadiusType private int mRadiusType = RadiusType.LEFT_HALF_OVAL; private int mMargin; @@ -185,7 +187,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout updateLocationWith(mAlignment, mPercentageY); updateMarginsWith(mAlignment); - updateOffsetWith(mShapeType, mAlignment); updateInsetWith(getResources().getConfiguration().uiMode, mAlignment); @@ -228,6 +229,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout setInset(0, 0); } + mTemporaryShapeType = + isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX) + ? ShapeType.HALF_OVAL + : ShapeType.OVAL; final int newWindowX = currentRawX + mRelativeToPointerDownX; final int newWindowY = currentRawY + mRelativeToPointerDownY; mCurrentLayoutParams.x = constrain(newWindowX, MIN_WINDOW_X, getMaxWindowX()); @@ -246,10 +251,19 @@ public class AccessibilityFloatingMenuView extends FrameLayout final int endY = mCurrentLayoutParams.y; snapToLocation(endX, endY); + setShapeType(mTemporaryShapeType); + // Avoid triggering the listener of the item. return true; } - updateOffsetWith(mShapeType, mAlignment); + + // Must switch the oval shape type before tapping the corresponding item in the + // list view, otherwise it can't work on it. + if (mShapeType == ShapeType.HALF_OVAL) { + setShapeType(ShapeType.OVAL); + + return true; + } fadeOut(); break; @@ -393,6 +407,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout return mListView.dispatchTouchEvent(event); } + private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) { + return (side == Alignment.RIGHT && currentRawX > downX) + || (side == Alignment.LEFT && downX > currentRawX); + } + private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) { return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index b177e00f14f8a..8683dd6c33bd8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -306,6 +306,39 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase { /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight); } + + @Test + public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() { + mMenuView.show(); + mMenuView.onTargetsChanged(mTargets); + mMenuView.setSizeType(0); + mMenuView.setShapeType(/* oval */ 0); + final int currentWindowX = mMenuView.mCurrentLayoutParams.x; + final int currentWindowY = mMenuView.mCurrentLayoutParams.y; + final MotionEvent downEvent = + mMotionEventHelper.obtainMotionEvent(0, 1, + MotionEvent.ACTION_DOWN, + currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth, + currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight); + final MotionEvent moveEvent = + mMotionEventHelper.obtainMotionEvent(2, 3, + MotionEvent.ACTION_MOVE, + /* downX */(currentWindowX + mMenuHalfWidth) + + /* offsetXToScreenRightSide */ mMenuHalfWidth, + /* downY */ (currentWindowY + mMenuHalfHeight)); + final MotionEvent upEvent = + mMotionEventHelper.obtainMotionEvent(4, 5, + MotionEvent.ACTION_UP, + /* downX */(currentWindowX + mMenuHalfWidth) + + /* offsetXToScreenRightSide */ mMenuHalfWidth, + /* downY */ (currentWindowY + mMenuHalfHeight)); + mListView.dispatchTouchEvent(downEvent); + mListView.dispatchTouchEvent(moveEvent); + mListView.dispatchTouchEvent(upEvent); + + assertThat(mMenuView.mShapeType).isEqualTo(/* halfOval */ 1); + } + @After public void tearDown() { mInterceptMotionEvent = null;