diff options
| author | 2021-02-26 19:26:15 +0800 | |
|---|---|---|
| committer | 2021-03-19 11:35:26 +0800 | |
| commit | 2f5db393669d3b35e3632c1b87bffce01dcb8fc0 (patch) | |
| tree | 52977b8c3d7a76edd94ff9f34c02dc2c1fb46873 | |
| parent | 596c9fab0aeb4a071598156567a8e5ee2f494989 (diff) | |
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
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; + } } } |