• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.view;
18 
19 import static android.view.Gravity.BOTTOM;
20 import static android.view.Gravity.LEFT;
21 import static android.view.Gravity.RIGHT;
22 import static android.view.Gravity.TOP;
23 
24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.graphics.Insets;
29 import android.graphics.Matrix;
30 import android.graphics.Path;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.graphics.Region;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.util.PathParser;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.util.Locale;
41 import java.util.Objects;
42 
43 /**
44  * In order to accept the cutout specification for all of edges in devices, the specification
45  * parsing method is extracted from
46  * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be
47  * the specified class for parsing the specification.
48  * BNF definition:
49  * <ul>
50  *      <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li>
51  *      <li>Cutout Specification  = [Vertical Position], (SVG Path Element), [Horizontal Position]
52  *                                  [Bind Cutout] ;</li>
53  *      <li>Vertical Position     = "@bottom" | "@center_vertical" ;</li>
54  *      <li>Horizontal Position   = "@left" | "@right" ;</li>
55  *      <li>Bind Cutout           = "@bind_left_cutout" | "@bind_right_cutout" ;</li>
56  *      <li>Cutout Delimiter      = "@cutout" ;</li>
57  *      <li>Dp                    = "@dp"</li>
58  * </ul>
59  *
60  * <ul>
61  *     <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical"
62  *     </li>
63  *     <li>Horizontal position is center horizontal by default if there is neither "@left" nor
64  *     "@right".</li>
65  *     <li>@bottom make the cutout piece bind to bottom edge.</li>
66  *     <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to
67  *     left or right edge cutout.</li>
68  * </ul>
69  *
70  * @hide
71  */
72 @VisibleForTesting(visibility = PACKAGE)
73 public class CutoutSpecification {
74     private static final String TAG = "CutoutSpecification";
75     private static final boolean DEBUG = false;
76 
77     private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length();
78 
79     private static final char MARKER_START_CHAR = '@';
80     private static final String DP_MARKER = MARKER_START_CHAR + "dp";
81 
82     private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom";
83     private static final String RIGHT_MARKER = MARKER_START_CHAR + "right";
84     private static final String LEFT_MARKER = MARKER_START_CHAR + "left";
85     private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout";
86     private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical";
87 
88     /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */
89     private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout";
90     private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout";
91 
92     private final Path mPath;
93     private final Rect mLeftBound;
94     private final Rect mTopBound;
95     private final Rect mRightBound;
96     private final Rect mBottomBound;
97     private Insets mInsets;
98 
CutoutSpecification(@onNull Parser parser)99     private CutoutSpecification(@NonNull Parser parser) {
100         mPath = parser.mPath;
101         mLeftBound = parser.mLeftBound;
102         mTopBound = parser.mTopBound;
103         mRightBound = parser.mRightBound;
104         mBottomBound = parser.mBottomBound;
105         mInsets = parser.mInsets;
106 
107         applyPhysicalPixelDisplaySizeRatio(parser.mPhysicalPixelDisplaySizeRatio);
108 
109         if (DEBUG) {
110             Log.d(TAG, String.format(Locale.ENGLISH,
111                     "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s",
112                     mLeftBound != null ? mLeftBound.toString() : "",
113                     mTopBound != null ? mTopBound.toString() : "",
114                     mRightBound != null ? mRightBound.toString() : "",
115                     mBottomBound != null ? mBottomBound.toString() : ""));
116         }
117     }
118 
applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio)119     private void applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio) {
120         if (physicalPixelDisplaySizeRatio == 1f) {
121             return;
122         }
123 
124         if (mPath != null && !mPath.isEmpty()) {
125             final Matrix matrix = new Matrix();
126             matrix.postScale(physicalPixelDisplaySizeRatio, physicalPixelDisplaySizeRatio);
127             mPath.transform(matrix);
128         }
129 
130         scaleBounds(mLeftBound, physicalPixelDisplaySizeRatio);
131         scaleBounds(mTopBound, physicalPixelDisplaySizeRatio);
132         scaleBounds(mRightBound, physicalPixelDisplaySizeRatio);
133         scaleBounds(mBottomBound, physicalPixelDisplaySizeRatio);
134         mInsets = scaleInsets(mInsets, physicalPixelDisplaySizeRatio);
135     }
136 
scaleBounds(Rect r, float ratio)137     private void scaleBounds(Rect r, float ratio) {
138         if (r != null && !r.isEmpty()) {
139             r.scale(ratio);
140         }
141     }
142 
scaleInsets(Insets insets, float ratio)143     private Insets scaleInsets(Insets insets, float ratio) {
144         return Insets.of(
145                 (int) (insets.left * ratio + 0.5f),
146                 (int) (insets.top * ratio + 0.5f),
147                 (int) (insets.right * ratio + 0.5f),
148                 (int) (insets.bottom * ratio + 0.5f));
149     }
150 
151     @VisibleForTesting(visibility = PACKAGE)
152     @Nullable
getPath()153     public Path getPath() {
154         return mPath;
155     }
156 
157     @VisibleForTesting(visibility = PACKAGE)
158     @Nullable
getLeftBound()159     public Rect getLeftBound() {
160         return mLeftBound;
161     }
162 
163     @VisibleForTesting(visibility = PACKAGE)
164     @Nullable
getTopBound()165     public Rect getTopBound() {
166         return mTopBound;
167     }
168 
169     @VisibleForTesting(visibility = PACKAGE)
170     @Nullable
getRightBound()171     public Rect getRightBound() {
172         return mRightBound;
173     }
174 
175     @VisibleForTesting(visibility = PACKAGE)
176     @Nullable
getBottomBound()177     public Rect getBottomBound() {
178         return mBottomBound;
179     }
180 
181     /**
182      * To count the safe inset according to the cutout bounds and waterfall inset.
183      *
184      * @return the safe inset.
185      */
186     @VisibleForTesting(visibility = PACKAGE)
187     @NonNull
getSafeInset()188     public Rect getSafeInset() {
189         return mInsets.toRect();
190     }
191 
decideWhichEdge(boolean isTopEdgeShortEdge, boolean isShortEdge, boolean isStart)192     private static int decideWhichEdge(boolean isTopEdgeShortEdge,
193             boolean isShortEdge, boolean isStart) {
194         return (isTopEdgeShortEdge)
195                 ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT))
196                 : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM));
197     }
198 
199     /**
200      * The CutoutSpecification Parser.
201      */
202     @VisibleForTesting(visibility = PACKAGE)
203     public static class Parser {
204         private final boolean mIsShortEdgeOnTop;
205         private final float mStableDensity;
206         private final int mPhysicalDisplayWidth;
207         private final int mPhysicalDisplayHeight;
208         private final float mPhysicalPixelDisplaySizeRatio;
209         private final Matrix mMatrix;
210         private Insets mInsets;
211         private int mSafeInsetLeft;
212         private int mSafeInsetTop;
213         private int mSafeInsetRight;
214         private int mSafeInsetBottom;
215 
216         private final Rect mTmpRect = new Rect();
217         private final RectF mTmpRectF = new RectF();
218 
219         private boolean mInDp;
220 
221         private Path mPath;
222         private Rect mLeftBound;
223         private Rect mTopBound;
224         private Rect mRightBound;
225         private Rect mBottomBound;
226 
227         private boolean mPositionFromLeft = false;
228         private boolean mPositionFromRight = false;
229         private boolean mPositionFromBottom = false;
230         private boolean mPositionFromCenterVertical = false;
231 
232         private boolean mBindLeftCutout = false;
233         private boolean mBindRightCutout = false;
234         private boolean mBindBottomCutout = false;
235 
236         private boolean mIsTouchShortEdgeStart;
237         private boolean mIsTouchShortEdgeEnd;
238         private boolean mIsCloserToStartSide;
239 
240         @VisibleForTesting(visibility = PACKAGE)
Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight)241         public Parser(float stableDensity, int physicalDisplayWidth,
242                 int physicalDisplayHeight) {
243             this(stableDensity, physicalDisplayWidth, physicalDisplayHeight, 1f);
244         }
245 
246         /**
247          * The constructor of the CutoutSpecification parser to parse the specification of cutout.
248          * @param stableDensity the display density.
249          * @param physicalDisplayWidth the display width.
250          * @param physicalDisplayHeight the display height.
251          * @param physicalPixelDisplaySizeRatio the display size ratio based on stable display size.
252          */
Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight, float physicalPixelDisplaySizeRatio)253         Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight,
254                 float physicalPixelDisplaySizeRatio) {
255             mStableDensity = stableDensity;
256             mPhysicalDisplayWidth = physicalDisplayWidth;
257             mPhysicalDisplayHeight = physicalDisplayHeight;
258             mPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio;
259             mMatrix = new Matrix();
260             mIsShortEdgeOnTop = mPhysicalDisplayWidth < mPhysicalDisplayHeight;
261         }
262 
263         private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
264             mTmpRectF.setEmpty();
265             p.computeBounds(mTmpRectF, false /* unused */);
266             mTmpRectF.round(inoutRect);
267             inoutRegion.op(inoutRect, Region.Op.UNION);
268         }
269 
270         private void resetStatus(StringBuilder sb) {
271             sb.setLength(0);
272             mPositionFromBottom = false;
273             mPositionFromLeft = false;
274             mPositionFromRight = false;
275             mPositionFromCenterVertical = false;
276 
277             mBindLeftCutout = false;
278             mBindRightCutout = false;
279             mBindBottomCutout = false;
280         }
281 
282         private void translateMatrix() {
283             final float offsetX;
284             if (mPositionFromRight) {
285                 offsetX = mPhysicalDisplayWidth;
286             } else if (mPositionFromLeft) {
287                 offsetX = 0;
288             } else {
289                 offsetX = mPhysicalDisplayWidth / 2f;
290             }
291 
292             final float offsetY;
293             if (mPositionFromBottom) {
294                 offsetY = mPhysicalDisplayHeight;
295             } else if (mPositionFromCenterVertical) {
296                 offsetY = mPhysicalDisplayHeight / 2f;
297             } else {
298                 offsetY = 0;
299             }
300 
301             mMatrix.reset();
302             if (mInDp) {
303                 mMatrix.postScale(mStableDensity, mStableDensity);
304             }
305             mMatrix.postTranslate(offsetX, offsetY);
306         }
307 
308         private int computeSafeInsets(int gravity, Rect rect) {
309             if (gravity == LEFT && rect.right > 0 && rect.right < mPhysicalDisplayWidth) {
310                 return rect.right;
311             } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mPhysicalDisplayHeight) {
312                 return rect.bottom;
313             } else if (gravity == RIGHT && rect.left > 0 && rect.left < mPhysicalDisplayWidth) {
314                 return mPhysicalDisplayWidth - rect.left;
315             } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mPhysicalDisplayHeight) {
316                 return mPhysicalDisplayHeight - rect.top;
317             }
318             return 0;
319         }
320 
321         private void setSafeInset(int gravity, int inset) {
322             if (gravity == LEFT) {
323                 mSafeInsetLeft = inset;
324             } else if (gravity == TOP) {
325                 mSafeInsetTop = inset;
326             } else if (gravity == RIGHT) {
327                 mSafeInsetRight = inset;
328             } else if (gravity == BOTTOM) {
329                 mSafeInsetBottom = inset;
330             }
331         }
332 
333         private int getSafeInset(int gravity) {
334             if (gravity == LEFT) {
335                 return mSafeInsetLeft;
336             } else if (gravity == TOP) {
337                 return mSafeInsetTop;
338             } else if (gravity == RIGHT) {
339                 return mSafeInsetRight;
340             } else if (gravity == BOTTOM) {
341                 return mSafeInsetBottom;
342             }
343             return 0;
344         }
345 
346         @NonNull
347         private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) {
348             final int gravity;
349             if (isShortEdge) {
350                 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart);
351             } else {
352                 if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) {
353                     gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart);
354                 } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) {
355                     gravity = decideWhichEdge(mIsShortEdgeOnTop, true,
356                             mIsCloserToStartSide);
357                 } else {
358                     gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart);
359                 }
360             }
361 
362             int oldSafeInset = getSafeInset(gravity);
363             int newSafeInset = computeSafeInsets(gravity, rect);
364             if (oldSafeInset < newSafeInset) {
365                 setSafeInset(gravity, newSafeInset);
366             }
367 
368             return new Rect(rect);
369         }
370 
371         private void setEdgeCutout(@NonNull Path newPath) {
372             if (mBindRightCutout && mRightBound == null) {
373                 mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect);
374             } else if (mBindLeftCutout && mLeftBound == null) {
375                 mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect);
376             } else if (mBindBottomCutout && mBottomBound == null) {
377                 mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect);
378             } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout)
379                     && mTopBound == null) {
380                 mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect);
381             } else {
382                 return;
383             }
384 
385             if (mPath != null) {
386                 mPath.addPath(newPath);
387             } else {
388                 mPath = newPath;
389             }
390         }
391 
392         private void parseSvgPathSpec(Region region, String spec) {
393             if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) {
394                 Log.e(TAG, "According to SVG definition, it shouldn't happen");
395                 return;
396             }
397             translateMatrix();
398 
399             final Path newPath = PathParser.createPathFromPathData(spec);
400             newPath.transform(mMatrix);
401             computeBoundsRectAndAddToRegion(newPath, region, mTmpRect);
402 
403             if (DEBUG) {
404                 Log.d(TAG, String.format(Locale.ENGLISH,
405                         "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b",
406                         mPositionFromLeft, mPositionFromRight, mPositionFromBottom,
407                         mPositionFromCenterVertical));
408                 Log.d(TAG, "region = " + region);
409                 Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath);
410             }
411 
412             if (mTmpRect.isEmpty()) {
413                 return;
414             }
415 
416             if (mIsShortEdgeOnTop) {
417                 mIsTouchShortEdgeStart = mTmpRect.top <= 0;
418                 mIsTouchShortEdgeEnd = mTmpRect.bottom >= mPhysicalDisplayHeight;
419                 mIsCloserToStartSide = mTmpRect.centerY() < mPhysicalDisplayHeight / 2;
420             } else {
421                 mIsTouchShortEdgeStart = mTmpRect.left <= 0;
422                 mIsTouchShortEdgeEnd = mTmpRect.right >= mPhysicalDisplayWidth;
423                 mIsCloserToStartSide = mTmpRect.centerX() < mPhysicalDisplayWidth / 2;
424             }
425 
426             setEdgeCutout(newPath);
427         }
428 
429         private void parseSpecWithoutDp(@NonNull String specWithoutDp) {
430             Region region = Region.obtain();
431             StringBuilder sb = null;
432             int currentIndex = 0;
433             int lastIndex = 0;
434             while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) {
435                 if (sb == null) {
436                     sb = new StringBuilder(specWithoutDp.length());
437                 }
438                 sb.append(specWithoutDp, lastIndex, currentIndex);
439 
440                 if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) {
441                     if (!mPositionFromRight) {
442                         mPositionFromLeft = true;
443                     }
444                     currentIndex += LEFT_MARKER.length();
445                 } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) {
446                     if (!mPositionFromLeft) {
447                         mPositionFromRight = true;
448                     }
449                     currentIndex += RIGHT_MARKER.length();
450                 } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) {
451                     parseSvgPathSpec(region, sb.toString());
452                     currentIndex += BOTTOM_MARKER.length();
453 
454                     /* prepare to parse the rest path */
455                     resetStatus(sb);
456                     mBindBottomCutout = true;
457                     mPositionFromBottom = true;
458                 } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) {
459                     parseSvgPathSpec(region, sb.toString());
460                     currentIndex += CENTER_VERTICAL_MARKER.length();
461 
462                     /* prepare to parse the rest path */
463                     resetStatus(sb);
464                     mPositionFromCenterVertical = true;
465                 } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) {
466                     parseSvgPathSpec(region, sb.toString());
467                     currentIndex += CUTOUT_MARKER.length();
468 
469                     /* prepare to parse the rest path */
470                     resetStatus(sb);
471                 } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) {
472                     mBindBottomCutout = false;
473                     mBindRightCutout = false;
474                     mBindLeftCutout = true;
475 
476                     currentIndex += BIND_LEFT_CUTOUT_MARKER.length();
477                 } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) {
478                     mBindBottomCutout = false;
479                     mBindLeftCutout = false;
480                     mBindRightCutout = true;
481 
482                     currentIndex += BIND_RIGHT_CUTOUT_MARKER.length();
483                 } else {
484                     currentIndex += 1;
485                 }
486 
487                 lastIndex = currentIndex;
488             }
489 
490             if (sb == null) {
491                 parseSvgPathSpec(region, specWithoutDp);
492             } else {
493                 sb.append(specWithoutDp, lastIndex, specWithoutDp.length());
494                 parseSvgPathSpec(region, sb.toString());
495             }
496 
497             region.recycle();
498         }
499 
500         /**
501          * To parse specification string as the CutoutSpecification.
502          *
503          * @param originalSpec the specification string
504          * @return the CutoutSpecification instance
505          */
506         @VisibleForTesting(visibility = PACKAGE)
507         public CutoutSpecification parse(@NonNull String originalSpec) {
508             Objects.requireNonNull(originalSpec);
509 
510             int dpIndex = originalSpec.lastIndexOf(DP_MARKER);
511             mInDp = (dpIndex != -1);
512             final String spec;
513             if (dpIndex != -1) {
514                 spec = originalSpec.substring(0, dpIndex)
515                         + originalSpec.substring(dpIndex + DP_MARKER.length());
516             } else {
517                 spec = originalSpec;
518             }
519 
520             parseSpecWithoutDp(spec);
521             mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom);
522             return new CutoutSpecification(this);
523         }
524     }
525 }
526