diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2020-02-10 06:53:49 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-02-10 06:53:49 +0000 |
commit | d0db68c89082d96c632ea1745a0759fce84ff9c7 (patch) | |
tree | 11bbdc3fce1f618440a240f364178cd75229477e | |
parent | e7f3f6946141387df1fbd8dba127bcb463d66a1a (diff) | |
parent | 489cf260aa879e8f33b25db7dac6c263154dc0d8 (diff) |
Merge "To add new marker to support long edge cutout"
4 files changed, 999 insertions, 90 deletions
diff --git a/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java b/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java new file mode 100644 index 000000000000..14282bfbd24e --- /dev/null +++ b/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.PathParser; + +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class CutoutSpecificationBenchmark { + private static final String TAG = "CutoutSpecificationBenchmark"; + + private static final String BOTTOM_MARKER = "@bottom"; + private static final String DP_MARKER = "@dp"; + private static final String RIGHT_MARKER = "@right"; + private static final String LEFT_MARKER = "@left"; + + private static final String DOUBLE_CUTOUT_SPEC = "M 0,0\n" + + "L -72, 0\n" + + "L -69.9940446283, 20.0595537175\n" + + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n" + + "L 56.8, 32.0\n" + + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n" + + "L 72, 0\n" + + "Z\n" + + "@bottom\n" + + "M 0,0\n" + + "L -72, 0\n" + + "L -69.9940446283, -20.0595537175\n" + + "C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0\n" + + "L 56.8, -32.0\n" + + "C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20.0595537175\n" + + "L 72, 0\n" + + "Z\n" + + "@dp"; + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private Context mContext; + private DisplayMetrics mDisplayMetrics; + + /** + * Setup the necessary member field used by test methods. + */ + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + } + + + private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { + final RectF rectF = new RectF(); + p.computeBounds(rectF, false /* unused */); + rectF.round(inoutRect); + inoutRegion.op(inoutRect, Region.Op.UNION); + } + + private static void oldMethodParsingSpec(String spec, int displayWidth, int displayHeight, + float density) { + Path p = null; + Rect boundTop = null; + Rect boundBottom = null; + Rect safeInset = new Rect(); + String bottomSpec = null; + if (!TextUtils.isEmpty(spec)) { + spec = spec.trim(); + final float offsetX; + if (spec.endsWith(RIGHT_MARKER)) { + offsetX = displayWidth; + spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim(); + } else if (spec.endsWith(LEFT_MARKER)) { + offsetX = 0; + spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim(); + } else { + offsetX = displayWidth / 2f; + } + final boolean inDp = spec.endsWith(DP_MARKER); + if (inDp) { + spec = spec.substring(0, spec.length() - DP_MARKER.length()); + } + + if (spec.contains(BOTTOM_MARKER)) { + String[] splits = spec.split(BOTTOM_MARKER, 2); + spec = splits[0].trim(); + bottomSpec = splits[1].trim(); + } + + final Matrix m = new Matrix(); + final Region r = Region.obtain(); + if (!spec.isEmpty()) { + try { + p = PathParser.createPathFromPathData(spec); + } catch (Throwable e) { + Log.wtf(TAG, "Could not inflate cutout: ", e); + } + + if (p != null) { + if (inDp) { + m.postScale(density, density); + } + m.postTranslate(offsetX, 0); + p.transform(m); + + boundTop = new Rect(); + toRectAndAddToRegion(p, r, boundTop); + safeInset.top = boundTop.bottom; + } + } + + if (bottomSpec != null) { + int bottomInset = 0; + Path bottomPath = null; + try { + bottomPath = PathParser.createPathFromPathData(bottomSpec); + } catch (Throwable e) { + Log.wtf(TAG, "Could not inflate bottom cutout: ", e); + } + + if (bottomPath != null) { + // Keep top transform + m.postTranslate(0, displayHeight); + bottomPath.transform(m); + p.addPath(bottomPath); + boundBottom = new Rect(); + toRectAndAddToRegion(bottomPath, r, boundBottom); + bottomInset = displayHeight - boundBottom.top; + } + safeInset.bottom = bottomInset; + } + } + } + + @Test + public void parseByOldMethodForDoubleCutout() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + oldMethodParsingSpec(DOUBLE_CUTOUT_SPEC, mDisplayMetrics.widthPixels, + mDisplayMetrics.heightPixels, mDisplayMetrics.density); + } + } + + @Test + public void parseByNewMethodForDoubleCutout() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + new CutoutSpecification.Parser(mDisplayMetrics.density, + mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels) + .parse(DOUBLE_CUTOUT_SPEC); + } + } + + @Test + public void parseLongEdgeCutout() { + final String spec = "M 0,0\n" + + "H 48\n" + + "V 48\n" + + "H -48\n" + + "Z\n" + + "@left\n" + + "@center_vertical\n" + + "M 0,0\n" + + "H 48\n" + + "V 48\n" + + "H -48\n" + + "Z\n" + + "@left\n" + + "@center_vertical\n" + + "M 0,0\n" + + "H -48\n" + + "V 48\n" + + "H 48\n" + + "Z\n" + + "@right\n" + + "@dp"; + + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + new CutoutSpecification.Parser(mDisplayMetrics.density, + mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels).parse(spec); + } + } + + @Test + public void parseShortEdgeCutout() { + final String spec = "M 0,0\n" + + "H 48\n" + + "V 48\n" + + "H -48\n" + + "Z\n" + + "@bottom\n" + + "M 0,0\n" + + "H 48\n" + + "V -48\n" + + "H -48\n" + + "Z\n" + + "@dp"; + + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + new CutoutSpecification.Parser(mDisplayMetrics.density, + mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels).parse(spec); + } + } +} diff --git a/core/java/android/view/CutoutSpecification.java b/core/java/android/view/CutoutSpecification.java new file mode 100644 index 000000000000..d21a9520e12c --- /dev/null +++ b/core/java/android/view/CutoutSpecification.java @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.Gravity.BOTTOM; +import static android.view.Gravity.LEFT; +import static android.view.Gravity.RIGHT; +import static android.view.Gravity.TOP; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.text.TextUtils; +import android.util.Log; +import android.util.PathParser; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Locale; +import java.util.Objects; + +/** + * In order to accept the cutout specification for all of edges in devices, the specification + * parsing method is extracted from + * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be + * the specified class for parsing the specification. + * BNF definition: + * <ul> + * <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li> + * <li>Cutout Specification = [Vertical Position], (SVG Path Element), [Horizontal Position] + * [Bind Cutout] ;</li> + * <li>Vertical Position = "@bottom" | "@center_vertical" ;</li> + * <li>Horizontal Position = "@left" | "@right" ;</li> + * <li>Bind Cutout = "@bind_left_cutout" | "@bind_right_cutout" ;</li> + * <li>Cutout Delimiter = "@cutout" ;</li> + * <li>Dp = "@dp"</li> + * </ul> + * + * <ul> + * <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical" + * </li> + * <li>Horizontal position is center horizontal by default if there is neither "@left" nor + * "@right".</li> + * <li>@bottom make the cutout piece bind to bottom edge.</li> + * <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to + * left or right edge cutout.</li> + * </ul> + * + * @hide + */ +@VisibleForTesting(visibility = PACKAGE) +public class CutoutSpecification { + private static final String TAG = "CutoutSpecification"; + private static final boolean DEBUG = false; + + private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length(); + + private static final char MARKER_START_CHAR = '@'; + private static final String DP_MARKER = MARKER_START_CHAR + "dp"; + + private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom"; + private static final String RIGHT_MARKER = MARKER_START_CHAR + "right"; + private static final String LEFT_MARKER = MARKER_START_CHAR + "left"; + private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout"; + private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical"; + + /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */ + private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout"; + private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout"; + + private final Path mPath; + private final Rect mLeftBound; + private final Rect mTopBound; + private final Rect mRightBound; + private final Rect mBottomBound; + private final Insets mInsets; + + private CutoutSpecification(@NonNull Parser parser) { + mPath = parser.mPath; + mLeftBound = parser.mLeftBound; + mTopBound = parser.mTopBound; + mRightBound = parser.mRightBound; + mBottomBound = parser.mBottomBound; + mInsets = parser.mInsets; + + if (DEBUG) { + Log.d(TAG, String.format(Locale.ENGLISH, + "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s", + mLeftBound != null ? mLeftBound.toString() : "", + mTopBound != null ? mTopBound.toString() : "", + mRightBound != null ? mRightBound.toString() : "", + mBottomBound != null ? mBottomBound.toString() : "")); + } + } + + @VisibleForTesting(visibility = PACKAGE) + @Nullable + public Path getPath() { + return mPath; + } + + @VisibleForTesting(visibility = PACKAGE) + @Nullable + public Rect getLeftBound() { + return mLeftBound; + } + + @VisibleForTesting(visibility = PACKAGE) + @Nullable + public Rect getTopBound() { + return mTopBound; + } + + @VisibleForTesting(visibility = PACKAGE) + @Nullable + public Rect getRightBound() { + return mRightBound; + } + + @VisibleForTesting(visibility = PACKAGE) + @Nullable + public Rect getBottomBound() { + return mBottomBound; + } + + /** + * To count the safe inset according to the cutout bounds and waterfall inset. + * + * @return the safe inset. + */ + @VisibleForTesting(visibility = PACKAGE) + @NonNull + public Rect getSafeInset() { + return mInsets.toRect(); + } + + private static int decideWhichEdge(boolean isTopEdgeShortEdge, + boolean isShortEdge, boolean isStart) { + return (isTopEdgeShortEdge) + ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT)) + : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM)); + } + + /** + * The CutoutSpecification Parser. + */ + @VisibleForTesting(visibility = PACKAGE) + public static class Parser { + private final boolean mIsShortEdgeOnTop; + private final float mDensity; + private final int mDisplayWidth; + private final int mDisplayHeight; + private final Matrix mMatrix; + private Insets mInsets; + private int mSafeInsetLeft; + private int mSafeInsetTop; + private int mSafeInsetRight; + private int mSafeInsetBottom; + + private final Rect mTmpRect = new Rect(); + private final RectF mTmpRectF = new RectF(); + + private boolean mInDp; + + private Path mPath; + private Rect mLeftBound; + private Rect mTopBound; + private Rect mRightBound; + private Rect mBottomBound; + + private boolean mPositionFromLeft = false; + private boolean mPositionFromRight = false; + private boolean mPositionFromBottom = false; + private boolean mPositionFromCenterVertical = false; + + private boolean mBindLeftCutout = false; + private boolean mBindRightCutout = false; + private boolean mBindBottomCutout = false; + + private boolean mIsTouchShortEdgeStart; + private boolean mIsTouchShortEdgeEnd; + private boolean mIsCloserToStartSide; + + /** + * The constructor of the CutoutSpecification parser to parse the specification of cutout. + * @param density the display density. + * @param displayWidth the display width. + * @param displayHeight the display height. + */ + @VisibleForTesting(visibility = PACKAGE) + public Parser(float density, int displayWidth, int displayHeight) { + mDensity = density; + mDisplayWidth = displayWidth; + mDisplayHeight = displayHeight; + mMatrix = new Matrix(); + mIsShortEdgeOnTop = mDisplayWidth < mDisplayHeight; + } + + private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { + mTmpRectF.setEmpty(); + p.computeBounds(mTmpRectF, false /* unused */); + mTmpRectF.round(inoutRect); + inoutRegion.op(inoutRect, Region.Op.UNION); + } + + private void resetStatus(StringBuilder sb) { + sb.setLength(0); + mPositionFromBottom = false; + mPositionFromLeft = false; + mPositionFromRight = false; + mPositionFromCenterVertical = false; + + mBindLeftCutout = false; + mBindRightCutout = false; + mBindBottomCutout = false; + } + + private void translateMatrix() { + final float offsetX; + if (mPositionFromRight) { + offsetX = mDisplayWidth; + } else if (mPositionFromLeft) { + offsetX = 0; + } else { + offsetX = mDisplayWidth / 2f; + } + + final float offsetY; + if (mPositionFromBottom) { + offsetY = mDisplayHeight; + } else if (mPositionFromCenterVertical) { + offsetY = mDisplayHeight / 2f; + } else { + offsetY = 0; + } + + mMatrix.reset(); + if (mInDp) { + mMatrix.postScale(mDensity, mDensity); + } + mMatrix.postTranslate(offsetX, offsetY); + } + + private int computeSafeInsets(int gravity, Rect rect) { + if (gravity == LEFT && rect.right > 0 && rect.right < mDisplayWidth) { + return rect.right; + } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mDisplayHeight) { + return rect.bottom; + } else if (gravity == RIGHT && rect.left > 0 && rect.left < mDisplayWidth) { + return mDisplayWidth - rect.left; + } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mDisplayHeight) { + return mDisplayHeight - rect.top; + } + return 0; + } + + private void setSafeInset(int gravity, int inset) { + if (gravity == LEFT) { + mSafeInsetLeft = inset; + } else if (gravity == TOP) { + mSafeInsetTop = inset; + } else if (gravity == RIGHT) { + mSafeInsetRight = inset; + } else if (gravity == BOTTOM) { + mSafeInsetBottom = inset; + } + } + + private int getSafeInset(int gravity) { + if (gravity == LEFT) { + return mSafeInsetLeft; + } else if (gravity == TOP) { + return mSafeInsetTop; + } else if (gravity == RIGHT) { + return mSafeInsetRight; + } else if (gravity == BOTTOM) { + return mSafeInsetBottom; + } + return 0; + } + + @NonNull + private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) { + final int gravity; + if (isShortEdge) { + gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart); + } else { + if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) { + gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart); + } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) { + gravity = decideWhichEdge(mIsShortEdgeOnTop, true, + mIsCloserToStartSide); + } else { + gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart); + } + } + + int oldSafeInset = getSafeInset(gravity); + int newSafeInset = computeSafeInsets(gravity, rect); + if (oldSafeInset < newSafeInset) { + setSafeInset(gravity, newSafeInset); + } + + return new Rect(rect); + } + + private void setEdgeCutout(@NonNull Path newPath) { + if (mBindRightCutout && mRightBound == null) { + mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect); + } else if (mBindLeftCutout && mLeftBound == null) { + mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect); + } else if (mBindBottomCutout && mBottomBound == null) { + mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect); + } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout) + && mTopBound == null) { + mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect); + } else { + return; + } + + if (mPath != null) { + mPath.addPath(newPath); + } else { + mPath = newPath; + } + } + + private void parseSvgPathSpec(Region region, String spec) { + if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) { + Log.e(TAG, "According to SVG definition, it shouldn't happen"); + return; + } + spec.trim(); + translateMatrix(); + + final Path newPath = PathParser.createPathFromPathData(spec); + newPath.transform(mMatrix); + computeBoundsRectAndAddToRegion(newPath, region, mTmpRect); + + if (DEBUG) { + Log.d(TAG, String.format(Locale.ENGLISH, + "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b", + mPositionFromLeft, mPositionFromRight, mPositionFromBottom, + mPositionFromCenterVertical)); + Log.d(TAG, "region = " + region); + Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath); + } + + if (mTmpRect.isEmpty()) { + return; + } + + if (mIsShortEdgeOnTop) { + mIsTouchShortEdgeStart = mTmpRect.top <= 0; + mIsTouchShortEdgeEnd = mTmpRect.bottom >= mDisplayHeight; + mIsCloserToStartSide = mTmpRect.centerY() < mDisplayHeight / 2; + } else { + mIsTouchShortEdgeStart = mTmpRect.left <= 0; + mIsTouchShortEdgeEnd = mTmpRect.right >= mDisplayWidth; + mIsCloserToStartSide = mTmpRect.centerX() < mDisplayWidth / 2; + } + + setEdgeCutout(newPath); + } + + private void parseSpecWithoutDp(@NonNull String specWithoutDp) { + Region region = Region.obtain(); + StringBuilder sb = null; + int currentIndex = 0; + int lastIndex = 0; + while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) { + if (sb == null) { + sb = new StringBuilder(specWithoutDp.length()); + } + sb.append(specWithoutDp, lastIndex, currentIndex); + + if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) { + if (!mPositionFromRight) { + mPositionFromLeft = true; + } + currentIndex += LEFT_MARKER.length(); + } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) { + if (!mPositionFromLeft) { + mPositionFromRight = true; + } + currentIndex += RIGHT_MARKER.length(); + } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) { + if (!mPositionFromCenterVertical) { + parseSvgPathSpec(region, sb.toString()); + } + currentIndex += BOTTOM_MARKER.length(); + + /* prepare to parse the rest path */ + resetStatus(sb); + mBindBottomCutout = true; + mPositionFromBottom = true; + } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) { + if (!mPositionFromBottom) { + parseSvgPathSpec(region, sb.toString()); + } + currentIndex += CENTER_VERTICAL_MARKER.length(); + + /* prepare to parse the rest path */ + resetStatus(sb); + mPositionFromCenterVertical = true; + } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) { + parseSvgPathSpec(region, sb.toString()); + currentIndex += CUTOUT_MARKER.length(); + + /* prepare to parse the rest path */ + resetStatus(sb); + } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) { + if (!mBindBottomCutout && !mBindRightCutout) { + mBindLeftCutout = true; + } + currentIndex += BIND_LEFT_CUTOUT_MARKER.length(); + } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) { + if (!mBindBottomCutout && !mBindLeftCutout) { + mBindRightCutout = true; + } + currentIndex += BIND_RIGHT_CUTOUT_MARKER.length(); + } else { + currentIndex += 1; + } + + lastIndex = currentIndex; + } + + if (sb == null) { + parseSvgPathSpec(region, specWithoutDp); + } else { + sb.append(specWithoutDp, lastIndex, specWithoutDp.length()); + parseSvgPathSpec(region, sb.toString()); + } + + region.recycle(); + } + + /** + * To parse specification string as the CutoutSpecification. + * + * @param originalSpec the specification string + * @return the CutoutSpecification instance + */ + @VisibleForTesting(visibility = PACKAGE) + public CutoutSpecification parse(@NonNull String originalSpec) { + Objects.requireNonNull(originalSpec); + + int dpIndex = originalSpec.lastIndexOf(DP_MARKER); + mInDp = (dpIndex != -1); + final String spec; + if (dpIndex != -1) { + spec = originalSpec.substring(0, dpIndex) + + originalSpec.substring(dpIndex + DP_MARKER.length()); + } else { + spec = originalSpec; + } + + parseSpecWithoutDp(spec); + + mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom); + return new CutoutSpecification(this); + } + } +} diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index d433591f2b3c..31fc16188814 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -31,18 +31,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.graphics.Insets; -import android.graphics.Matrix; import android.graphics.Path; import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.Region.Op; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.util.Log; import android.util.Pair; -import android.util.PathParser; import android.util.proto.ProtoOutputStream; import com.android.internal.R; @@ -63,10 +57,6 @@ import java.util.List; public final class DisplayCutout { private static final String TAG = "DisplayCutout"; - private static final String BOTTOM_MARKER = "@bottom"; - private static final String DP_MARKER = "@dp"; - private static final String RIGHT_MARKER = "@right"; - private static final String LEFT_MARKER = "@left"; /** * Category for overlays that allow emulating a display cutout on devices that don't have @@ -703,77 +693,16 @@ public final class DisplayCutout { } } - Path p = null; - Rect boundTop = null; - Rect boundBottom = null; - Rect safeInset = new Rect(); - String bottomSpec = null; - if (!TextUtils.isEmpty(spec)) { - spec = spec.trim(); - final float offsetX; - if (spec.endsWith(RIGHT_MARKER)) { - offsetX = displayWidth; - spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim(); - } else if (spec.endsWith(LEFT_MARKER)) { - offsetX = 0; - spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim(); - } else { - offsetX = displayWidth / 2f; - } - final boolean inDp = spec.endsWith(DP_MARKER); - if (inDp) { - spec = spec.substring(0, spec.length() - DP_MARKER.length()); - } - - if (spec.contains(BOTTOM_MARKER)) { - String[] splits = spec.split(BOTTOM_MARKER, 2); - spec = splits[0].trim(); - bottomSpec = splits[1].trim(); - } + spec = spec.trim(); - final Matrix m = new Matrix(); - final Region r = Region.obtain(); - if (!spec.isEmpty()) { - try { - p = PathParser.createPathFromPathData(spec); - } catch (Throwable e) { - Log.wtf(TAG, "Could not inflate cutout: ", e); - } - - if (p != null) { - if (inDp) { - m.postScale(density, density); - } - m.postTranslate(offsetX, 0); - p.transform(m); + CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(density, + displayWidth, displayHeight).parse(spec); + Rect safeInset = cutoutSpec.getSafeInset(); + final Rect boundLeft = cutoutSpec.getLeftBound(); + final Rect boundTop = cutoutSpec.getTopBound(); + final Rect boundRight = cutoutSpec.getRightBound(); + final Rect boundBottom = cutoutSpec.getBottomBound(); - boundTop = new Rect(); - toRectAndAddToRegion(p, r, boundTop); - safeInset.top = boundTop.bottom; - } - } - - if (bottomSpec != null) { - int bottomInset = 0; - Path bottomPath = null; - try { - bottomPath = PathParser.createPathFromPathData(bottomSpec); - } catch (Throwable e) { - Log.wtf(TAG, "Could not inflate bottom cutout: ", e); - } - - if (bottomPath != null) { - // Keep top transform - m.postTranslate(0, displayHeight); - bottomPath.transform(m); - p.addPath(bottomPath); - boundBottom = new Rect(); - toRectAndAddToRegion(bottomPath, r, boundBottom); - bottomInset = displayHeight - boundBottom.top; - } - safeInset.bottom = bottomInset; - } - } if (!waterfallInsets.equals(Insets.NONE)) { safeInset.set( @@ -784,9 +713,9 @@ public final class DisplayCutout { } final DisplayCutout cutout = new DisplayCutout( - safeInset, waterfallInsets, null /* boundLeft */, boundTop, - null /* boundRight */, boundBottom, false /* copyArguments */); - final Pair<Path, DisplayCutout> result = new Pair<>(p, cutout); + safeInset, waterfallInsets, boundLeft, boundTop, + boundRight, boundBottom, false /* copyArguments */); + final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout); synchronized (CACHE_LOCK) { sCachedSpec = spec; sCachedDisplayWidth = displayWidth; @@ -798,14 +727,6 @@ public final class DisplayCutout { return result; } - private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { - final RectF rectF = new RectF(); - p.computeBounds(rectF, false /* unused */); - rectF.round(inoutRect); - inoutRegion.op(inoutRect, Op.UNION); - } - - private static Insets loadWaterfallInset(Resources res) { return Insets.of( res.getDimensionPixelSize(R.dimen.waterfall_display_left_edge_size), diff --git a/core/tests/coretests/src/android/view/CutoutSpecificationTest.java b/core/tests/coretests/src/android/view/CutoutSpecificationTest.java new file mode 100644 index 000000000000..1f831bb2f9a8 --- /dev/null +++ b/core/tests/coretests/src/android/view/CutoutSpecificationTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.graphics.Rect; + +import org.junit.Before; +import org.junit.Test; + +public class CutoutSpecificationTest { + private static final String WITHOUT_BIND_CUTOUT_SPECIFICATION = "M 0,0\n" + + "h 48\n" + + "v 48\n" + + "h -48\n" + + "z\n" + + "@left\n" + + "@center_vertical\n" + + "M 0,0\n" + + "h 48\n" + + "v 48\n" + + "h -48\n" + + "z\n" + + "@left\n" + + "@center_vertical\n" + + "M 0,0\n" + + "h -48\n" + + "v 48\n" + + "h 48\n" + + "z\n" + + "@right\n" + + "@dp"; + private static final String WITH_BIND_CUTOUT_SPECIFICATION = "M 0,0\n" + + "h 48\n" + + "v 48\n" + + "h -48\n" + + "z\n" + + "@left\n" + + "@center_vertical\n" + + "M 0,0\n" + + "h 48\n" + + "v 48\n" + + "h -48\n" + + "z\n" + + "@left\n" + + "@bind_left_cutout\n" + + "@center_vertical\n" + + "M 0,0\n" + + "h -48\n" + + "v 48\n" + + "h 48\n" + + "z\n" + + "@right\n" + + "@bind_right_cutout\n" + + "@dp"; + private static final String CORNER_CUTOUT_SPECIFICATION = "M 0,0\n" + + "h 1\n" + + "v 1\n" + + "h -1\n" + + "z\n" + + "@left\n" + + "@cutout\n" + + "M 0, 0\n" + + "h -2\n" + + "v 2\n" + + "h 2\n" + + "z\n" + + "@right\n" + + "@bind_right_cutout\n" + + "@cutout\n" + + "M 0, 200\n" + + "h 3\n" + + "v -3\n" + + "h -3\n" + + "z\n" + + "@left\n" + + "@bind_left_cutout\n" + + "@bottom\n" + + "M 0, 0\n" + + "h -4\n" + + "v -4\n" + + "h 4\n" + + "z\n" + + "@right\n" + + "@dp"; + + private CutoutSpecification.Parser mParser; + + /** + * Setup the necessary member field used by test methods. + */ + @Before + public void setUp() { + mParser = new CutoutSpecification.Parser(3.5f, 1080, 1920); + } + + @Test + public void parse_nullString_shouldTriggerException() { + assertThrows(NullPointerException.class, () -> mParser.parse(null)); + } + + @Test + public void parse_emptyString_pathShouldBeNull() { + CutoutSpecification cutoutSpecification = mParser.parse(""); + assertThat(cutoutSpecification.getPath()).isNull(); + } + + @Test + public void parse_withoutBindMarker_shouldHaveNoLeftBound() { + CutoutSpecification cutoutSpecification = mParser.parse(WITHOUT_BIND_CUTOUT_SPECIFICATION); + assertThat(cutoutSpecification.getLeftBound()).isNull(); + } + + @Test + public void parse_withoutBindMarker_shouldHaveNoRightBound() { + CutoutSpecification cutoutSpecification = mParser.parse(WITHOUT_BIND_CUTOUT_SPECIFICATION); + assertThat(cutoutSpecification.getRightBound()).isNull(); + } + + @Test + public void parse_withBindMarker_shouldHaveLeftBound() { + CutoutSpecification cutoutSpecification = mParser.parse(WITH_BIND_CUTOUT_SPECIFICATION); + assertThat(cutoutSpecification.getLeftBound()).isEqualTo(new Rect(0, 960, 168, 1128)); + } + + @Test + public void parse_withBindMarker_shouldHaveRightBound() { + CutoutSpecification cutoutSpecification = mParser.parse(WITH_BIND_CUTOUT_SPECIFICATION); + assertThat(cutoutSpecification.getRightBound()).isEqualTo(new Rect(912, 960, 1080, 1128)); + } + + @Test + public void parse_tallCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n" + + "L -48, 0\n" + + "L -44.3940446283, 36.0595537175\n" + + "C -43.5582133885, 44.4178661152 -39.6, 48.0 -31.2, 48.0\n" + + "L 31.2, 48.0\n" + + "C 39.6, 48.0 43.5582133885, 44.4178661152 44.3940446283, 36.0595537175\n" + + "L 48, 0\n" + + "Z\n" + + "@dp"); + + assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(168); + } + + @Test + public void parse_wideCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n" + + "L -72, 0\n" + + "L -69.9940446283, 20.0595537175\n" + + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n" + + "L 56.8, 32.0\n" + + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n" + + "L 72, 0\n" + + "Z\n" + + "@dp"); + + assertThat(cutoutSpecification.getTopBound().width()).isEqualTo(504); + } + + @Test + public void parse_narrowCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n" + + "L -24, 0\n" + + "L -21.9940446283, 20.0595537175\n" + + "C -21.1582133885, 28.4178661152 -17.2, 32.0 -8.8, 32.0\n" + + "L 8.8, 32.0\n" + + "C 17.2, 32.0 21.1582133885, 28.4178661152 21.9940446283, 20.0595537175\n" + + "L 24, 0\n" + + "Z\n" + + "@dp"); + + assertThat(cutoutSpecification.getTopBound().width()).isEqualTo(168); + } + + @Test + public void parse_doubleCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n" + + "L -72, 0\n" + + "L -69.9940446283, 20.0595537175\n" + + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n" + + "L 56.8, 32.0\n" + + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n" + + "L 72, 0\n" + + "Z\n" + + "@bottom\n" + + "M 0,0\n" + + "L -72, 0\n" + + "L -69.9940446283, -20.0595537175\n" + + "C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0\n" + + "L 56.8, -32.0\n" + + "C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20" + + ".0595537175\n" + + "L 72, 0\n" + + "Z\n" + + "@dp"); + + assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(112); + } + + @Test + public void parse_cornerCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n" + + "L -48, 0\n" + + "C -48,48 -48,48 0,48\n" + + "Z\n" + + "@dp\n" + + "@right"); + + assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(168); + } + + @Test + public void parse_holeCutout_shouldBeDone() { + CutoutSpecification cutoutSpecification = mParser.parse("M 20.0,20.0\n" + + "h 136\n" + + "v 136\n" + + "h -136\n" + + "Z\n" + + "@left"); + + assertThat(cutoutSpecification.getSafeInset()).isEqualTo(new Rect(0, 156, 0, 0)); + } + + @Test + public void getSafeInset_shortEdgeIsTopBottom_shouldMatchExpectedInset() { + CutoutSpecification cutoutSpecification = + new CutoutSpecification.Parser(2f, 200, 400) + .parse(CORNER_CUTOUT_SPECIFICATION); + + assertThat(cutoutSpecification.getSafeInset()) + .isEqualTo(new Rect(0, 4, 0, 8)); + } + + @Test + public void getSafeInset_shortEdgeIsLeftRight_shouldMatchExpectedInset() { + CutoutSpecification cutoutSpecification = + new CutoutSpecification.Parser(2f, 400, 200) + .parse(CORNER_CUTOUT_SPECIFICATION); + + assertThat(cutoutSpecification.getSafeInset()) + .isEqualTo(new Rect(6, 0, 8, 0)); + } +} |