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