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 PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer, int edgeInset, int screenWidth, int screenHeight)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 */ setRotation(int rotation)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 */ strokeSegment(Path path, float startCoord, float endCoord)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 */ getPerimeterPx()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 */ getBottomCornerRadiusPx()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 */ getCoord(Region region, float progress)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 */ getRegionCenter(Region region)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 */ getRegionWidth(Region region)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 */ makeClockwise(float point)191 public static float makeClockwise(float point) { 192 return point - 1; 193 } 194 getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner)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. computeRegions()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 getRotatedCorner( CircularCornerPathRenderer.Corner screenCorner)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 strokeSegmentInternal(Path path, float startCoord, float endCoord)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 strokeRegion(Path path, Region r, float relativeStart, float relativeEnd)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 */ placePoint(float coord)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 getRegionForPoint(float coord)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 } 393