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