blob: 8eea36892aa75e0cfa990d089db4169e6433045d [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;
290 accum += mRegions[i].normalizedLength;
291 mRegions[i].endCoordinate = accum;
292 }
293 }
294
295 private CircularCornerPathRenderer.Corner getRotatedCorner(
296 CircularCornerPathRenderer.Corner screenCorner) {
297 int corner = screenCorner.ordinal();
298 switch (mRotation) {
299 case ROTATION_90:
300 corner += 3;
301 break;
302 case ROTATION_180:
303 corner += 2;
304 break;
305 case Surface.ROTATION_270:
306 corner += 1;
307 break;
308 }
309 return CircularCornerPathRenderer.Corner.values()[corner % 4];
310 }
311
312 private void strokeSegmentInternal(Path path, float startCoord, float endCoord) {
313 Pair<Region, Float> startPoint = placePoint(startCoord);
314 Pair<Region, Float> endPoint = placePoint(endCoord);
315
316 if (startPoint.first.equals(endPoint.first)) {
317 strokeRegion(path, startPoint.first, startPoint.second, endPoint.second);
318 } else {
319 strokeRegion(path, startPoint.first, startPoint.second, 1f);
320 boolean hitStart = false;
321 for (Region r : Region.values()) {
322 if (r.equals(startPoint.first)) {
323 hitStart = true;
324 continue;
325 }
326 if (hitStart) {
327 if (!r.equals(endPoint.first)) {
328 strokeRegion(path, r, 0f, 1f);
329 } else {
330 strokeRegion(path, r, 0f, endPoint.second);
331 break;
332 }
333 }
334 }
335 }
336 }
337
338 private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) {
339 if (relativeStart == relativeEnd) {
340 return;
341 }
342
343 mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false);
344 mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(),
345 relativeEnd * mScratchPathMeasure.getLength(), path, true);
346 }
347
348 /**
349 * Return the Region where the point is located, and its relative position within that region
350 * (from 0 to 1).
351 * Note that we move counterclockwise around the perimeter; for example, a relative position of
352 * 0 in
353 * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the
354 * right.
355 */
356 private Pair<Region, Float> placePoint(float coord) {
357 if (0 > coord || coord > 1) {
358 coord = ((coord % 1) + 1)
359 % 1; // Wrap to the range [0, 1). Inputs of exactly 1 are preserved.
360 }
361
362 Region r = getRegionForPoint(coord);
363 if (r.equals(Region.BOTTOM)) {
364 return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength);
365 } else {
366 float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate;
367 float coordRelativeToRegion =
368 coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength;
369 return Pair.create(r, coordRelativeToRegion);
370 }
371 }
372
373 private Region getRegionForPoint(float coord) {
374 // If coord is outside of [0,1], wrap to [0,1).
375 if (coord < 0 || coord > 1) {
376 coord = ((coord % 1) + 1) % 1;
377 }
378
379 for (Region region : Region.values()) {
380 if (coord <= mRegions[region.ordinal()].endCoordinate) {
381 return region;
382 }
383 }
384
385 // Should never happen.
386 Log.e(TAG, "Fell out of getRegionForPoint");
387 return Region.BOTTOM;
388 }
389}