/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility.floatingmenu; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.util.MathUtils.constrain; import static android.util.MathUtils.sq; import static android.view.WindowInsets.Type.displayCutout; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.FloatRange; import android.annotation.IntDef; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.os.Handler; import android.os.Looper; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.view.animation.Animation; import android.view.animation.OvershootInterpolator; import android.view.animation.TranslateAnimation; import android.widget.FrameLayout; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.core.view.AccessibilityDelegateCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; /** * Accessibility floating menu is used for the actions of accessibility features, it's also the * action set. * *

The number of items would depend on strings key * {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}. */ public class AccessibilityFloatingMenuView extends FrameLayout implements RecyclerView.OnItemTouchListener { 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_Y = 0; private static final int ANIMATION_START_OFFSET = 600; private static final int ANIMATION_DURATION_MS = 600; private static final float ANIMATION_TO_X_VALUE = 0.5f; private boolean mIsFadeEffectEnabled; private boolean mIsShowing; private boolean mIsDownInEnlargedTouchArea; private boolean mIsDragging = false; @Alignment private int mAlignment; @SizeType private int mSizeType = SizeType.SMALL; @VisibleForTesting @ShapeType int mShapeType = ShapeType.OVAL; private int mTemporaryShapeType; @RadiusType private int mRadiusType; private int mMargin; private int mPadding; // The display width excludes the window insets of the system bar and display cutout. private int mDisplayHeight; // The display Height excludes the window insets of the system bar and display cutout. private int mDisplayWidth; 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 final Rect mDisplayInsetsRect = new Rect(); private final Rect mImeInsetsRect = new Rect(); private final Position mPosition; private float mSquareScaledTouchSlop; private final Configuration mLastConfiguration; private Optional mOnDragEndListener = Optional.empty(); private final RecyclerView mListView; private final AccessibilityTargetAdapter mAdapter; private float mFadeOutValue; private final ValueAnimator mFadeOutAnimator; @VisibleForTesting final ValueAnimator mDragAnimator; private final Handler mUiHandler; @VisibleForTesting final WindowManager.LayoutParams mCurrentLayoutParams; private final WindowManager mWindowManager; private final List mTargets = new ArrayList<>(); @IntDef({ SizeType.SMALL, SizeType.LARGE }) @Retention(RetentionPolicy.SOURCE) @interface SizeType { int SMALL = 0; int LARGE = 1; } @IntDef({ ShapeType.OVAL, ShapeType.HALF_OVAL }) @Retention(RetentionPolicy.SOURCE) @interface ShapeType { 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; } /** * Interface for a callback to be invoked when the floating menu was dragging. */ interface OnDragEndListener { /** * Called when a drag is completed. * * @param position Stores information about the position */ void onDragEnd(Position position); } public AccessibilityFloatingMenuView(Context context, @NonNull Position position) { this(context, position, new RecyclerView(context)); } @VisibleForTesting AccessibilityFloatingMenuView(Context context, @NonNull Position position, RecyclerView listView) { super(context); mListView = listView; mWindowManager = context.getSystemService(WindowManager.class); mLastConfiguration = new Configuration(getResources().getConfiguration()); mAdapter = new AccessibilityTargetAdapter(mTargets); mUiHandler = createUiHandler(); mPosition = position; mAlignment = transformToAlignment(mPosition.getPercentageX()); mRadiusType = (mAlignment == Alignment.RIGHT) ? RadiusType.LEFT_HALF_OVAL : RadiusType.RIGHT_HALF_OVAL; updateDimensions(); mCurrentLayoutParams = createDefaultLayoutParams(); mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue); mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS); 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) { mPosition.update(transformCurrentPercentageXToEdge(), calculateCurrentPercentageY()); mAlignment = transformToAlignment(mPosition.getPercentageX()); updateLocationWith(mPosition); updateInsetWith(getResources().getConfiguration().uiMode, mAlignment); mRadiusType = (mAlignment == Alignment.RIGHT) ? RadiusType.LEFT_HALF_OVAL : RadiusType.RIGHT_HALF_OVAL; updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); fadeOut(); mOnDragEndListener.ifPresent( onDragEndListener -> onDragEndListener.onDragEnd(mPosition)); } }); initListView(); 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: if (mIsDragging || hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) { if (!mIsDragging) { mIsDragging = true; setRadius(mRadius, RadiusType.OVAL); setInset(0, 0); } mTemporaryShapeType = isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX) ? ShapeType.HALF_OVAL : ShapeType.OVAL; final int newWindowX = currentRawX + mRelativeToPointerDownX; final int newWindowY = currentRawY + mRelativeToPointerDownY; mCurrentLayoutParams.x = constrain(newWindowX, getMinWindowX(), 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 minX = getMinWindowX(); final int maxX = getMaxWindowX(); final int endX = mCurrentLayoutParams.x > ((minX + maxX) / 2) ? maxX : minX; final int endY = mCurrentLayoutParams.y; snapToLocation(endX, endY); setShapeType(mTemporaryShapeType); // Avoid triggering the listener of the item. return true; } // Must switch the oval shape type before tapping the corresponding item in the // list view, otherwise it can't work on it. if (!isOvalShape()) { setShapeType(ShapeType.OVAL); return true; } fadeOut(); break; default: // Do nothing } // not consume all the events here because keeping the scroll behavior of list view. return false; } @Override public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) { // Do Nothing } @Override public void onRequestDisallowInterceptTouchEvent(boolean b) { // Do Nothing } void show() { if (isShowing()) { return; } mIsShowing = true; mWindowManager.addView(this, mCurrentLayoutParams); setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets)); setSystemGestureExclusion(); } void hide() { if (!isShowing()) { return; } mIsShowing = false; mWindowManager.removeView(this); setOnApplyWindowInsetsListener(null); setSystemGestureExclusion(); } boolean isShowing() { return mIsShowing; } boolean isOvalShape() { return mShapeType == ShapeType.OVAL; } void onTargetsChanged(List newTargets) { fadeIn(); mTargets.clear(); mTargets.addAll(newTargets); onEnabledFeaturesChanged(); updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); updateScrollModeWith(hasExceededMaxLayoutHeight()); setSystemGestureExclusion(); fadeOut(); } void setSizeType(@SizeType int newSizeType) { fadeIn(); mSizeType = newSizeType; updateItemViewWith(newSizeType); updateRadiusWith(newSizeType, mRadiusType, mTargets.size()); // When the icon sized changed, the menu size and location will be impacted. updateLocationWith(mPosition); updateScrollModeWith(hasExceededMaxLayoutHeight()); updateOffsetWith(mShapeType, mAlignment); setSystemGestureExclusion(); fadeOut(); } void setShapeType(@ShapeType int newShapeType) { fadeIn(); mShapeType = newShapeType; updateOffsetWith(newShapeType, mAlignment); setOnTouchListener( newShapeType == ShapeType.OVAL ? null : (view, event) -> onTouched(event)); fadeOut(); } public void setOnDragEndListener(OnDragEndListener onDragEndListener) { mOnDragEndListener = Optional.ofNullable(onDragEndListener); } void startTranslateXAnimation() { fadeIn(); final float toXValue = (mAlignment == Alignment.RIGHT) ? ANIMATION_TO_X_VALUE : -ANIMATION_TO_X_VALUE; final TranslateAnimation animation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, toXValue, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0); animation.setDuration(ANIMATION_DURATION_MS); animation.setRepeatMode(Animation.REVERSE); animation.setInterpolator(new OvershootInterpolator()); animation.setRepeatCount(Animation.INFINITE); animation.setStartOffset(ANIMATION_START_OFFSET); mListView.startAnimation(animation); } void stopTranslateXAnimation() { mListView.clearAnimation(); fadeOut(); } Rect getWindowLocationOnScreen() { final int left = mCurrentLayoutParams.x; final int top = mCurrentLayoutParams.y; return new Rect(left, top, left + getWindowWidth(), top + getWindowHeight()); } void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) { mIsFadeEffectEnabled = isFadeEffectEnabled; mFadeOutValue = newOpacityValue; mFadeOutAnimator.cancel(); mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue); setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f); } void onEnabledFeaturesChanged() { mAdapter.notifyDataSetChanged(); } @VisibleForTesting void fadeIn() { if (!mIsFadeEffectEnabled) { return; } mFadeOutAnimator.cancel(); mUiHandler.removeCallbacksAndMessages(null); mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f)); } @VisibleForTesting void fadeOut() { if (!mIsFadeEffectEnabled) { return; } mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS); } private boolean onTouched(MotionEvent event) { final int action = event.getAction(); final int currentX = (int) event.getX(); final int currentY = (int) event.getY(); final int marginStartEnd = getMarginStartEndWith(mLastConfiguration); final Rect touchDelegateBounds = new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(), mMargin + getLayoutHeight()); if (action == MotionEvent.ACTION_DOWN && touchDelegateBounds.contains(currentX, currentY)) { mIsDownInEnlargedTouchArea = true; } 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 WindowInsets onWindowInsetsApplied(WindowInsets insets) { final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect(); if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) { updateDisplaySizeWith(windowMetrics); updateLocationWith(mPosition); } final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect(); if (!imeInsetsRect.equals(mImeInsetsRect)) { if (isImeVisible(imeInsetsRect)) { mImeInsetsRect.set(imeInsetsRect); } else { mImeInsetsRect.setEmpty(); } updateLocationWith(mPosition); } return insets; } private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) { return (side == Alignment.RIGHT && currentRawX > downX) || (side == Alignment.LEFT && downX > currentRawX); } private boolean isImeVisible(Rect imeInsetsRect) { return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0 || imeInsetsRect.bottom != 0; } 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() { return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null")); } private void updateDimensions() { final Resources res = getResources(); updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics()); mMargin = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); mInset = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); mSquareScaledTouchSlop = sq(ViewConfiguration.get(getContext()).getScaledTouchSlop()); updateItemViewDimensionsWith(mSizeType); } private void updateDisplaySizeWith(WindowMetrics metrics) { final Rect displayBounds = metrics.getBounds(); final Insets displayInsets = getDisplayInsets(metrics); mDisplayInsetsRect.set(displayInsets.toRect()); displayBounds.inset(displayInsets); mDisplayWidth = displayBounds.width(); mDisplayHeight = displayBounds.height(); } private void updateItemViewDimensionsWith(@SizeType int sizeType) { final Resources res = getResources(); final int paddingResId = sizeType == SizeType.SMALL ? R.dimen.accessibility_floating_menu_small_padding : R.dimen.accessibility_floating_menu_large_padding; mPadding = res.getDimensionPixelSize(paddingResId); final int iconResId = sizeType == SizeType.SMALL ? R.dimen.accessibility_floating_menu_small_width_height : R.dimen.accessibility_floating_menu_large_width_height; mIconWidth = res.getDimensionPixelSize(iconResId); mIconHeight = mIconWidth; } private void updateItemViewWith(@SizeType int sizeType) { updateItemViewDimensionsWith(sizeType); mAdapter.setItemPadding(mPadding); mAdapter.setIconWidthHeight(mIconWidth); mAdapter.notifyDataSetChanged(); } private void initListView() { final Drawable background = getContext().getDrawable(R.drawable.accessibility_floating_menu_background); final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); 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()); mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) { @NonNull @Override public AccessibilityDelegateCompat getItemDelegate() { return new ItemDelegateCompat(this, AccessibilityFloatingMenuView.this); } }); updateListViewWith(mLastConfiguration); addView(mListView); } private void updateListViewWith(Configuration configuration) { updateMarginWith(configuration); final int elevation = getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); mListView.setElevation(elevation); } private WindowManager.LayoutParams createDefaultLayoutParams() { final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT); params.receiveInsetsIgnoringZOrder = true; params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; params.windowAnimations = android.R.style.Animation_Translucent; params.gravity = Gravity.START | Gravity.TOP; params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX(); // params.y = (int) (mPosition.getPercentageY() * getMaxWindowY()); final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY()); params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval()); updateAccessibilityTitle(params); return params; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mLastConfiguration.setTo(newConfig); final int diff = newConfig.diff(mLastConfiguration); if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) { updateAccessibilityTitle(mCurrentLayoutParams); } updateDimensions(); updateListViewWith(newConfig); updateItemViewWith(mSizeType); updateColor(); updateStrokeWith(newConfig.uiMode, mAlignment); updateLocationWith(mPosition); updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); updateScrollModeWith(hasExceededMaxLayoutHeight()); setSystemGestureExclusion(); } @VisibleForTesting 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 getMinWindowX() { return -getMarginStartEndWith(mLastConfiguration); } private int getMaxWindowX() { return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth(); } private int getMaxWindowY() { return mDisplayHeight - getWindowHeight(); } private InstantInsetLayerDrawable getMenuLayerDrawable() { return (InstantInsetLayerDrawable) mListView.getBackground(); } private GradientDrawable getMenuGradientDrawable() { return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM); } private Insets getDisplayInsets(WindowMetrics metrics) { return metrics.getWindowInsets().getInsetsIgnoringVisibility( systemBars() | displayCutout()); } /** * Updates the floating menu to be fixed at the side of the display. */ private void updateLocationWith(Position position) { final @Alignment int alignment = transformToAlignment(position.getPercentageX()); mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX(); final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY()); mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval()); mWindowManager.updateViewLayout(this, mCurrentLayoutParams); } /** * Gets the moving interval to not overlap between the keyboard and menu view. * * @return the moving interval if they overlap each other, otherwise 0. */ private int getInterval() { final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY()); final int imeY = mDisplayHeight - mImeInsetsRect.bottom; final int layoutBottomY = currentLayoutY + getWindowHeight(); return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0; } private void updateMarginWith(Configuration configuration) { // Avoid overlapping with system bars under landscape mode, update the margins of the menu // to align the edge of system bars. final int marginStartEnd = getMarginStartEndWith(configuration); final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams(); layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin); mListView.setLayoutParams(layoutParams); } 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 updateScrollModeWith(boolean hasExceededMaxLayoutHeight) { mListView.setOverScrollMode(hasExceededMaxLayoutHeight ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER); } private void updateColor() { final int menuColorResId = R.color.accessibility_floating_menu_background; getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId)); } 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 Resources res = getResources(); final int width = res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width); final int strokeWidth = isNightMode ? width : 0; final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark); getMenuGradientDrawable().setStroke(strokeWidth, strokeColor); } 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 updateAccessibilityTitle(WindowManager.LayoutParams params) { params.accessibilityTitle = getResources().getString( com.android.internal.R.string.accessibility_select_shortcut_menu_title); } 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); } @VisibleForTesting boolean hasExceededMaxLayoutHeight() { return calculateActualLayoutHeight() > getMaxLayoutHeight(); } @Alignment private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) { return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT; } private float transformCurrentPercentageXToEdge() { final float percentageX = calculateCurrentPercentageX(); return (percentageX < 0.5) ? 0.0f : 1.0f; } private float calculateCurrentPercentageX() { return mCurrentLayoutParams.x / (float) getMaxWindowX(); } private float calculateCurrentPercentageY() { return mCurrentLayoutParams.y / (float) getMaxWindowY(); } private int calculateActualLayoutHeight() { return (mPadding + mIconHeight) * mTargets.size() + mPadding; } private int getMarginStartEndWith(Configuration configuration) { return configuration != null && configuration.orientation == ORIENTATION_PORTRAIT ? mMargin : 0; } private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) { return sizeType == SizeType.SMALL ? getSmallSizeResIdWith(itemCount) : getLargeSizeResIdWith(itemCount); } private int getSmallSizeResIdWith(int itemCount) { return itemCount > 1 ? R.dimen.accessibility_floating_menu_small_multiple_radius : R.dimen.accessibility_floating_menu_small_single_radius; } private int getLargeSizeResIdWith(int itemCount) { return itemCount > 1 ? R.dimen.accessibility_floating_menu_large_multiple_radius : R.dimen.accessibility_floating_menu_large_single_radius; } @VisibleForTesting Rect getAvailableBounds() { return new Rect(0, 0, mDisplayWidth - getWindowWidth(), mDisplayHeight - getWindowHeight()); } private int getMaxLayoutHeight() { return mDisplayHeight - mMargin * 2; } private int getLayoutWidth() { return mPadding * 2 + mIconWidth; } private int getLayoutHeight() { return Math.min(getMaxLayoutHeight(), calculateActualLayoutHeight()); } private int getWindowWidth() { return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth(); } private int getWindowHeight() { return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight()); } private void setSystemGestureExclusion() { final Rect excludeZone = new Rect(0, 0, getWindowWidth(), getWindowHeight()); post(() -> setSystemGestureExclusionRects( mIsShowing ? Collections.singletonList(excludeZone) : Collections.emptyList())); } }