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 mWindowManager.removeView(this); 357 358 setOnApplyWindowInsetsListener(null); 359 setSystemGestureExclusion(); 360 } 361 isShowing()362 boolean isShowing() { 363 return mIsShowing; 364 } 365 isOvalShape()366 boolean isOvalShape() { 367 return mShapeType == ShapeType.OVAL; 368 } 369 onTargetsChanged(List<AccessibilityTarget> newTargets)370 void onTargetsChanged(List<AccessibilityTarget> newTargets) { 371 fadeIn(); 372 373 mTargets.clear(); 374 mTargets.addAll(newTargets); 375 onEnabledFeaturesChanged(); 376 377 updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); 378 updateScrollModeWith(hasExceededMaxLayoutHeight()); 379 setSystemGestureExclusion(); 380 381 fadeOut(); 382 } 383 setSizeType(@izeType int newSizeType)384 void setSizeType(@SizeType int newSizeType) { 385 fadeIn(); 386 387 mSizeType = newSizeType; 388 389 updateItemViewWith(newSizeType); 390 updateRadiusWith(newSizeType, mRadiusType, mTargets.size()); 391 392 // When the icon sized changed, the menu size and location will be impacted. 393 updateLocationWith(mPosition); 394 updateScrollModeWith(hasExceededMaxLayoutHeight()); 395 updateOffsetWith(mShapeType, mAlignment); 396 setSystemGestureExclusion(); 397 398 fadeOut(); 399 } 400 setShapeType(@hapeType int newShapeType)401 void setShapeType(@ShapeType int newShapeType) { 402 fadeIn(); 403 404 mShapeType = newShapeType; 405 406 updateOffsetWith(newShapeType, mAlignment); 407 408 setOnTouchListener( 409 newShapeType == ShapeType.OVAL 410 ? null 411 : (view, event) -> onTouched(event)); 412 413 fadeOut(); 414 } 415 setOnDragEndListener(OnDragEndListener onDragEndListener)416 public void setOnDragEndListener(OnDragEndListener onDragEndListener) { 417 mOnDragEndListener = Optional.ofNullable(onDragEndListener); 418 } 419 startTranslateXAnimation()420 void startTranslateXAnimation() { 421 fadeIn(); 422 423 final float toXValue = (mAlignment == Alignment.RIGHT) 424 ? ANIMATION_TO_X_VALUE 425 : -ANIMATION_TO_X_VALUE; 426 final TranslateAnimation animation = 427 new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, 428 Animation.RELATIVE_TO_SELF, toXValue, 429 Animation.RELATIVE_TO_SELF, 0, 430 Animation.RELATIVE_TO_SELF, 0); 431 animation.setDuration(ANIMATION_DURATION_MS); 432 animation.setRepeatMode(Animation.REVERSE); 433 animation.setInterpolator(new OvershootInterpolator()); 434 animation.setRepeatCount(Animation.INFINITE); 435 animation.setStartOffset(ANIMATION_START_OFFSET); 436 mListView.startAnimation(animation); 437 } 438 stopTranslateXAnimation()439 void stopTranslateXAnimation() { 440 mListView.clearAnimation(); 441 442 fadeOut(); 443 } 444 getWindowLocationOnScreen()445 Rect getWindowLocationOnScreen() { 446 final int left = mCurrentLayoutParams.x; 447 final int top = mCurrentLayoutParams.y; 448 return new Rect(left, top, left + getWindowWidth(), top + getWindowHeight()); 449 } 450 updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue)451 void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) { 452 mIsFadeEffectEnabled = isFadeEffectEnabled; 453 mFadeOutValue = newOpacityValue; 454 455 mFadeOutAnimator.cancel(); 456 mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue); 457 setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f); 458 } 459 onEnabledFeaturesChanged()460 void onEnabledFeaturesChanged() { 461 mAdapter.notifyDataSetChanged(); 462 } 463 464 @VisibleForTesting fadeIn()465 void fadeIn() { 466 if (!mIsFadeEffectEnabled) { 467 return; 468 } 469 470 mFadeOutAnimator.cancel(); 471 mUiHandler.removeCallbacksAndMessages(null); 472 mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f)); 473 } 474 475 @VisibleForTesting fadeOut()476 void fadeOut() { 477 if (!mIsFadeEffectEnabled) { 478 return; 479 } 480 481 mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS); 482 } 483 onTouched(MotionEvent event)484 private boolean onTouched(MotionEvent event) { 485 final int action = event.getAction(); 486 final int currentX = (int) event.getX(); 487 final int currentY = (int) event.getY(); 488 489 final int marginStartEnd = getMarginStartEndWith(mLastConfiguration); 490 final Rect touchDelegateBounds = 491 new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(), 492 mMargin + getLayoutHeight()); 493 if (action == MotionEvent.ACTION_DOWN 494 && touchDelegateBounds.contains(currentX, currentY)) { 495 mIsDownInEnlargedTouchArea = true; 496 } 497 498 if (!mIsDownInEnlargedTouchArea) { 499 return false; 500 } 501 502 if (action == MotionEvent.ACTION_UP 503 || action == MotionEvent.ACTION_CANCEL) { 504 mIsDownInEnlargedTouchArea = false; 505 } 506 507 // In order to correspond to the correct item of list view. 508 event.setLocation(currentX - mMargin, currentY - mMargin); 509 return mListView.dispatchTouchEvent(event); 510 } 511 onWindowInsetsApplied(WindowInsets insets)512 private WindowInsets onWindowInsetsApplied(WindowInsets insets) { 513 final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); 514 final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect(); 515 if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) { 516 updateDisplaySizeWith(windowMetrics); 517 updateLocationWith(mPosition); 518 } 519 520 final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect(); 521 if (!imeInsetsRect.equals(mImeInsetsRect)) { 522 if (isImeVisible(imeInsetsRect)) { 523 mImeInsetsRect.set(imeInsetsRect); 524 } else { 525 mImeInsetsRect.setEmpty(); 526 } 527 528 updateLocationWith(mPosition); 529 } 530 531 return insets; 532 } 533 isMovingTowardsScreenEdge(@lignment int side, int currentRawX, int downX)534 private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) { 535 return (side == Alignment.RIGHT && currentRawX > downX) 536 || (side == Alignment.LEFT && downX > currentRawX); 537 } 538 isImeVisible(Rect imeInsetsRect)539 private boolean isImeVisible(Rect imeInsetsRect) { 540 return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0 541 || imeInsetsRect.bottom != 0; 542 } 543 hasExceededTouchSlop(int startX, int startY, int endX, int endY)544 private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) { 545 return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop; 546 } 547 setRadius(float radius, @RadiusType int type)548 private void setRadius(float radius, @RadiusType int type) { 549 getMenuGradientDrawable().setCornerRadii(createRadii(radius, type)); 550 } 551 createRadii(float radius, @RadiusType int type)552 private float[] createRadii(float radius, @RadiusType int type) { 553 if (type == RadiusType.LEFT_HALF_OVAL) { 554 return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; 555 } 556 557 if (type == RadiusType.RIGHT_HALF_OVAL) { 558 return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}; 559 } 560 561 return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; 562 } 563 createUiHandler()564 private Handler createUiHandler() { 565 return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null")); 566 } 567 updateDimensions()568 private void updateDimensions() { 569 final Resources res = getResources(); 570 571 updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics()); 572 573 mMargin = 574 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); 575 mInset = 576 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); 577 578 mSquareScaledTouchSlop = 579 sq(ViewConfiguration.get(getContext()).getScaledTouchSlop()); 580 581 updateItemViewDimensionsWith(mSizeType); 582 } 583 updateDisplaySizeWith(WindowMetrics metrics)584 private void updateDisplaySizeWith(WindowMetrics metrics) { 585 final Rect displayBounds = metrics.getBounds(); 586 final Insets displayInsets = getDisplayInsets(metrics); 587 mDisplayInsetsRect.set(displayInsets.toRect()); 588 displayBounds.inset(displayInsets); 589 mDisplayWidth = displayBounds.width(); 590 mDisplayHeight = displayBounds.height(); 591 } 592 updateItemViewDimensionsWith(@izeType int sizeType)593 private void updateItemViewDimensionsWith(@SizeType int sizeType) { 594 final Resources res = getResources(); 595 final int paddingResId = 596 sizeType == SizeType.SMALL 597 ? R.dimen.accessibility_floating_menu_small_padding 598 : R.dimen.accessibility_floating_menu_large_padding; 599 mPadding = res.getDimensionPixelSize(paddingResId); 600 601 final int iconResId = 602 sizeType == SizeType.SMALL 603 ? R.dimen.accessibility_floating_menu_small_width_height 604 : R.dimen.accessibility_floating_menu_large_width_height; 605 mIconWidth = res.getDimensionPixelSize(iconResId); 606 mIconHeight = mIconWidth; 607 } 608 updateItemViewWith(@izeType int sizeType)609 private void updateItemViewWith(@SizeType int sizeType) { 610 updateItemViewDimensionsWith(sizeType); 611 612 mAdapter.setItemPadding(mPadding); 613 mAdapter.setIconWidthHeight(mIconWidth); 614 mAdapter.notifyDataSetChanged(); 615 } 616 initListView()617 private void initListView() { 618 final Drawable background = 619 getContext().getDrawable(R.drawable.accessibility_floating_menu_background); 620 final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); 621 final LayoutParams layoutParams = 622 new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 623 ViewGroup.LayoutParams.WRAP_CONTENT); 624 mListView.setLayoutParams(layoutParams); 625 final InstantInsetLayerDrawable layerDrawable = 626 new InstantInsetLayerDrawable(new Drawable[]{background}); 627 mListView.setBackground(layerDrawable); 628 mListView.setAdapter(mAdapter); 629 mListView.setLayoutManager(layoutManager); 630 mListView.addOnItemTouchListener(this); 631 mListView.animate().setInterpolator(new OvershootInterpolator()); 632 mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) { 633 @NonNull 634 @Override 635 public AccessibilityDelegateCompat getItemDelegate() { 636 return new ItemDelegateCompat(this, 637 AccessibilityFloatingMenuView.this); 638 } 639 }); 640 641 updateListViewWith(mLastConfiguration); 642 643 addView(mListView); 644 } 645 updateListViewWith(Configuration configuration)646 private void updateListViewWith(Configuration configuration) { 647 updateMarginWith(configuration); 648 649 final int elevation = 650 getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); 651 mListView.setElevation(elevation); 652 } 653 createDefaultLayoutParams()654 private WindowManager.LayoutParams createDefaultLayoutParams() { 655 final WindowManager.LayoutParams params = new WindowManager.LayoutParams( 656 WindowManager.LayoutParams.WRAP_CONTENT, 657 WindowManager.LayoutParams.WRAP_CONTENT, 658 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 659 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 660 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 661 PixelFormat.TRANSLUCENT); 662 params.receiveInsetsIgnoringZOrder = true; 663 params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; 664 params.windowAnimations = android.R.style.Animation_Translucent; 665 params.gravity = Gravity.START | Gravity.TOP; 666 params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX(); 667 // params.y = (int) (mPosition.getPercentageY() * getMaxWindowY()); 668 final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY()); 669 params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval()); 670 updateAccessibilityTitle(params); 671 return params; 672 } 673 674 @Override onConfigurationChanged(Configuration newConfig)675 protected void onConfigurationChanged(Configuration newConfig) { 676 super.onConfigurationChanged(newConfig); 677 mLastConfiguration.setTo(newConfig); 678 679 final int diff = newConfig.diff(mLastConfiguration); 680 if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) { 681 updateAccessibilityTitle(mCurrentLayoutParams); 682 } 683 684 updateDimensions(); 685 updateListViewWith(newConfig); 686 updateItemViewWith(mSizeType); 687 updateColor(); 688 updateStrokeWith(newConfig.uiMode, mAlignment); 689 updateLocationWith(mPosition); 690 updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); 691 updateScrollModeWith(hasExceededMaxLayoutHeight()); 692 setSystemGestureExclusion(); 693 } 694 695 @VisibleForTesting snapToLocation(int endX, int endY)696 void snapToLocation(int endX, int endY) { 697 mDragAnimator.cancel(); 698 mDragAnimator.removeAllUpdateListeners(); 699 mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY)); 700 mDragAnimator.start(); 701 } 702 onDragAnimationUpdate(ValueAnimator animator, int endX, int endY)703 private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) { 704 float value = (float) animator.getAnimatedValue(); 705 final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX)); 706 final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY)); 707 708 mCurrentLayoutParams.x = newX; 709 mCurrentLayoutParams.y = newY; 710 mWindowManager.updateViewLayout(this, mCurrentLayoutParams); 711 } 712 getMinWindowX()713 private int getMinWindowX() { 714 return -getMarginStartEndWith(mLastConfiguration); 715 } 716 getMaxWindowX()717 private int getMaxWindowX() { 718 return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth(); 719 } 720 getMaxWindowY()721 private int getMaxWindowY() { 722 return mDisplayHeight - getWindowHeight(); 723 } 724 getMenuLayerDrawable()725 private InstantInsetLayerDrawable getMenuLayerDrawable() { 726 return (InstantInsetLayerDrawable) mListView.getBackground(); 727 } 728 getMenuGradientDrawable()729 private GradientDrawable getMenuGradientDrawable() { 730 return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM); 731 } 732 getDisplayInsets(WindowMetrics metrics)733 private Insets getDisplayInsets(WindowMetrics metrics) { 734 return metrics.getWindowInsets().getInsetsIgnoringVisibility( 735 systemBars() | displayCutout()); 736 } 737 738 /** 739 * Updates the floating menu to be fixed at the side of the display. 740 */ updateLocationWith(Position position)741 private void updateLocationWith(Position position) { 742 final @Alignment int alignment = transformToAlignment(position.getPercentageX()); 743 mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX(); 744 final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY()); 745 mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval()); 746 mWindowManager.updateViewLayout(this, mCurrentLayoutParams); 747 } 748 749 /** 750 * Gets the moving interval to not overlap between the keyboard and menu view. 751 * 752 * @return the moving interval if they overlap each other, otherwise 0. 753 */ getInterval()754 private int getInterval() { 755 final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY()); 756 final int imeY = mDisplayHeight - mImeInsetsRect.bottom; 757 final int layoutBottomY = currentLayoutY + getWindowHeight(); 758 759 return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0; 760 } 761 updateMarginWith(Configuration configuration)762 private void updateMarginWith(Configuration configuration) { 763 // Avoid overlapping with system bars under landscape mode, update the margins of the menu 764 // to align the edge of system bars. 765 final int marginStartEnd = getMarginStartEndWith(configuration); 766 final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams(); 767 layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin); 768 mListView.setLayoutParams(layoutParams); 769 } 770 updateOffsetWith(@hapeType int shapeType, @Alignment int side)771 private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) { 772 final float halfWidth = getLayoutWidth() / 2.0f; 773 final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth; 774 mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset); 775 } 776 updateScrollModeWith(boolean hasExceededMaxLayoutHeight)777 private void updateScrollModeWith(boolean hasExceededMaxLayoutHeight) { 778 mListView.setOverScrollMode(hasExceededMaxLayoutHeight 779 ? OVER_SCROLL_ALWAYS 780 : OVER_SCROLL_NEVER); 781 } 782 updateColor()783 private void updateColor() { 784 final int menuColorResId = R.color.accessibility_floating_menu_background; 785 getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId)); 786 } 787 updateStrokeWith(int uiMode, @Alignment int side)788 private void updateStrokeWith(int uiMode, @Alignment int side) { 789 updateInsetWith(uiMode, side); 790 791 final boolean isNightMode = 792 (uiMode & Configuration.UI_MODE_NIGHT_MASK) 793 == Configuration.UI_MODE_NIGHT_YES; 794 final Resources res = getResources(); 795 final int width = 796 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width); 797 final int strokeWidth = isNightMode ? width : 0; 798 final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark); 799 getMenuGradientDrawable().setStroke(strokeWidth, strokeColor); 800 } 801 updateRadiusWith(@izeType int sizeType, @RadiusType int radiusType, int itemCount)802 private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType, 803 int itemCount) { 804 mRadius = 805 getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount)); 806 setRadius(mRadius, radiusType); 807 } 808 updateInsetWith(int uiMode, @Alignment int side)809 private void updateInsetWith(int uiMode, @Alignment int side) { 810 final boolean isNightMode = 811 (uiMode & Configuration.UI_MODE_NIGHT_MASK) 812 == Configuration.UI_MODE_NIGHT_YES; 813 814 final int layerInset = isNightMode ? mInset : 0; 815 final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0; 816 final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0; 817 setInset(insetLeft, insetRight); 818 } 819 updateAccessibilityTitle(WindowManager.LayoutParams params)820 private void updateAccessibilityTitle(WindowManager.LayoutParams params) { 821 params.accessibilityTitle = getResources().getString( 822 com.android.internal.R.string.accessibility_select_shortcut_menu_title); 823 } 824 setInset(int left, int right)825 private void setInset(int left, int right) { 826 final LayerDrawable layerDrawable = getMenuLayerDrawable(); 827 if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left 828 && layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) { 829 return; 830 } 831 832 layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0); 833 } 834 835 @VisibleForTesting hasExceededMaxLayoutHeight()836 boolean hasExceededMaxLayoutHeight() { 837 return calculateActualLayoutHeight() > getMaxLayoutHeight(); 838 } 839 840 @Alignment transformToAlignment(@loatRangefrom = 0.0, to = 1.0) float percentageX)841 private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) { 842 return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT; 843 } 844 transformCurrentPercentageXToEdge()845 private float transformCurrentPercentageXToEdge() { 846 final float percentageX = calculateCurrentPercentageX(); 847 return (percentageX < 0.5) ? 0.0f : 1.0f; 848 } 849 calculateCurrentPercentageX()850 private float calculateCurrentPercentageX() { 851 return mCurrentLayoutParams.x / (float) getMaxWindowX(); 852 } 853 calculateCurrentPercentageY()854 private float calculateCurrentPercentageY() { 855 return mCurrentLayoutParams.y / (float) getMaxWindowY(); 856 } 857 calculateActualLayoutHeight()858 private int calculateActualLayoutHeight() { 859 return (mPadding + mIconHeight) * mTargets.size() + mPadding; 860 } 861 getMarginStartEndWith(Configuration configuration)862 private int getMarginStartEndWith(Configuration configuration) { 863 return configuration != null 864 && configuration.orientation == ORIENTATION_PORTRAIT 865 ? mMargin : 0; 866 } 867 getRadiusResId(@izeType int sizeType, int itemCount)868 private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) { 869 return sizeType == SizeType.SMALL 870 ? getSmallSizeResIdWith(itemCount) 871 : getLargeSizeResIdWith(itemCount); 872 } 873 getSmallSizeResIdWith(int itemCount)874 private int getSmallSizeResIdWith(int itemCount) { 875 return itemCount > 1 876 ? R.dimen.accessibility_floating_menu_small_multiple_radius 877 : R.dimen.accessibility_floating_menu_small_single_radius; 878 } 879 getLargeSizeResIdWith(int itemCount)880 private int getLargeSizeResIdWith(int itemCount) { 881 return itemCount > 1 882 ? R.dimen.accessibility_floating_menu_large_multiple_radius 883 : R.dimen.accessibility_floating_menu_large_single_radius; 884 } 885 886 @VisibleForTesting getAvailableBounds()887 Rect getAvailableBounds() { 888 return new Rect(0, 0, mDisplayWidth - getWindowWidth(), 889 mDisplayHeight - getWindowHeight()); 890 } 891 getMaxLayoutHeight()892 private int getMaxLayoutHeight() { 893 return mDisplayHeight - mMargin * 2; 894 } 895 getLayoutWidth()896 private int getLayoutWidth() { 897 return mPadding * 2 + mIconWidth; 898 } 899 getLayoutHeight()900 private int getLayoutHeight() { 901 return Math.min(getMaxLayoutHeight(), calculateActualLayoutHeight()); 902 } 903 getWindowWidth()904 private int getWindowWidth() { 905 return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth(); 906 } 907 getWindowHeight()908 private int getWindowHeight() { 909 return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight()); 910 } 911 setSystemGestureExclusion()912 private void setSystemGestureExclusion() { 913 final Rect excludeZone = 914 new Rect(0, 0, getWindowWidth(), getWindowHeight()); 915 post(() -> setSystemGestureExclusionRects( 916 mIsShowing 917 ? Collections.singletonList(excludeZone) 918 : Collections.emptyList())); 919 } 920 } 921