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