• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility.floatingmenu;
18 
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 import static android.util.MathUtils.constrain;
21 import static android.util.MathUtils.sq;
22 import static android.view.WindowInsets.Type.ime;
23 import static android.view.WindowInsets.Type.navigationBars;
24 
25 import static java.util.Objects.requireNonNull;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.ValueAnimator;
30 import android.annotation.FloatRange;
31 import android.annotation.IntDef;
32 import android.content.Context;
33 import android.content.pm.ActivityInfo;
34 import android.content.res.Configuration;
35 import android.content.res.Resources;
36 import android.graphics.Insets;
37 import android.graphics.PixelFormat;
38 import android.graphics.Rect;
39 import android.graphics.drawable.Drawable;
40 import android.graphics.drawable.GradientDrawable;
41 import android.graphics.drawable.LayerDrawable;
42 import android.os.Handler;
43 import android.os.Looper;
44 import android.util.DisplayMetrics;
45 import android.view.Gravity;
46 import android.view.MotionEvent;
47 import android.view.ViewConfiguration;
48 import android.view.ViewGroup;
49 import android.view.WindowInsets;
50 import android.view.WindowManager;
51 import android.view.WindowMetrics;
52 import android.view.animation.Animation;
53 import android.view.animation.OvershootInterpolator;
54 import android.view.animation.TranslateAnimation;
55 import android.widget.FrameLayout;
56 
57 import androidx.annotation.DimenRes;
58 import androidx.annotation.NonNull;
59 import androidx.core.view.AccessibilityDelegateCompat;
60 import androidx.recyclerview.widget.LinearLayoutManager;
61 import androidx.recyclerview.widget.RecyclerView;
62 import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
63 
64 import com.android.internal.accessibility.dialog.AccessibilityTarget;
65 import com.android.internal.annotations.VisibleForTesting;
66 import com.android.systemui.R;
67 
68 import java.lang.annotation.Retention;
69 import java.lang.annotation.RetentionPolicy;
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.List;
73 import java.util.Optional;
74 
75 /**
76  * Accessibility floating menu is used for the actions of accessibility features, it's also the
77  * action set.
78  *
79  * <p>The number of items would depend on strings key
80  * {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
81  */
82 public class AccessibilityFloatingMenuView extends FrameLayout
83         implements RecyclerView.OnItemTouchListener {
84     private static final int INDEX_MENU_ITEM = 0;
85     private static final int FADE_OUT_DURATION_MS = 1000;
86     private static final int FADE_EFFECT_DURATION_MS = 3000;
87     private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
88     private static final int MIN_WINDOW_Y = 0;
89 
90     private static final int ANIMATION_START_OFFSET = 600;
91     private static final int ANIMATION_DURATION_MS = 600;
92     private static final float ANIMATION_TO_X_VALUE = 0.5f;
93 
94     private boolean mIsFadeEffectEnabled;
95     private boolean mIsShowing;
96     private boolean mIsDownInEnlargedTouchArea;
97     private boolean mIsDragging = false;
98     private boolean mImeVisibility;
99     @Alignment
100     private int mAlignment;
101     @SizeType
102     private int mSizeType = SizeType.SMALL;
103     @VisibleForTesting
104     @ShapeType
105     int mShapeType = ShapeType.OVAL;
106     private int mTemporaryShapeType;
107     @RadiusType
108     private int mRadiusType;
109     private int mMargin;
110     private int mPadding;
111     private int mScreenHeight;
112     private int mScreenWidth;
113     private int mIconWidth;
114     private int mIconHeight;
115     private int mInset;
116     private int mDownX;
117     private int mDownY;
118     private int mRelativeToPointerDownX;
119     private int mRelativeToPointerDownY;
120     private float mRadius;
121     private final Position mPosition;
122     private float mSquareScaledTouchSlop;
123     private final Configuration mLastConfiguration;
124     private Optional<OnDragEndListener> mOnDragEndListener = Optional.empty();
125     private final RecyclerView mListView;
126     private final AccessibilityTargetAdapter mAdapter;
127     private float mFadeOutValue;
128     private final ValueAnimator mFadeOutAnimator;
129     @VisibleForTesting
130     final ValueAnimator mDragAnimator;
131     private final Handler mUiHandler;
132     @VisibleForTesting
133     final WindowManager.LayoutParams mCurrentLayoutParams;
134     private final WindowManager mWindowManager;
135     private final List<AccessibilityTarget> mTargets = new ArrayList<>();
136 
137     @IntDef({
138             SizeType.SMALL,
139             SizeType.LARGE
140     })
141     @Retention(RetentionPolicy.SOURCE)
142     @interface SizeType {
143         int SMALL = 0;
144         int LARGE = 1;
145     }
146 
147     @IntDef({
148             ShapeType.OVAL,
149             ShapeType.HALF_OVAL
150     })
151     @Retention(RetentionPolicy.SOURCE)
152     @interface ShapeType {
153         int OVAL = 0;
154         int HALF_OVAL = 1;
155     }
156 
157     @IntDef({
158             RadiusType.LEFT_HALF_OVAL,
159             RadiusType.OVAL,
160             RadiusType.RIGHT_HALF_OVAL
161     })
162     @Retention(RetentionPolicy.SOURCE)
163     @interface RadiusType {
164         int LEFT_HALF_OVAL = 0;
165         int OVAL = 1;
166         int RIGHT_HALF_OVAL = 2;
167     }
168 
169     @IntDef({
170             Alignment.LEFT,
171             Alignment.RIGHT
172     })
173     @Retention(RetentionPolicy.SOURCE)
174     @interface Alignment {
175         int LEFT = 0;
176         int RIGHT = 1;
177     }
178 
179     /**
180      * Interface for a callback to be invoked when the floating menu was dragging.
181      */
182     interface OnDragEndListener {
183 
184         /**
185          * Called when a drag is completed.
186          *
187          * @param position Stores information about the position
188          */
onDragEnd(Position position)189         void onDragEnd(Position position);
190     }
191 
AccessibilityFloatingMenuView(Context context, @NonNull Position position)192     public AccessibilityFloatingMenuView(Context context, @NonNull Position position) {
193         this(context, position, new RecyclerView(context));
194     }
195 
196     @VisibleForTesting
AccessibilityFloatingMenuView(Context context, @NonNull Position position, RecyclerView listView)197     AccessibilityFloatingMenuView(Context context, @NonNull Position position,
198             RecyclerView listView) {
199         super(context);
200 
201         mListView = listView;
202         mWindowManager = context.getSystemService(WindowManager.class);
203         mLastConfiguration = new Configuration(getResources().getConfiguration());
204         mAdapter = new AccessibilityTargetAdapter(mTargets);
205         mUiHandler = createUiHandler();
206         mPosition = position;
207         mAlignment = transformToAlignment(mPosition.getPercentageX());
208         mRadiusType = (mAlignment == Alignment.RIGHT)
209                 ? RadiusType.LEFT_HALF_OVAL
210                 : RadiusType.RIGHT_HALF_OVAL;
211 
212         updateDimensions();
213 
214         mCurrentLayoutParams = createDefaultLayoutParams();
215 
216         mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
217         mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
218         mFadeOutAnimator.addUpdateListener(
219                 (animation) -> setAlpha((float) animation.getAnimatedValue()));
220 
221         mDragAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
222         mDragAnimator.setDuration(SNAP_TO_LOCATION_DURATION_MS);
223         mDragAnimator.setInterpolator(new OvershootInterpolator());
224         mDragAnimator.addListener(new AnimatorListenerAdapter() {
225             @Override
226             public void onAnimationEnd(Animator animation) {
227                 mPosition.update(transformCurrentPercentageXToEdge(),
228                         calculateCurrentPercentageY());
229                 mAlignment = transformToAlignment(mPosition.getPercentageX());
230 
231                 updateLocationWith(mPosition);
232 
233                 updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
234 
235                 mRadiusType = (mAlignment == Alignment.RIGHT)
236                         ? RadiusType.LEFT_HALF_OVAL
237                         : RadiusType.RIGHT_HALF_OVAL;
238                 updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
239 
240                 fadeOut();
241 
242                 mOnDragEndListener.ifPresent(
243                         onDragEndListener -> onDragEndListener.onDragEnd(mPosition));
244             }
245         });
246 
247 
248         initListView();
249         updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
250     }
251 
252     @Override
onInterceptTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent event)253     public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
254             @NonNull MotionEvent event) {
255         final int currentRawX = (int) event.getRawX();
256         final int currentRawY = (int) event.getRawY();
257 
258         switch (event.getAction()) {
259             case MotionEvent.ACTION_DOWN:
260                 fadeIn();
261 
262                 mDownX = currentRawX;
263                 mDownY = currentRawY;
264                 mRelativeToPointerDownX = mCurrentLayoutParams.x - mDownX;
265                 mRelativeToPointerDownY = mCurrentLayoutParams.y - mDownY;
266                 mListView.animate().translationX(0);
267                 break;
268             case MotionEvent.ACTION_MOVE:
269                 if (mIsDragging
270                         || hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) {
271                     if (!mIsDragging) {
272                         mIsDragging = true;
273                         setRadius(mRadius, RadiusType.OVAL);
274                         setInset(0, 0);
275                     }
276 
277                     mTemporaryShapeType =
278                             isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX)
279                                     ? ShapeType.HALF_OVAL
280                                     : ShapeType.OVAL;
281                     final int newWindowX = currentRawX + mRelativeToPointerDownX;
282                     final int newWindowY = currentRawY + mRelativeToPointerDownY;
283                     mCurrentLayoutParams.x =
284                             constrain(newWindowX, getMinWindowX(), getMaxWindowX());
285                     mCurrentLayoutParams.y = constrain(newWindowY, MIN_WINDOW_Y, getMaxWindowY());
286                     mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
287                 }
288                 break;
289             case MotionEvent.ACTION_UP:
290             case MotionEvent.ACTION_CANCEL:
291                 if (mIsDragging) {
292                     mIsDragging = false;
293 
294                     final int minX = getMinWindowX();
295                     final int maxX = getMaxWindowX();
296                     final int endX = mCurrentLayoutParams.x > ((minX + maxX) / 2)
297                             ? maxX : minX;
298                     final int endY = mCurrentLayoutParams.y;
299                     snapToLocation(endX, endY);
300 
301                     setShapeType(mTemporaryShapeType);
302 
303                     // Avoid triggering the listener of the item.
304                     return true;
305                 }
306 
307                 // Must switch the oval shape type before tapping the corresponding item in the
308                 // list view, otherwise it can't work on it.
309                 if (!isOvalShape()) {
310                     setShapeType(ShapeType.OVAL);
311 
312                     return true;
313                 }
314 
315                 fadeOut();
316                 break;
317             default: // Do nothing
318         }
319 
320         // not consume all the events here because keeping the scroll behavior of list view.
321         return false;
322     }
323 
324     @Override
onTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent)325     public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
326         // Do Nothing
327     }
328 
329     @Override
onRequestDisallowInterceptTouchEvent(boolean b)330     public void onRequestDisallowInterceptTouchEvent(boolean b) {
331         // Do Nothing
332     }
333 
show()334     void show() {
335         if (isShowing()) {
336             return;
337         }
338 
339         mIsShowing = true;
340         mWindowManager.addView(this, mCurrentLayoutParams);
341 
342         setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
343         setSystemGestureExclusion();
344     }
345 
hide()346     void hide() {
347         if (!isShowing()) {
348             return;
349         }
350 
351         mIsShowing = false;
352         mWindowManager.removeView(this);
353 
354         setOnApplyWindowInsetsListener(null);
355         setSystemGestureExclusion();
356     }
357 
isShowing()358     boolean isShowing() {
359         return mIsShowing;
360     }
361 
isOvalShape()362     boolean isOvalShape() {
363         return mShapeType == ShapeType.OVAL;
364     }
365 
onTargetsChanged(List<AccessibilityTarget> newTargets)366     void onTargetsChanged(List<AccessibilityTarget> newTargets) {
367         fadeIn();
368 
369         mTargets.clear();
370         mTargets.addAll(newTargets);
371         onEnabledFeaturesChanged();
372 
373         updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
374         updateScrollModeWith(hasExceededMaxLayoutHeight());
375         setSystemGestureExclusion();
376 
377         fadeOut();
378     }
379 
setSizeType(@izeType int newSizeType)380     void setSizeType(@SizeType int newSizeType) {
381         fadeIn();
382 
383         mSizeType = newSizeType;
384 
385         updateItemViewWith(newSizeType);
386         updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
387 
388         // When the icon sized changed, the menu size and location will be impacted.
389         updateLocationWith(mPosition);
390         updateScrollModeWith(hasExceededMaxLayoutHeight());
391         updateOffsetWith(mShapeType, mAlignment);
392         setSystemGestureExclusion();
393 
394         fadeOut();
395     }
396 
setShapeType(@hapeType int newShapeType)397     void setShapeType(@ShapeType int newShapeType) {
398         fadeIn();
399 
400         mShapeType = newShapeType;
401 
402         updateOffsetWith(newShapeType, mAlignment);
403 
404         setOnTouchListener(
405                 newShapeType == ShapeType.OVAL
406                         ? null
407                         : (view, event) -> onTouched(event));
408 
409         fadeOut();
410     }
411 
setOnDragEndListener(OnDragEndListener onDragEndListener)412     public void setOnDragEndListener(OnDragEndListener onDragEndListener) {
413         mOnDragEndListener = Optional.ofNullable(onDragEndListener);
414     }
415 
startTranslateXAnimation()416     void startTranslateXAnimation() {
417         fadeIn();
418 
419         final float toXValue = (mAlignment == Alignment.RIGHT)
420                 ? ANIMATION_TO_X_VALUE
421                 : -ANIMATION_TO_X_VALUE;
422         final TranslateAnimation animation =
423                 new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
424                         Animation.RELATIVE_TO_SELF, toXValue,
425                         Animation.RELATIVE_TO_SELF, 0,
426                         Animation.RELATIVE_TO_SELF, 0);
427         animation.setDuration(ANIMATION_DURATION_MS);
428         animation.setRepeatMode(Animation.REVERSE);
429         animation.setInterpolator(new OvershootInterpolator());
430         animation.setRepeatCount(Animation.INFINITE);
431         animation.setStartOffset(ANIMATION_START_OFFSET);
432         mListView.startAnimation(animation);
433     }
434 
stopTranslateXAnimation()435     void stopTranslateXAnimation() {
436         mListView.clearAnimation();
437 
438         fadeOut();
439     }
440 
getWindowLocationOnScreen()441     Rect getWindowLocationOnScreen() {
442         final int left = mCurrentLayoutParams.x;
443         final int top = mCurrentLayoutParams.y;
444         return new Rect(left, top, left + getWindowWidth(), top + getWindowHeight());
445     }
446 
updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue)447     void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
448         mIsFadeEffectEnabled = isFadeEffectEnabled;
449         mFadeOutValue = newOpacityValue;
450 
451         mFadeOutAnimator.cancel();
452         mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue);
453         setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f);
454     }
455 
onEnabledFeaturesChanged()456     void onEnabledFeaturesChanged() {
457         mAdapter.notifyDataSetChanged();
458     }
459 
460     @VisibleForTesting
fadeIn()461     void fadeIn() {
462         if (!mIsFadeEffectEnabled) {
463             return;
464         }
465 
466         mFadeOutAnimator.cancel();
467         mUiHandler.removeCallbacksAndMessages(null);
468         mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f));
469     }
470 
471     @VisibleForTesting
fadeOut()472     void fadeOut() {
473         if (!mIsFadeEffectEnabled) {
474             return;
475         }
476 
477         mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS);
478     }
479 
onTouched(MotionEvent event)480     private boolean onTouched(MotionEvent event) {
481         final int action = event.getAction();
482         final int currentX = (int) event.getX();
483         final int currentY = (int) event.getY();
484 
485         final int marginStartEnd = getMarginStartEndWith(mLastConfiguration);
486         final Rect touchDelegateBounds =
487                 new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(),
488                         mMargin + getLayoutHeight());
489         if (action == MotionEvent.ACTION_DOWN
490                 && touchDelegateBounds.contains(currentX, currentY)) {
491             mIsDownInEnlargedTouchArea = true;
492         }
493 
494         if (!mIsDownInEnlargedTouchArea) {
495             return false;
496         }
497 
498         if (action == MotionEvent.ACTION_UP
499                 || action == MotionEvent.ACTION_CANCEL) {
500             mIsDownInEnlargedTouchArea = false;
501         }
502 
503         // In order to correspond to the correct item of list view.
504         event.setLocation(currentX - mMargin, currentY - mMargin);
505         return mListView.dispatchTouchEvent(event);
506     }
507 
onWindowInsetsApplied(WindowInsets insets)508     private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
509         final boolean currentImeVisibility = insets.isVisible(ime());
510         if (currentImeVisibility != mImeVisibility) {
511             mImeVisibility = currentImeVisibility;
512             updateLocationWith(mPosition);
513         }
514 
515         return insets;
516     }
517 
isMovingTowardsScreenEdge(@lignment int side, int currentRawX, int downX)518     private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
519         return (side == Alignment.RIGHT && currentRawX > downX)
520                 || (side == Alignment.LEFT && downX > currentRawX);
521     }
522 
hasExceededTouchSlop(int startX, int startY, int endX, int endY)523     private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
524         return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
525     }
526 
setRadius(float radius, @RadiusType int type)527     private void setRadius(float radius, @RadiusType int type) {
528         getMenuGradientDrawable().setCornerRadii(createRadii(radius, type));
529     }
530 
createRadii(float radius, @RadiusType int type)531     private float[] createRadii(float radius, @RadiusType int type) {
532         if (type == RadiusType.LEFT_HALF_OVAL) {
533             return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
534         }
535 
536         if (type == RadiusType.RIGHT_HALF_OVAL) {
537             return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
538         }
539 
540         return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
541     }
542 
createUiHandler()543     private Handler createUiHandler() {
544         return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
545     }
546 
updateDimensions()547     private void updateDimensions() {
548         final Resources res = getResources();
549         final DisplayMetrics dm = res.getDisplayMetrics();
550         mScreenWidth = dm.widthPixels;
551         mScreenHeight = dm.heightPixels;
552         mMargin =
553                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
554         mInset =
555                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
556 
557         mSquareScaledTouchSlop =
558                 sq(ViewConfiguration.get(getContext()).getScaledTouchSlop());
559 
560         updateItemViewDimensionsWith(mSizeType);
561     }
562 
updateItemViewDimensionsWith(@izeType int sizeType)563     private void updateItemViewDimensionsWith(@SizeType int sizeType) {
564         final Resources res = getResources();
565         final int paddingResId =
566                 sizeType == SizeType.SMALL
567                         ? R.dimen.accessibility_floating_menu_small_padding
568                         : R.dimen.accessibility_floating_menu_large_padding;
569         mPadding = res.getDimensionPixelSize(paddingResId);
570 
571         final int iconResId =
572                 sizeType == SizeType.SMALL
573                         ? R.dimen.accessibility_floating_menu_small_width_height
574                         : R.dimen.accessibility_floating_menu_large_width_height;
575         mIconWidth = res.getDimensionPixelSize(iconResId);
576         mIconHeight = mIconWidth;
577     }
578 
updateItemViewWith(@izeType int sizeType)579     private void updateItemViewWith(@SizeType int sizeType) {
580         updateItemViewDimensionsWith(sizeType);
581 
582         mAdapter.setItemPadding(mPadding);
583         mAdapter.setIconWidthHeight(mIconWidth);
584         mAdapter.notifyDataSetChanged();
585     }
586 
initListView()587     private void initListView() {
588         final Drawable background =
589                 getContext().getDrawable(R.drawable.accessibility_floating_menu_background);
590         final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
591         final LayoutParams layoutParams =
592                 new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
593                         ViewGroup.LayoutParams.WRAP_CONTENT);
594         mListView.setLayoutParams(layoutParams);
595         final InstantInsetLayerDrawable layerDrawable =
596                 new InstantInsetLayerDrawable(new Drawable[]{background});
597         mListView.setBackground(layerDrawable);
598         mListView.setAdapter(mAdapter);
599         mListView.setLayoutManager(layoutManager);
600         mListView.addOnItemTouchListener(this);
601         mListView.animate().setInterpolator(new OvershootInterpolator());
602         mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) {
603             @NonNull
604             @Override
605             public AccessibilityDelegateCompat getItemDelegate() {
606                 return new ItemDelegateCompat(this,
607                         AccessibilityFloatingMenuView.this);
608             }
609         });
610 
611         updateListViewWith(mLastConfiguration);
612 
613         addView(mListView);
614     }
615 
updateListViewWith(Configuration configuration)616     private void updateListViewWith(Configuration configuration) {
617         updateMarginWith(configuration);
618 
619         final int elevation =
620                 getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
621         mListView.setElevation(elevation);
622     }
623 
createDefaultLayoutParams()624     private WindowManager.LayoutParams createDefaultLayoutParams() {
625         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
626                 WindowManager.LayoutParams.WRAP_CONTENT,
627                 WindowManager.LayoutParams.WRAP_CONTENT,
628                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
629                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
630                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
631                 PixelFormat.TRANSLUCENT);
632         params.receiveInsetsIgnoringZOrder = true;
633         params.windowAnimations = android.R.style.Animation_Translucent;
634         params.gravity = Gravity.START | Gravity.TOP;
635         params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
636 //        params.y = (int) (mPosition.getPercentageY() * getMaxWindowY());
637         final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
638         params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
639         updateAccessibilityTitle(params);
640         return params;
641     }
642 
643     @Override
onConfigurationChanged(Configuration newConfig)644     protected void onConfigurationChanged(Configuration newConfig) {
645         super.onConfigurationChanged(newConfig);
646         mLastConfiguration.setTo(newConfig);
647 
648         final int diff = newConfig.diff(mLastConfiguration);
649         if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) {
650             updateAccessibilityTitle(mCurrentLayoutParams);
651         }
652 
653         updateDimensions();
654         updateListViewWith(newConfig);
655         updateItemViewWith(mSizeType);
656         updateColor();
657         updateStrokeWith(newConfig.uiMode, mAlignment);
658         updateLocationWith(mPosition);
659         updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
660         updateScrollModeWith(hasExceededMaxLayoutHeight());
661         setSystemGestureExclusion();
662     }
663 
664     @VisibleForTesting
snapToLocation(int endX, int endY)665     void snapToLocation(int endX, int endY) {
666         mDragAnimator.cancel();
667         mDragAnimator.removeAllUpdateListeners();
668         mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY));
669         mDragAnimator.start();
670     }
671 
onDragAnimationUpdate(ValueAnimator animator, int endX, int endY)672     private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) {
673         float value = (float) animator.getAnimatedValue();
674         final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX));
675         final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY));
676 
677         mCurrentLayoutParams.x = newX;
678         mCurrentLayoutParams.y = newY;
679         mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
680     }
681 
getMinWindowX()682     private int getMinWindowX() {
683         return -getMarginStartEndWith(mLastConfiguration);
684     }
685 
getMaxWindowX()686     private int getMaxWindowX() {
687         return mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
688     }
689 
getMaxWindowY()690     private int getMaxWindowY() {
691         return mScreenHeight - getWindowHeight();
692     }
693 
getMenuLayerDrawable()694     private InstantInsetLayerDrawable getMenuLayerDrawable() {
695         return (InstantInsetLayerDrawable) mListView.getBackground();
696     }
697 
getMenuGradientDrawable()698     private GradientDrawable getMenuGradientDrawable() {
699         return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
700     }
701 
702     /**
703      * Updates the floating menu to be fixed at the side of the screen.
704      */
updateLocationWith(Position position)705     private void updateLocationWith(Position position) {
706         final @Alignment int alignment = transformToAlignment(position.getPercentageX());
707         mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
708         final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY());
709         mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
710         mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
711     }
712 
713     /**
714      * Gets the moving interval to not overlap between the keyboard and menu view.
715      *
716      * @return the moving interval if they overlap each other, otherwise 0.
717      */
getInterval()718     private int getInterval() {
719         if (!mImeVisibility) {
720             return 0;
721         }
722 
723         final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
724         final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
725                 ime() | navigationBars());
726         final int imeY = mScreenHeight - imeInsets.bottom;
727         final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight();
728 
729         return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
730     }
731 
updateMarginWith(Configuration configuration)732     private void updateMarginWith(Configuration configuration) {
733         // Avoid overlapping with system bars under landscape mode, update the margins of the menu
734         // to align the edge of system bars.
735         final int marginStartEnd = getMarginStartEndWith(configuration);
736         final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams();
737         layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin);
738         mListView.setLayoutParams(layoutParams);
739     }
740 
updateOffsetWith(@hapeType int shapeType, @Alignment int side)741     private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
742         final float halfWidth = getLayoutWidth() / 2.0f;
743         final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
744         mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset);
745     }
746 
updateScrollModeWith(boolean hasExceededMaxLayoutHeight)747     private void updateScrollModeWith(boolean hasExceededMaxLayoutHeight) {
748         mListView.setOverScrollMode(hasExceededMaxLayoutHeight
749                 ? OVER_SCROLL_ALWAYS
750                 : OVER_SCROLL_NEVER);
751     }
752 
updateColor()753     private void updateColor() {
754         final int menuColorResId = R.color.accessibility_floating_menu_background;
755         getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId));
756     }
757 
updateStrokeWith(int uiMode, @Alignment int side)758     private void updateStrokeWith(int uiMode, @Alignment int side) {
759         updateInsetWith(uiMode, side);
760 
761         final boolean isNightMode =
762                 (uiMode & Configuration.UI_MODE_NIGHT_MASK)
763                         == Configuration.UI_MODE_NIGHT_YES;
764         final Resources res = getResources();
765         final int width =
766                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width);
767         final int strokeWidth = isNightMode ? width : 0;
768         final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark);
769         getMenuGradientDrawable().setStroke(strokeWidth, strokeColor);
770     }
771 
updateRadiusWith(@izeType int sizeType, @RadiusType int radiusType, int itemCount)772     private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType,
773             int itemCount) {
774         mRadius =
775                 getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount));
776         setRadius(mRadius, radiusType);
777     }
778 
updateInsetWith(int uiMode, @Alignment int side)779     private void updateInsetWith(int uiMode, @Alignment int side) {
780         final boolean isNightMode =
781                 (uiMode & Configuration.UI_MODE_NIGHT_MASK)
782                         == Configuration.UI_MODE_NIGHT_YES;
783 
784         final int layerInset = isNightMode ? mInset : 0;
785         final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0;
786         final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0;
787         setInset(insetLeft, insetRight);
788     }
789 
updateAccessibilityTitle(WindowManager.LayoutParams params)790     private void updateAccessibilityTitle(WindowManager.LayoutParams params) {
791         params.accessibilityTitle = getResources().getString(
792                 com.android.internal.R.string.accessibility_select_shortcut_menu_title);
793     }
794 
setInset(int left, int right)795     private void setInset(int left, int right) {
796         final LayerDrawable layerDrawable = getMenuLayerDrawable();
797         if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left
798                 && layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) {
799             return;
800         }
801 
802         layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0);
803     }
804 
805     @VisibleForTesting
hasExceededMaxLayoutHeight()806     boolean hasExceededMaxLayoutHeight() {
807         return calculateActualLayoutHeight() > getMaxLayoutHeight();
808     }
809 
810     @Alignment
transformToAlignment(@loatRangefrom = 0.0, to = 1.0) float percentageX)811     private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) {
812         return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT;
813     }
814 
transformCurrentPercentageXToEdge()815     private float transformCurrentPercentageXToEdge() {
816         final float percentageX = calculateCurrentPercentageX();
817         return (percentageX < 0.5) ? 0.0f : 1.0f;
818     }
819 
calculateCurrentPercentageX()820     private float calculateCurrentPercentageX() {
821         return mCurrentLayoutParams.x / (float) getMaxWindowX();
822     }
823 
calculateCurrentPercentageY()824     private float calculateCurrentPercentageY() {
825         return mCurrentLayoutParams.y / (float) getMaxWindowY();
826     }
827 
calculateActualLayoutHeight()828     private int calculateActualLayoutHeight() {
829         return (mPadding + mIconHeight) * mTargets.size() + mPadding;
830     }
831 
getMarginStartEndWith(Configuration configuration)832     private int getMarginStartEndWith(Configuration configuration) {
833         return configuration != null
834                 && configuration.orientation == ORIENTATION_PORTRAIT
835                 ? mMargin : 0;
836     }
837 
getRadiusResId(@izeType int sizeType, int itemCount)838     private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) {
839         return sizeType == SizeType.SMALL
840                 ? getSmallSizeResIdWith(itemCount)
841                 : getLargeSizeResIdWith(itemCount);
842     }
843 
getSmallSizeResIdWith(int itemCount)844     private int getSmallSizeResIdWith(int itemCount) {
845         return itemCount > 1
846                 ? R.dimen.accessibility_floating_menu_small_multiple_radius
847                 : R.dimen.accessibility_floating_menu_small_single_radius;
848     }
849 
getLargeSizeResIdWith(int itemCount)850     private int getLargeSizeResIdWith(int itemCount) {
851         return itemCount > 1
852                 ? R.dimen.accessibility_floating_menu_large_multiple_radius
853                 : R.dimen.accessibility_floating_menu_large_single_radius;
854     }
855 
856     @VisibleForTesting
getAvailableBounds()857     Rect getAvailableBounds() {
858         return new Rect(0, 0, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight());
859     }
860 
getMaxLayoutHeight()861     private int getMaxLayoutHeight() {
862         return mScreenHeight - mMargin * 2;
863     }
864 
getLayoutWidth()865     private int getLayoutWidth() {
866         return mPadding * 2 + mIconWidth;
867     }
868 
getLayoutHeight()869     private int getLayoutHeight() {
870         return Math.min(getMaxLayoutHeight(), calculateActualLayoutHeight());
871     }
872 
getWindowWidth()873     private int getWindowWidth() {
874         return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth();
875     }
876 
getWindowHeight()877     private int getWindowHeight() {
878         return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
879     }
880 
setSystemGestureExclusion()881     private void setSystemGestureExclusion() {
882         final Rect excludeZone =
883                 new Rect(0, 0, getWindowWidth(), getWindowHeight());
884         post(() -> setSystemGestureExclusionRects(
885                 mIsShowing
886                         ? Collections.singletonList(excludeZone)
887                         : Collections.emptyList()));
888     }
889 }
890