• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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