2 * Copyright (C) 2019 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.systemui.assist.ui;
19 import static android.view.Surface.ROTATION_0;
20 import static android.view.Surface.ROTATION_180;
21 import static android.view.Surface.ROTATION_270;
22 import static android.view.Surface.ROTATION_90;
24 import android.content.Context;
25 import android.graphics.Matrix;
26 import android.graphics.Path;
27 import android.graphics.PathMeasure;
28 import android.util.Log;
29 import android.util.Pair;
30 import android.view.Surface;
32 import androidx.core.math.MathUtils;
35 * PerimeterPathGuide establishes a coordinate system for drawing paths along the perimeter of the
36 * screen. All positions around the perimeter have a coordinate [0, 1). The origin is the bottom
37 * left corner of the screen, to the right of the curved corner, if any. Coordinates increase
38 * counter-clockwise around the screen.
40 * Non-square screens require PerimeterPathGuide to be notified when the rotation changes, such that
41 * it can recompute the edge lengths for the coordinate system.
43 public class PerimeterPathGuide {
45 private static final String TAG = "PerimeterPathGuide";
48 * For convenience, labels sections of the device perimeter.
50 * Must be listed in CCW order.
63 private final int mDeviceWidthPx;
64 private final int mDeviceHeightPx;
65 private final int mTopCornerRadiusPx;
66 private final int mBottomCornerRadiusPx;
68 private class RegionAttributes {
69 public float absoluteLength;
70 public float normalizedLength;
71 public float endCoordinate;
75 // Allocate a Path and PathMeasure for use by intermediate operations that would otherwise have
76 // to allocate. reset() must be called before using this path, this ensures state from previous
77 // operations is cleared.
78 private final Path mScratchPath = new Path();
79 private final CornerPathRenderer mCornerPathRenderer;
80 private final PathMeasure mScratchPathMeasure = new PathMeasure(mScratchPath, false);
81 private RegionAttributes[] mRegions;
82 private final int mEdgeInset;
83 private int mRotation = ROTATION_0;
85 public PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer,
86 int edgeInset, int screenWidth, int screenHeight) {
87 mCornerPathRenderer = cornerPathRenderer;
88 mDeviceWidthPx = screenWidth;
89 mDeviceHeightPx = screenHeight;
90 mTopCornerRadiusPx = DisplayUtils.getCornerRadiusTop(context);
91 mBottomCornerRadiusPx = DisplayUtils.getCornerRadiusBottom(context);
92 mEdgeInset = edgeInset;
94 mRegions = new RegionAttributes[8];
95 for (int i = 0; i < mRegions.length; i++) {
96 mRegions[i] = new RegionAttributes();
104 * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
105 * Surface.ROTATION_270
107 public void setRotation(int rotation) {
108 if (rotation != mRotation) {
114 mRotation = rotation;
118 Log.e(TAG, "Invalid rotation provided: " + rotation);
124 * Sets path to the section of the perimeter between startCoord and endCoord (measured
125 * counter-clockwise from the bottom left).
127 public void strokeSegment(Path path, float startCoord, float endCoord) {
130 startCoord = ((startCoord % 1) + 1) % 1; // Wrap to the range [0, 1).
131 endCoord = ((endCoord % 1) + 1) % 1; // Wrap to the range [0, 1).
132 boolean outOfOrder = startCoord > endCoord;
135 strokeSegmentInternal(path, startCoord, 1f);
138 strokeSegmentInternal(path, startCoord, endCoord);
142 * Returns the device perimeter in pixels.
144 public float getPerimeterPx() {
146 for (RegionAttributes region : mRegions) {
147 total += region.absoluteLength;
153 * Returns the bottom corner radius in pixels.
155 public float getBottomCornerRadiusPx() {
156 return mBottomCornerRadiusPx;
160 * Given a region and a progress value [0,1] indicating the counter-clockwise progress within
161 * that region, compute the global [0,1) coordinate.
163 public float getCoord(Region region, float progress) {
164 RegionAttributes regionAttributes = mRegions[region.ordinal()];
165 progress = MathUtils.clamp(progress, 0, 1);
166 return regionAttributes.endCoordinate - (1 - progress) * regionAttributes.normalizedLength;
170 * Returns the center of the provided region, relative to the entire perimeter.
172 public float getRegionCenter(Region region) {
173 return getCoord(region, 0.5f);
177 * Returns the width of the provided region, in units relative to the entire perimeter.
179 public float getRegionWidth(Region region) {
180 return mRegions[region.ordinal()].normalizedLength;
184 * Points are expressed in terms of their relative position on the perimeter of the display,
185 * moving counter-clockwise. This method converts a point to clockwise, assisting use cases
186 * such as animating to a point clockwise instead of counter-clockwise.
188 * @param point A point in the range from 0 to 1.
189 * @return A point in the range of -1 to 0 that represents the same location as {@code point}.
191 public static float makeClockwise(float point) {
195 private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) {
196 if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT
197 || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) {
198 return mBottomCornerRadiusPx;
200 return mTopCornerRadiusPx;
203 // Populate mRegions based upon the current rotation value.
204 private void computeRegions() {
205 int screenWidth = mDeviceWidthPx;
206 int screenHeight = mDeviceHeightPx;
208 int rotateMatrix = 0;
217 case Surface.ROTATION_270:
222 Matrix matrix = new Matrix();
223 matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2);
225 if (mRotation == ROTATION_90 || mRotation == Surface.ROTATION_270) {
226 screenHeight = mDeviceWidthPx;
227 screenWidth = mDeviceHeightPx;
228 matrix.postTranslate((mDeviceHeightPx
229 - mDeviceWidthPx) / 2, (mDeviceWidthPx - mDeviceHeightPx) / 2);
232 CornerPathRenderer.Corner screenBottomLeft = getRotatedCorner(
233 CornerPathRenderer.Corner.BOTTOM_LEFT);
234 CornerPathRenderer.Corner screenBottomRight = getRotatedCorner(
235 CornerPathRenderer.Corner.BOTTOM_RIGHT);
236 CornerPathRenderer.Corner screenTopLeft = getRotatedCorner(
237 CornerPathRenderer.Corner.TOP_LEFT);
238 CornerPathRenderer.Corner screenTopRight = getRotatedCorner(
239 CornerPathRenderer.Corner.TOP_RIGHT);
241 mRegions[Region.BOTTOM_LEFT.ordinal()].path =
242 mCornerPathRenderer.getInsetPath(screenBottomLeft, mEdgeInset);
243 mRegions[Region.BOTTOM_RIGHT.ordinal()].path =
244 mCornerPathRenderer.getInsetPath(screenBottomRight, mEdgeInset);
245 mRegions[Region.TOP_RIGHT.ordinal()].path =
246 mCornerPathRenderer.getInsetPath(screenTopRight, mEdgeInset);
247 mRegions[Region.TOP_LEFT.ordinal()].path =
248 mCornerPathRenderer.getInsetPath(screenTopLeft, mEdgeInset);
250 mRegions[Region.BOTTOM_LEFT.ordinal()].path.transform(matrix);
251 mRegions[Region.BOTTOM_RIGHT.ordinal()].path.transform(matrix);
252 mRegions[Region.TOP_RIGHT.ordinal()].path.transform(matrix);
253 mRegions[Region.TOP_LEFT.ordinal()].path.transform(matrix);
256 Path bottomPath = new Path();
257 bottomPath.moveTo(getPhysicalCornerRadius(screenBottomLeft), screenHeight - mEdgeInset);
258 bottomPath.lineTo(screenWidth - getPhysicalCornerRadius(screenBottomRight),
259 screenHeight - mEdgeInset);
260 mRegions[Region.BOTTOM.ordinal()].path = bottomPath;
262 Path topPath = new Path();
263 topPath.moveTo(screenWidth - getPhysicalCornerRadius(screenTopRight), mEdgeInset);
264 topPath.lineTo(getPhysicalCornerRadius(screenTopLeft), mEdgeInset);
265 mRegions[Region.TOP.ordinal()].path = topPath;
267 Path rightPath = new Path();
268 rightPath.moveTo(screenWidth - mEdgeInset,
269 screenHeight - getPhysicalCornerRadius(screenBottomRight));
270 rightPath.lineTo(screenWidth - mEdgeInset, getPhysicalCornerRadius(screenTopRight));
271 mRegions[Region.RIGHT.ordinal()].path = rightPath;
273 Path leftPath = new Path();
274 leftPath.moveTo(mEdgeInset,
275 getPhysicalCornerRadius(screenTopLeft));
276 leftPath.lineTo(mEdgeInset, screenHeight - getPhysicalCornerRadius(screenBottomLeft));
277 mRegions[Region.LEFT.ordinal()].path = leftPath;
279 float perimeterLength = 0;
280 PathMeasure pathMeasure = new PathMeasure();
281 for (int i = 0; i < mRegions.length; i++) {
282 pathMeasure.setPath(mRegions[i].path, false);
283 mRegions[i].absoluteLength = pathMeasure.getLength();
284 perimeterLength += mRegions[i].absoluteLength;
288 for (int i = 0; i < mRegions.length; i++) {
289 mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength;
290 accum += mRegions[i].absoluteLength;
291 mRegions[i].endCoordinate = accum / perimeterLength;
293 // Ensure that the last coordinate is 1. Setting it explicitly to avoid floating point
295 mRegions[mRegions.length - 1].endCoordinate = 1f;
298 private CircularCornerPathRenderer.Corner getRotatedCorner(
299 CircularCornerPathRenderer.Corner screenCorner) {
300 int corner = screenCorner.ordinal();
308 case Surface.ROTATION_270:
312 return CircularCornerPathRenderer.Corner.values()[corner % 4];
315 private void strokeSegmentInternal(Path path, float startCoord, float endCoord) {
316 Pair<Region, Float> startPoint = placePoint(startCoord);
317 Pair<Region, Float> endPoint = placePoint(endCoord);
319 if (startPoint.first.equals(endPoint.first)) {
320 strokeRegion(path, startPoint.first, startPoint.second, endPoint.second);
322 strokeRegion(path, startPoint.first, startPoint.second, 1f);
323 boolean hitStart = false;
324 for (Region r : Region.values()) {
325 if (r.equals(startPoint.first)) {
330 if (!r.equals(endPoint.first)) {
331 strokeRegion(path, r, 0f, 1f);
333 strokeRegion(path, r, 0f, endPoint.second);
341 private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) {
342 if (relativeStart == relativeEnd) {
346 mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false);
347 mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(),
348 relativeEnd * mScratchPathMeasure.getLength(), path, true);
352 * Return the Region where the point is located, and its relative position within that region
354 * Note that we move counterclockwise around the perimeter; for example, a relative position of
356 * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the
359 private Pair<Region, Float> placePoint(float coord) {
360 if (0 > coord || coord > 1) {
361 coord = ((coord % 1) + 1)
362 % 1; // Wrap to the range [0, 1). Inputs of exactly 1 are preserved.
365 Region r = getRegionForPoint(coord);
366 if (r.equals(Region.BOTTOM)) {
367 return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength);
369 float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate;
370 float coordRelativeToRegion =
371 coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength;
372 return Pair.create(r, coordRelativeToRegion);
376 private Region getRegionForPoint(float coord) {
377 // If coord is outside of [0,1], wrap to [0,1).
378 if (coord < 0 || coord > 1) {
379 coord = ((coord % 1) + 1) % 1;
382 for (Region region : Region.values()) {
383 if (coord <= mRegions[region.ordinal()].endCoordinate) {
388 // Should never happen.
389 Log.e(TAG, "Fell out of getRegionForPoint");
390 return Region.BOTTOM;