blob: 17818cd9c7ee0ffaabbeca6eb053b8d40b58728b [file] [log] [blame]
/*
* 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.ime;
import static android.view.WindowInsets.Type.navigationBars;
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.util.DisplayMetrics;
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.
*
* <p>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;
private boolean mImeVisibility;
@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;
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 final Position mPosition;
private float mSquareScaledTouchSlop;
private final Configuration mLastConfiguration;
private Optional<OnDragEndListener> 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<AccessibilityTarget> 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<AccessibilityTarget> 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 boolean currentImeVisibility = insets.isVisible(ime());
if (currentImeVisibility != mImeVisibility) {
mImeVisibility = currentImeVisibility;
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 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();
final DisplayMetrics dm = res.getDisplayMetrics();
mScreenWidth = dm.widthPixels;
mScreenHeight = dm.heightPixels;
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 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.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 mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
}
private int getMaxWindowY() {
return mScreenHeight - getWindowHeight();
}
private InstantInsetLayerDrawable getMenuLayerDrawable() {
return (InstantInsetLayerDrawable) mListView.getBackground();
}
private GradientDrawable getMenuGradientDrawable() {
return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
}
/**
* Updates the floating menu to be fixed at the side of the screen.
*/
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() {
if (!mImeVisibility) {
return 0;
}
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
ime() | navigationBars());
final int imeY = mScreenHeight - imeInsets.bottom;
final int layoutBottomY = mCurrentLayoutParams.y + 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, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight());
}
private int getMaxLayoutHeight() {
return mScreenHeight - 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(mScreenHeight, mMargin * 2 + getLayoutHeight());
}
private void setSystemGestureExclusion() {
final Rect excludeZone =
new Rect(0, 0, getWindowWidth(), getWindowHeight());
post(() -> setSystemGestureExclusionRects(
mIsShowing
? Collections.singletonList(excludeZone)
: Collections.emptyList()));
}
}