OSDN Git Service

Merge "docs: Add documentation for equals() method" into qt-dev am: 732a127636
[android-x86/frameworks-base.git] / packages / SystemUI / src / com / android / systemui / assist / ui / PerimeterPathGuide.java
1 /*
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
17 package com.android.systemui.assist.ui;
18
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;
23
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;
31
32 import 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  */
43 public 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         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);
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].absoluteLength;
291             mRegions[i].endCoordinate = accum / perimeterLength;
292         }
293         // Ensure that the last coordinate is 1. Setting it explicitly to avoid floating point
294         // error.
295         mRegions[mRegions.length - 1].endCoordinate = 1f;
296     }
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 }