blob: 65a9fcc3a95524d222b7656f53f81deb6c74e69c [file] [log] [blame]
Miranda Kephart433c8112019-05-22 12:25:51 -04001/*
2 * Copyright (C) 2019 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 com.android.systemui.assist.ui;
18
19import static android.view.Surface.ROTATION_0;
20import static android.view.Surface.ROTATION_180;
21import static android.view.Surface.ROTATION_270;
22import static android.view.Surface.ROTATION_90;
23
24import android.content.Context;
25import android.graphics.Matrix;
26import android.graphics.Path;
27import android.graphics.PathMeasure;
28import android.util.Log;
29import android.util.Pair;
30import android.view.Surface;
31
32import androidx.core.math.MathUtils;
33
34/**
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.
39 *
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.
42 */
43public class PerimeterPathGuide {
44
45 private static final String TAG = "PerimeterPathGuide";
46
47 /**
48 * For convenience, labels sections of the device perimeter.
49 *
50 * Must be listed in CCW order.
51 */
52 public enum Region {
53 BOTTOM,
54 BOTTOM_RIGHT,
55 RIGHT,
56 TOP_RIGHT,
57 TOP,
58 TOP_LEFT,
59 LEFT,
60 BOTTOM_LEFT
61 }
62
63 private final int mDeviceWidthPx;
64 private final int mDeviceHeightPx;
65 private final int mTopCornerRadiusPx;
66 private final int mBottomCornerRadiusPx;
67
68 private class RegionAttributes {
69 public float absoluteLength;
70 public float normalizedLength;
71 public float endCoordinate;
72 public Path path;
73 }
74
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;
84
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;
93
94 mRegions = new RegionAttributes[8];
95 for (int i = 0; i < mRegions.length; i++) {
96 mRegions[i] = new RegionAttributes();
97 }
98 computeRegions();
99 }
100
101 /**
102 * Sets the rotation.
103 *
104 * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
105 * Surface.ROTATION_270
106 */
107 public void setRotation(int rotation) {
108 if (rotation != mRotation) {
109 switch (rotation) {
110 case ROTATION_0:
111 case ROTATION_90:
112 case ROTATION_180:
113 case ROTATION_270:
114 mRotation = rotation;
115 computeRegions();
116 break;
117 default:
118 Log.e(TAG, "Invalid rotation provided: " + rotation);
119 }
120 }
121 }
122
123 /**
124 * Sets path to the section of the perimeter between startCoord and endCoord (measured
125 * counter-clockwise from the bottom left).
126 */
127 public void strokeSegment(Path path, float startCoord, float endCoord) {
128 path.reset();
129
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;
133
134 if (outOfOrder) {
135 strokeSegmentInternal(path, startCoord, 1f);
136 startCoord = 0;
137 }
138 strokeSegmentInternal(path, startCoord, endCoord);
139 }
140
141 /**
142 * Returns the device perimeter in pixels.
143 */
144 public float getPerimeterPx() {
145 float total = 0;
146 for (RegionAttributes region : mRegions) {
147 total += region.absoluteLength;
148 }
149 return total;
150 }
151
152 /**
153 * Returns the bottom corner radius in pixels.
154 */
155 public float getBottomCornerRadiusPx() {
156 return mBottomCornerRadiusPx;
157 }
158
159 /**
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.
162 */
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;
167 }
168
169 /**
170 * Returns the center of the provided region, relative to the entire perimeter.
171 */
172 public float getRegionCenter(Region region) {
173 return getCoord(region, 0.5f);
174 }
175
176 /**
177 * Returns the width of the provided region, in units relative to the entire perimeter.
178 */
179 public float getRegionWidth(Region region) {
180 return mRegions[region.ordinal()].normalizedLength;
181 }
182
183 /**
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.
187 *
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}.
190 */
191 public static float makeClockwise(float point) {
192 return point - 1;
193 }
194
195 private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) {
196 if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT
197 || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) {
198 return mBottomCornerRadiusPx;
199 }
200 return mTopCornerRadiusPx;
201 }
202
203 // Populate mRegions based upon the current rotation value.
204 private void computeRegions() {
205 int screenWidth = mDeviceWidthPx;
206 int screenHeight = mDeviceHeightPx;
207
208 int rotateMatrix = 0;
209
210 switch (mRotation) {
211 case ROTATION_90:
212 rotateMatrix = -90;
213 break;
214 case ROTATION_180:
215 rotateMatrix = -180;
216 break;
217 case Surface.ROTATION_270:
218 rotateMatrix = -270;
219 break;
220 }
221
222 Matrix matrix = new Matrix();
223 matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2);
224
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);
230 }
231
232 CircularCornerPathRenderer.Corner screenBottomLeft = getRotatedCorner(
233 CircularCornerPathRenderer.Corner.BOTTOM_LEFT);
234 CircularCornerPathRenderer.Corner screenBottomRight = getRotatedCorner(
235 CircularCornerPathRenderer.Corner.BOTTOM_RIGHT);
236 CircularCornerPathRenderer.Corner screenTopLeft = getRotatedCorner(
237 CircularCornerPathRenderer.Corner.TOP_LEFT);
238 CircularCornerPathRenderer.Corner screenTopRight = getRotatedCorner(
239 CircularCornerPathRenderer.Corner.TOP_RIGHT);
240
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);
249
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);
254
255
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;
261
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;
266
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;
272
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;
278
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;
285 }
286
287 float accum = 0;
288 for (int i = 0; i < mRegions.length; i++) {
289 mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength;
Matt Casey59b44012019-06-26 12:02:05 -0400290 accum += mRegions[i].absoluteLength;
291 mRegions[i].endCoordinate = accum / perimeterLength;
Miranda Kephart433c8112019-05-22 12:25:51 -0400292 }
Matt Casey59b44012019-06-26 12:02:05 -0400293 // Ensure that the last coordinate is 1. Setting it explicitly to avoid floating point
294 // error.
295 mRegions[mRegions.length - 1].endCoordinate = 1f;
Miranda Kephart433c8112019-05-22 12:25:51 -0400296 }
297
298 private CircularCornerPathRenderer.Corner getRotatedCorner(
299 CircularCornerPathRenderer.Corner screenCorner) {
300 int corner = screenCorner.ordinal();
301 switch (mRotation) {
302 case ROTATION_90:
303 corner += 3;
304 break;
305 case ROTATION_180:
306 corner += 2;
307 break;
308 case Surface.ROTATION_270:
309 corner += 1;
310 break;
311 }
312 return CircularCornerPathRenderer.Corner.values()[corner % 4];
313 }
314
315 private void strokeSegmentInternal(Path path, float startCoord, float endCoord) {
316 Pair<Region, Float> startPoint = placePoint(startCoord);
317 Pair<Region, Float> endPoint = placePoint(endCoord);
318
319 if (startPoint.first.equals(endPoint.first)) {
320 strokeRegion(path, startPoint.first, startPoint.second, endPoint.second);
321 } else {
322 strokeRegion(path, startPoint.first, startPoint.second, 1f);
323 boolean hitStart = false;
324 for (Region r : Region.values()) {
325 if (r.equals(startPoint.first)) {
326 hitStart = true;
327 continue;
328 }
329 if (hitStart) {
330 if (!r.equals(endPoint.first)) {
331 strokeRegion(path, r, 0f, 1f);
332 } else {
333 strokeRegion(path, r, 0f, endPoint.second);
334 break;
335 }
336 }
337 }
338 }
339 }
340
341 private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) {
342 if (relativeStart == relativeEnd) {
343 return;
344 }
345
346 mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false);
347 mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(),
348 relativeEnd * mScratchPathMeasure.getLength(), path, true);
349 }
350
351 /**
352 * Return the Region where the point is located, and its relative position within that region
353 * (from 0 to 1).
354 * Note that we move counterclockwise around the perimeter; for example, a relative position of
355 * 0 in
356 * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the
357 * right.
358 */
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.
363 }
364
365 Region r = getRegionForPoint(coord);
366 if (r.equals(Region.BOTTOM)) {
367 return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength);
368 } else {
369 float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate;
370 float coordRelativeToRegion =
371 coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength;
372 return Pair.create(r, coordRelativeToRegion);
373 }
374 }
375
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;
380 }
381
382 for (Region region : Region.values()) {
383 if (coord <= mRegions[region.ordinal()].endCoordinate) {
384 return region;
385 }
386 }
387
388 // Should never happen.
389 Log.e(TAG, "Fell out of getRegionForPoint");
390 return Region.BOTTOM;
391 }
392}