summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml25
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java361
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/InstantInsetLayerDrawable.java37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java157
6 files changed, 498 insertions, 90 deletions
diff --git a/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_menu_background.xml
index ef67e512d95d..5148668328da 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.
-->
-<layer-list
- xmlns:android="http://schemas.android.com/apk/res/android">
-
- <item android:id="@+id/menu_background_item">
- <shape android:shape="rectangle">
- <corners
- android:bottomLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
- android:bottomRightRadius="0dp"
- android:topLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
- android:topRightRadius="0dp"/>
- <solid
- android:color="@color/accessibility_floating_menu_background"/>
- </shape>
- </item>
-</layer-list> \ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
+ android:bottomRightRadius="0dp"
+ android:topLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
+ android:topRightRadius="0dp"/>
+ <solid
+ android:color="@color/accessibility_floating_menu_background"/>
+</shape>
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 b71e135fe644..c5f35e2d1a8a 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 3aed1d857cd1..9ae6034c4d6f 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<AccessibilityTarget> 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 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 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 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 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 LayerDrawable getMenuLayerDrawable() {
- return (LayerDrawable) mListView.getBackground();
+ 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 000000000000..6c021a6f3c7a
--- /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 92dad9bdb120..550e77d63c3b 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<MotionEvent> 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 124a749e2be1..b177e00f14f8 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<AccessibilityTarget> 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);
+
+ menuView.onTargetsChanged(mTargets);
- verify(mMenuView).fadeIn();
- verify(mMenuView).fadeOut();
+ 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);
+
+ 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;
+ }
- verify(mMenuView).fadeIn();
- verify(mMenuView).fadeOut();
+ return intercept;
+ }
}
}