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 adad00ef6f
Bug: 173958541
Test: atest AccessibilityFloatingMenuTest
AccessibilityFloatingMenuViewTest AccessibilityTargetAdapterTest
Change-Id: I5f207b86b9ca4342f1414230c4d7524c59f48953
Merged-In: I5f207b86b9ca4342f1414230c4d7524c59f48953
This commit is contained in:
@@ -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>
|
||||
<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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user