blob: 850e9fc0db7eb234bf02ec3ec01aaad534fd1586 [file] [log] [blame]
Felka Chang489cf262019-12-26 13:36:23 +08001/*
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
17package android.view;
18
19import static android.view.Gravity.BOTTOM;
20import static android.view.Gravity.LEFT;
21import static android.view.Gravity.RIGHT;
22import static android.view.Gravity.TOP;
23
24import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
25
26import android.annotation.NonNull;
27import android.annotation.Nullable;
28import android.graphics.Insets;
29import android.graphics.Matrix;
30import android.graphics.Path;
31import android.graphics.Rect;
32import android.graphics.RectF;
33import android.graphics.Region;
34import android.text.TextUtils;
35import android.util.Log;
36import android.util.PathParser;
37
38import com.android.internal.annotations.VisibleForTesting;
39
40import java.util.Locale;
41import 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)
73public 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 final Insets mInsets;
98
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 if (DEBUG) {
108 Log.d(TAG, String.format(Locale.ENGLISH,
109 "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s",
110 mLeftBound != null ? mLeftBound.toString() : "",
111 mTopBound != null ? mTopBound.toString() : "",
112 mRightBound != null ? mRightBound.toString() : "",
113 mBottomBound != null ? mBottomBound.toString() : ""));
114 }
115 }
116
117 @VisibleForTesting(visibility = PACKAGE)
118 @Nullable
119 public Path getPath() {
120 return mPath;
121 }
122
123 @VisibleForTesting(visibility = PACKAGE)
124 @Nullable
125 public Rect getLeftBound() {
126 return mLeftBound;
127 }
128
129 @VisibleForTesting(visibility = PACKAGE)
130 @Nullable
131 public Rect getTopBound() {
132 return mTopBound;
133 }
134
135 @VisibleForTesting(visibility = PACKAGE)
136 @Nullable
137 public Rect getRightBound() {
138 return mRightBound;
139 }
140
141 @VisibleForTesting(visibility = PACKAGE)
142 @Nullable
143 public Rect getBottomBound() {
144 return mBottomBound;
145 }
146
147 /**
148 * To count the safe inset according to the cutout bounds and waterfall inset.
149 *
150 * @return the safe inset.
151 */
152 @VisibleForTesting(visibility = PACKAGE)
153 @NonNull
154 public Rect getSafeInset() {
155 return mInsets.toRect();
156 }
157
158 private static int decideWhichEdge(boolean isTopEdgeShortEdge,
159 boolean isShortEdge, boolean isStart) {
160 return (isTopEdgeShortEdge)
161 ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT))
162 : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM));
163 }
164
165 /**
166 * The CutoutSpecification Parser.
167 */
168 @VisibleForTesting(visibility = PACKAGE)
169 public static class Parser {
170 private final boolean mIsShortEdgeOnTop;
171 private final float mDensity;
172 private final int mDisplayWidth;
173 private final int mDisplayHeight;
174 private final Matrix mMatrix;
175 private Insets mInsets;
176 private int mSafeInsetLeft;
177 private int mSafeInsetTop;
178 private int mSafeInsetRight;
179 private int mSafeInsetBottom;
180
181 private final Rect mTmpRect = new Rect();
182 private final RectF mTmpRectF = new RectF();
183
184 private boolean mInDp;
185
186 private Path mPath;
187 private Rect mLeftBound;
188 private Rect mTopBound;
189 private Rect mRightBound;
190 private Rect mBottomBound;
191
192 private boolean mPositionFromLeft = false;
193 private boolean mPositionFromRight = false;
194 private boolean mPositionFromBottom = false;
195 private boolean mPositionFromCenterVertical = false;
196
197 private boolean mBindLeftCutout = false;
198 private boolean mBindRightCutout = false;
199 private boolean mBindBottomCutout = false;
200
201 private boolean mIsTouchShortEdgeStart;
202 private boolean mIsTouchShortEdgeEnd;
203 private boolean mIsCloserToStartSide;
204
205 /**
206 * The constructor of the CutoutSpecification parser to parse the specification of cutout.
207 * @param density the display density.
208 * @param displayWidth the display width.
209 * @param displayHeight the display height.
210 */
211 @VisibleForTesting(visibility = PACKAGE)
212 public Parser(float density, int displayWidth, int displayHeight) {
213 mDensity = density;
214 mDisplayWidth = displayWidth;
215 mDisplayHeight = displayHeight;
216 mMatrix = new Matrix();
217 mIsShortEdgeOnTop = mDisplayWidth < mDisplayHeight;
218 }
219
220 private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
221 mTmpRectF.setEmpty();
222 p.computeBounds(mTmpRectF, false /* unused */);
223 mTmpRectF.round(inoutRect);
224 inoutRegion.op(inoutRect, Region.Op.UNION);
225 }
226
227 private void resetStatus(StringBuilder sb) {
228 sb.setLength(0);
229 mPositionFromBottom = false;
230 mPositionFromLeft = false;
231 mPositionFromRight = false;
232 mPositionFromCenterVertical = false;
233
234 mBindLeftCutout = false;
235 mBindRightCutout = false;
236 mBindBottomCutout = false;
237 }
238
239 private void translateMatrix() {
240 final float offsetX;
241 if (mPositionFromRight) {
242 offsetX = mDisplayWidth;
243 } else if (mPositionFromLeft) {
244 offsetX = 0;
245 } else {
246 offsetX = mDisplayWidth / 2f;
247 }
248
249 final float offsetY;
250 if (mPositionFromBottom) {
251 offsetY = mDisplayHeight;
252 } else if (mPositionFromCenterVertical) {
253 offsetY = mDisplayHeight / 2f;
254 } else {
255 offsetY = 0;
256 }
257
258 mMatrix.reset();
259 if (mInDp) {
260 mMatrix.postScale(mDensity, mDensity);
261 }
262 mMatrix.postTranslate(offsetX, offsetY);
263 }
264
265 private int computeSafeInsets(int gravity, Rect rect) {
266 if (gravity == LEFT && rect.right > 0 && rect.right < mDisplayWidth) {
267 return rect.right;
268 } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mDisplayHeight) {
269 return rect.bottom;
270 } else if (gravity == RIGHT && rect.left > 0 && rect.left < mDisplayWidth) {
271 return mDisplayWidth - rect.left;
272 } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mDisplayHeight) {
273 return mDisplayHeight - rect.top;
274 }
275 return 0;
276 }
277
278 private void setSafeInset(int gravity, int inset) {
279 if (gravity == LEFT) {
280 mSafeInsetLeft = inset;
281 } else if (gravity == TOP) {
282 mSafeInsetTop = inset;
283 } else if (gravity == RIGHT) {
284 mSafeInsetRight = inset;
285 } else if (gravity == BOTTOM) {
286 mSafeInsetBottom = inset;
287 }
288 }
289
290 private int getSafeInset(int gravity) {
291 if (gravity == LEFT) {
292 return mSafeInsetLeft;
293 } else if (gravity == TOP) {
294 return mSafeInsetTop;
295 } else if (gravity == RIGHT) {
296 return mSafeInsetRight;
297 } else if (gravity == BOTTOM) {
298 return mSafeInsetBottom;
299 }
300 return 0;
301 }
302
303 @NonNull
304 private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) {
305 final int gravity;
306 if (isShortEdge) {
307 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart);
308 } else {
309 if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) {
310 gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart);
311 } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) {
312 gravity = decideWhichEdge(mIsShortEdgeOnTop, true,
313 mIsCloserToStartSide);
314 } else {
315 gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart);
316 }
317 }
318
319 int oldSafeInset = getSafeInset(gravity);
320 int newSafeInset = computeSafeInsets(gravity, rect);
321 if (oldSafeInset < newSafeInset) {
322 setSafeInset(gravity, newSafeInset);
323 }
324
325 return new Rect(rect);
326 }
327
328 private void setEdgeCutout(@NonNull Path newPath) {
329 if (mBindRightCutout && mRightBound == null) {
330 mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect);
331 } else if (mBindLeftCutout && mLeftBound == null) {
332 mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect);
333 } else if (mBindBottomCutout && mBottomBound == null) {
334 mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect);
335 } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout)
336 && mTopBound == null) {
337 mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect);
338 } else {
339 return;
340 }
341
342 if (mPath != null) {
343 mPath.addPath(newPath);
344 } else {
345 mPath = newPath;
346 }
347 }
348
349 private void parseSvgPathSpec(Region region, String spec) {
350 if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) {
351 Log.e(TAG, "According to SVG definition, it shouldn't happen");
352 return;
353 }
354 spec.trim();
355 translateMatrix();
356
357 final Path newPath = PathParser.createPathFromPathData(spec);
358 newPath.transform(mMatrix);
359 computeBoundsRectAndAddToRegion(newPath, region, mTmpRect);
360
361 if (DEBUG) {
362 Log.d(TAG, String.format(Locale.ENGLISH,
363 "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b",
364 mPositionFromLeft, mPositionFromRight, mPositionFromBottom,
365 mPositionFromCenterVertical));
366 Log.d(TAG, "region = " + region);
367 Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath);
368 }
369
370 if (mTmpRect.isEmpty()) {
371 return;
372 }
373
374 if (mIsShortEdgeOnTop) {
375 mIsTouchShortEdgeStart = mTmpRect.top <= 0;
376 mIsTouchShortEdgeEnd = mTmpRect.bottom >= mDisplayHeight;
377 mIsCloserToStartSide = mTmpRect.centerY() < mDisplayHeight / 2;
378 } else {
379 mIsTouchShortEdgeStart = mTmpRect.left <= 0;
380 mIsTouchShortEdgeEnd = mTmpRect.right >= mDisplayWidth;
381 mIsCloserToStartSide = mTmpRect.centerX() < mDisplayWidth / 2;
382 }
383
384 setEdgeCutout(newPath);
385 }
386
387 private void parseSpecWithoutDp(@NonNull String specWithoutDp) {
388 Region region = Region.obtain();
389 StringBuilder sb = null;
390 int currentIndex = 0;
391 int lastIndex = 0;
392 while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) {
393 if (sb == null) {
394 sb = new StringBuilder(specWithoutDp.length());
395 }
396 sb.append(specWithoutDp, lastIndex, currentIndex);
397
398 if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) {
399 if (!mPositionFromRight) {
400 mPositionFromLeft = true;
401 }
402 currentIndex += LEFT_MARKER.length();
403 } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) {
404 if (!mPositionFromLeft) {
405 mPositionFromRight = true;
406 }
407 currentIndex += RIGHT_MARKER.length();
408 } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) {
Felka Chang15322da2020-02-15 12:03:00 +0800409 parseSvgPathSpec(region, sb.toString());
Felka Chang489cf262019-12-26 13:36:23 +0800410 currentIndex += BOTTOM_MARKER.length();
411
412 /* prepare to parse the rest path */
413 resetStatus(sb);
414 mBindBottomCutout = true;
415 mPositionFromBottom = true;
416 } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) {
Felka Chang15322da2020-02-15 12:03:00 +0800417 parseSvgPathSpec(region, sb.toString());
Felka Chang489cf262019-12-26 13:36:23 +0800418 currentIndex += CENTER_VERTICAL_MARKER.length();
419
420 /* prepare to parse the rest path */
421 resetStatus(sb);
422 mPositionFromCenterVertical = true;
423 } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) {
424 parseSvgPathSpec(region, sb.toString());
425 currentIndex += CUTOUT_MARKER.length();
426
427 /* prepare to parse the rest path */
428 resetStatus(sb);
429 } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) {
Felka Chang15322da2020-02-15 12:03:00 +0800430 mBindBottomCutout = false;
431 mBindRightCutout = false;
432 mBindLeftCutout = true;
433
Felka Chang489cf262019-12-26 13:36:23 +0800434 currentIndex += BIND_LEFT_CUTOUT_MARKER.length();
435 } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) {
Felka Chang15322da2020-02-15 12:03:00 +0800436 mBindBottomCutout = false;
437 mBindLeftCutout = false;
438 mBindRightCutout = true;
439
Felka Chang489cf262019-12-26 13:36:23 +0800440 currentIndex += BIND_RIGHT_CUTOUT_MARKER.length();
441 } else {
442 currentIndex += 1;
443 }
444
445 lastIndex = currentIndex;
446 }
447
448 if (sb == null) {
449 parseSvgPathSpec(region, specWithoutDp);
450 } else {
451 sb.append(specWithoutDp, lastIndex, specWithoutDp.length());
452 parseSvgPathSpec(region, sb.toString());
453 }
454
455 region.recycle();
456 }
457
458 /**
459 * To parse specification string as the CutoutSpecification.
460 *
461 * @param originalSpec the specification string
462 * @return the CutoutSpecification instance
463 */
464 @VisibleForTesting(visibility = PACKAGE)
465 public CutoutSpecification parse(@NonNull String originalSpec) {
466 Objects.requireNonNull(originalSpec);
467
468 int dpIndex = originalSpec.lastIndexOf(DP_MARKER);
469 mInDp = (dpIndex != -1);
470 final String spec;
471 if (dpIndex != -1) {
472 spec = originalSpec.substring(0, dpIndex)
473 + originalSpec.substring(dpIndex + DP_MARKER.length());
474 } else {
475 spec = originalSpec;
476 }
477
478 parseSpecWithoutDp(spec);
479
480 mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom);
481 return new CutoutSpecification(this);
482 }
483 }
484}