1 /* 2 * Copyright (C) 2022 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.view.View.OVER_SCROLL_ALWAYS; 20 import static android.view.View.OVER_SCROLL_NEVER; 21 22 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL; 23 24 import android.annotation.IntDef; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.graphics.Insets; 28 import android.graphics.PointF; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.view.DisplayCutout; 32 import android.view.WindowInsets; 33 import android.view.WindowManager; 34 import android.view.WindowMetrics; 35 36 import androidx.annotation.DimenRes; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.systemui.res.R; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 44 /** 45 * Provides the layout resources information of the {@link MenuView}. 46 */ 47 class MenuViewAppearance { 48 private final WindowManager mWindowManager; 49 private final Resources mRes; 50 private final Position mPercentagePosition = new Position(/* percentageX= */ 51 0f, /* percentageY= */ 0f); 52 private boolean mIsImeShowing; 53 // Avoid the menu view overlapping on the primary action button under the bottom as possible. 54 private int mImeShiftingSpace; 55 private int mTargetFeaturesSize; 56 private int mSizeType; 57 private int mMargin; 58 private int mSmallPadding; 59 private int mLargePadding; 60 private int mSmallIconSize; 61 private int mLargeIconSize; 62 private int mSmallBadgeSize; 63 private int mLargeBadgeSize; 64 private int mSmallSingleRadius; 65 private int mSmallMultipleRadius; 66 private int mLargeSingleRadius; 67 private int mLargeMultipleRadius; 68 private int mStrokeWidth; 69 private int mStrokeColor; 70 private int mInset; 71 private int mElevation; 72 private float mImeTop; 73 private float[] mRadii; 74 private Drawable mBackgroundDrawable; 75 private String mContentDescription; 76 77 @IntDef({ 78 SMALL, 79 MenuSizeType.LARGE 80 }) 81 @Retention(RetentionPolicy.SOURCE) 82 @interface MenuSizeType { 83 int SMALL = 0; 84 int LARGE = 1; 85 } 86 MenuViewAppearance(Context context, WindowManager windowManager)87 MenuViewAppearance(Context context, WindowManager windowManager) { 88 mWindowManager = windowManager; 89 mRes = context.getResources(); 90 91 update(); 92 } 93 update()94 void update() { 95 mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); 96 mSmallPadding = 97 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding); 98 mLargePadding = 99 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_padding); 100 mSmallIconSize = 101 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height); 102 mLargeIconSize = 103 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_width_height); 104 mSmallBadgeSize = 105 mRes.getDimensionPixelSize( 106 R.dimen.accessibility_floating_menu_small_badge_width_height); 107 mLargeBadgeSize = 108 mRes.getDimensionPixelSize( 109 R.dimen.accessibility_floating_menu_large_badge_width_height); 110 mSmallSingleRadius = 111 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius); 112 mSmallMultipleRadius = mRes.getDimensionPixelSize( 113 R.dimen.accessibility_floating_menu_small_multiple_radius); 114 mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); 115 mLargeSingleRadius = 116 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius); 117 mLargeMultipleRadius = mRes.getDimensionPixelSize( 118 R.dimen.accessibility_floating_menu_large_multiple_radius); 119 mStrokeWidth = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width); 120 mStrokeColor = mRes.getColor(R.color.accessibility_floating_menu_stroke_dark); 121 mInset = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); 122 mElevation = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); 123 mImeShiftingSpace = mRes.getDimensionPixelSize( 124 R.dimen.accessibility_floating_menu_ime_shifting_space); 125 final Drawable drawable = 126 mRes.getDrawable(R.drawable.accessibility_floating_menu_background); 127 mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable}); 128 mContentDescription = mRes.getString( 129 com.android.internal.R.string.accessibility_select_shortcut_menu_title); 130 } 131 setSizeType(int sizeType)132 void setSizeType(int sizeType) { 133 mSizeType = sizeType; 134 135 mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); 136 } 137 setTargetFeaturesSize(int targetFeaturesSize)138 void setTargetFeaturesSize(int targetFeaturesSize) { 139 mTargetFeaturesSize = targetFeaturesSize; 140 141 mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize)); 142 } 143 setPercentagePosition(Position percentagePosition)144 void setPercentagePosition(Position percentagePosition) { 145 mPercentagePosition.update(percentagePosition); 146 147 mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); 148 } 149 onImeVisibilityChanged(boolean imeShowing, float imeTop)150 void onImeVisibilityChanged(boolean imeShowing, float imeTop) { 151 mIsImeShowing = imeShowing; 152 mImeTop = imeTop; 153 } 154 getMenuDraggableBounds()155 Rect getMenuDraggableBounds() { 156 return getMenuDraggableBoundsWith(/* includeIme= */ true); 157 } 158 getMenuDraggableBoundsExcludeIme()159 Rect getMenuDraggableBoundsExcludeIme() { 160 return getMenuDraggableBoundsWith(/* includeIme= */ false); 161 } 162 getMenuDraggableBoundsWith(boolean includeIme)163 private Rect getMenuDraggableBoundsWith(boolean includeIme) { 164 final int margin = getMenuMargin(); 165 final Rect draggableBounds = new Rect(getWindowAvailableBounds()); 166 167 draggableBounds.top += margin; 168 draggableBounds.right -= getMenuWidth(); 169 170 if (includeIme && mIsImeShowing) { 171 final int imeHeight = (int) (draggableBounds.bottom - mImeTop); 172 draggableBounds.bottom -= (imeHeight + mImeShiftingSpace); 173 } 174 draggableBounds.bottom -= (calculateActualMenuHeight() + margin); 175 draggableBounds.bottom = Math.max(draggableBounds.top, draggableBounds.bottom); 176 177 return draggableBounds; 178 } 179 getMenuPosition()180 PointF getMenuPosition() { 181 final Rect draggableBounds = getMenuDraggableBoundsExcludeIme(); 182 final float x = draggableBounds.left 183 + draggableBounds.width() * mPercentagePosition.getPercentageX(); 184 185 float y = draggableBounds.top 186 + draggableBounds.height() * mPercentagePosition.getPercentageY(); 187 188 // If the bottom of the menu view and overlap on the ime, its position y will be 189 // overridden with new y. 190 final float menuBottom = y + getMenuHeight() + mMargin; 191 if (mIsImeShowing && (menuBottom >= mImeTop)) { 192 y = Math.max(draggableBounds.top, 193 mImeTop - getMenuHeight() - mMargin - mImeShiftingSpace); 194 } 195 196 return new PointF(x, y); 197 } 198 getContentDescription()199 String getContentDescription() { 200 return mContentDescription; 201 } 202 getMenuBackground()203 Drawable getMenuBackground() { 204 return mBackgroundDrawable; 205 } 206 getMenuElevation()207 int getMenuElevation() { 208 return mElevation; 209 } 210 getMenuWidth()211 int getMenuWidth() { 212 return getMenuPadding() * 2 + getMenuIconSize(); 213 } 214 getMenuHeight()215 int getMenuHeight() { 216 return Math.min(getWindowAvailableBounds().height() - mMargin * 2, 217 calculateActualMenuHeight()); 218 } 219 getMenuIconSize()220 int getMenuIconSize() { 221 return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize; 222 } 223 getBadgeIconSize()224 int getBadgeIconSize() { 225 return mSizeType == SMALL ? mSmallBadgeSize : mLargeBadgeSize; 226 } 227 getMenuMargin()228 private int getMenuMargin() { 229 return mMargin; 230 } 231 getMenuPadding()232 int getMenuPadding() { 233 return mSizeType == SMALL ? mSmallPadding : mLargePadding; 234 } 235 getMenuInsets()236 int[] getMenuInsets() { 237 final int left = isMenuOnLeftSide() ? mInset : 0; 238 final int right = isMenuOnLeftSide() ? 0 : mInset; 239 240 return new int[]{left, 0, right, 0}; 241 } 242 getMenuMovingStateInsets()243 int[] getMenuMovingStateInsets() { 244 return new int[]{0, 0, 0, 0}; 245 } 246 getMenuMovingStateRadii()247 float[] getMenuMovingStateRadii() { 248 final float radius = getMenuRadius(mTargetFeaturesSize); 249 return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; 250 } 251 getMenuStrokeWidth()252 int getMenuStrokeWidth() { 253 return mStrokeWidth; 254 } 255 getMenuStrokeColor()256 int getMenuStrokeColor() { 257 return mStrokeColor; 258 } 259 getMenuRadii()260 float[] getMenuRadii() { 261 return mRadii; 262 } 263 getMenuRadius(int itemCount)264 private int getMenuRadius(int itemCount) { 265 return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount); 266 } 267 getMenuScrollMode()268 int getMenuScrollMode() { 269 return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER; 270 } 271 hasExceededMaxWindowHeight()272 private boolean hasExceededMaxWindowHeight() { 273 return calculateActualMenuHeight() > getWindowAvailableBounds().height(); 274 } 275 276 @DimenRes getSmallSize(int itemCount)277 private int getSmallSize(int itemCount) { 278 return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius; 279 } 280 281 @DimenRes getLargeSize(int itemCount)282 private int getLargeSize(int itemCount) { 283 return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius; 284 } 285 createRadii(boolean isMenuOnLeftSide, float radius)286 private static float[] createRadii(boolean isMenuOnLeftSide, float radius) { 287 return isMenuOnLeftSide 288 ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f} 289 : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; 290 } 291 getWindowAvailableBounds()292 public Rect getWindowAvailableBounds() { 293 final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); 294 final WindowInsets windowInsets = windowMetrics.getWindowInsets(); 295 final Insets insets = windowInsets.getInsetsIgnoringVisibility( 296 WindowInsets.Type.systemBars()); 297 298 final Rect bounds = new Rect(windowMetrics.getBounds()); 299 bounds.left += insets.left; 300 bounds.right -= insets.right; 301 bounds.top += insets.top; 302 bounds.bottom -= insets.bottom; 303 304 return bounds; 305 } 306 getDisplayCutout()307 DisplayCutout getDisplayCutout() { 308 return mWindowManager.getCurrentWindowMetrics().getWindowInsets().getDisplayCutout(); 309 } 310 avoidVerticalDisplayCutout(float y, Rect bounds, Rect cutout)311 float avoidVerticalDisplayCutout(float y, Rect bounds, Rect cutout) { 312 int menuHeight = calculateActualMenuHeight(); 313 return avoidVerticalDisplayCutout(y, menuHeight, bounds, cutout); 314 } 315 316 @VisibleForTesting avoidVerticalDisplayCutout( float y, float menuHeight, Rect bounds, Rect cutout)317 public static float avoidVerticalDisplayCutout( 318 float y, float menuHeight, Rect bounds, Rect cutout) { 319 if (cutout.top > y + menuHeight || cutout.bottom < y) { 320 return clampVerticalPosition(y, menuHeight, bounds.top, bounds.bottom); 321 } 322 323 boolean topAvailable = cutout.top - bounds.top >= menuHeight; 324 boolean bottomAvailable = bounds.bottom - cutout.bottom >= menuHeight; 325 boolean topOrBottom; 326 if (!topAvailable && !bottomAvailable) { 327 return clampVerticalPosition(y, menuHeight, bounds.top, bounds.bottom); 328 } else if (topAvailable && !bottomAvailable) { 329 topOrBottom = true; 330 } else if (!topAvailable && bottomAvailable) { 331 topOrBottom = false; 332 } else { 333 topOrBottom = y + menuHeight * 0.5f < cutout.centerY(); 334 } 335 336 float finalPosition = (topOrBottom) ? cutout.top - menuHeight : cutout.bottom; 337 return clampVerticalPosition(finalPosition, menuHeight, bounds.top, bounds.bottom); 338 } 339 340 private static float clampVerticalPosition( 341 float position, float height, float min, float max) { 342 position = Float.max(min + height / 2, position); 343 position = Float.min(max - height / 2, position); 344 return position; 345 } 346 347 boolean isMenuOnLeftSide() { 348 return mPercentagePosition.getPercentageX() < 0.5f; 349 } 350 351 private int calculateActualMenuHeight() { 352 final int menuPadding = getMenuPadding(); 353 354 return (menuPadding + getMenuIconSize()) * mTargetFeaturesSize + menuPadding; 355 } 356 } 357