| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.assist.ui; |
| |
| import static android.view.Surface.ROTATION_0; |
| import static android.view.Surface.ROTATION_180; |
| import static android.view.Surface.ROTATION_270; |
| import static android.view.Surface.ROTATION_90; |
| |
| import android.content.Context; |
| import android.graphics.Matrix; |
| import android.graphics.Path; |
| import android.graphics.PathMeasure; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.Surface; |
| |
| import androidx.core.math.MathUtils; |
| |
| /** |
| * PerimeterPathGuide establishes a coordinate system for drawing paths along the perimeter of the |
| * screen. All positions around the perimeter have a coordinate [0, 1). The origin is the bottom |
| * left corner of the screen, to the right of the curved corner, if any. Coordinates increase |
| * counter-clockwise around the screen. |
| * |
| * Non-square screens require PerimeterPathGuide to be notified when the rotation changes, such that |
| * it can recompute the edge lengths for the coordinate system. |
| */ |
| public class PerimeterPathGuide { |
| |
| private static final String TAG = "PerimeterPathGuide"; |
| |
| /** |
| * For convenience, labels sections of the device perimeter. |
| * |
| * Must be listed in CCW order. |
| */ |
| public enum Region { |
| BOTTOM, |
| BOTTOM_RIGHT, |
| RIGHT, |
| TOP_RIGHT, |
| TOP, |
| TOP_LEFT, |
| LEFT, |
| BOTTOM_LEFT |
| } |
| |
| private final int mDeviceWidthPx; |
| private final int mDeviceHeightPx; |
| private final int mTopCornerRadiusPx; |
| private final int mBottomCornerRadiusPx; |
| |
| private class RegionAttributes { |
| public float absoluteLength; |
| public float normalizedLength; |
| public float endCoordinate; |
| public Path path; |
| } |
| |
| // Allocate a Path and PathMeasure for use by intermediate operations that would otherwise have |
| // to allocate. reset() must be called before using this path, this ensures state from previous |
| // operations is cleared. |
| private final Path mScratchPath = new Path(); |
| private final CornerPathRenderer mCornerPathRenderer; |
| private final PathMeasure mScratchPathMeasure = new PathMeasure(mScratchPath, false); |
| private RegionAttributes[] mRegions; |
| private final int mEdgeInset; |
| private int mRotation = ROTATION_0; |
| |
| public PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer, |
| int edgeInset, int screenWidth, int screenHeight) { |
| mCornerPathRenderer = cornerPathRenderer; |
| mDeviceWidthPx = screenWidth; |
| mDeviceHeightPx = screenHeight; |
| mTopCornerRadiusPx = DisplayUtils.getCornerRadiusTop(context); |
| mBottomCornerRadiusPx = DisplayUtils.getCornerRadiusBottom(context); |
| mEdgeInset = edgeInset; |
| |
| mRegions = new RegionAttributes[8]; |
| for (int i = 0; i < mRegions.length; i++) { |
| mRegions[i] = new RegionAttributes(); |
| } |
| computeRegions(); |
| } |
| |
| /** |
| * Sets the rotation. |
| * |
| * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, |
| * Surface.ROTATION_270 |
| */ |
| public void setRotation(int rotation) { |
| if (rotation != mRotation) { |
| switch (rotation) { |
| case ROTATION_0: |
| case ROTATION_90: |
| case ROTATION_180: |
| case ROTATION_270: |
| mRotation = rotation; |
| computeRegions(); |
| break; |
| default: |
| Log.e(TAG, "Invalid rotation provided: " + rotation); |
| } |
| } |
| } |
| |
| /** |
| * Sets path to the section of the perimeter between startCoord and endCoord (measured |
| * counter-clockwise from the bottom left). |
| */ |
| public void strokeSegment(Path path, float startCoord, float endCoord) { |
| path.reset(); |
| |
| startCoord = ((startCoord % 1) + 1) % 1; // Wrap to the range [0, 1). |
| endCoord = ((endCoord % 1) + 1) % 1; // Wrap to the range [0, 1). |
| boolean outOfOrder = startCoord > endCoord; |
| |
| if (outOfOrder) { |
| strokeSegmentInternal(path, startCoord, 1f); |
| startCoord = 0; |
| } |
| strokeSegmentInternal(path, startCoord, endCoord); |
| } |
| |
| /** |
| * Returns the device perimeter in pixels. |
| */ |
| public float getPerimeterPx() { |
| float total = 0; |
| for (RegionAttributes region : mRegions) { |
| total += region.absoluteLength; |
| } |
| return total; |
| } |
| |
| /** |
| * Returns the bottom corner radius in pixels. |
| */ |
| public float getBottomCornerRadiusPx() { |
| return mBottomCornerRadiusPx; |
| } |
| |
| /** |
| * Given a region and a progress value [0,1] indicating the counter-clockwise progress within |
| * that region, compute the global [0,1) coordinate. |
| */ |
| public float getCoord(Region region, float progress) { |
| RegionAttributes regionAttributes = mRegions[region.ordinal()]; |
| progress = MathUtils.clamp(progress, 0, 1); |
| return regionAttributes.endCoordinate - (1 - progress) * regionAttributes.normalizedLength; |
| } |
| |
| /** |
| * Returns the center of the provided region, relative to the entire perimeter. |
| */ |
| public float getRegionCenter(Region region) { |
| return getCoord(region, 0.5f); |
| } |
| |
| /** |
| * Returns the width of the provided region, in units relative to the entire perimeter. |
| */ |
| public float getRegionWidth(Region region) { |
| return mRegions[region.ordinal()].normalizedLength; |
| } |
| |
| /** |
| * Points are expressed in terms of their relative position on the perimeter of the display, |
| * moving counter-clockwise. This method converts a point to clockwise, assisting use cases |
| * such as animating to a point clockwise instead of counter-clockwise. |
| * |
| * @param point A point in the range from 0 to 1. |
| * @return A point in the range of -1 to 0 that represents the same location as {@code point}. |
| */ |
| public static float makeClockwise(float point) { |
| return point - 1; |
| } |
| |
| private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) { |
| if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT |
| || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) { |
| return mBottomCornerRadiusPx; |
| } |
| return mTopCornerRadiusPx; |
| } |
| |
| // Populate mRegions based upon the current rotation value. |
| private void computeRegions() { |
| int screenWidth = mDeviceWidthPx; |
| int screenHeight = mDeviceHeightPx; |
| |
| int rotateMatrix = 0; |
| |
| switch (mRotation) { |
| case ROTATION_90: |
| rotateMatrix = -90; |
| break; |
| case ROTATION_180: |
| rotateMatrix = -180; |
| break; |
| case Surface.ROTATION_270: |
| rotateMatrix = -270; |
| break; |
| } |
| |
| Matrix matrix = new Matrix(); |
| matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2); |
| |
| if (mRotation == ROTATION_90 || mRotation == Surface.ROTATION_270) { |
| screenHeight = mDeviceWidthPx; |
| screenWidth = mDeviceHeightPx; |
| matrix.postTranslate((mDeviceHeightPx |
| - mDeviceWidthPx) / 2, (mDeviceWidthPx - mDeviceHeightPx) / 2); |
| } |
| |
| CircularCornerPathRenderer.Corner screenBottomLeft = getRotatedCorner( |
| CircularCornerPathRenderer.Corner.BOTTOM_LEFT); |
| CircularCornerPathRenderer.Corner screenBottomRight = getRotatedCorner( |
| CircularCornerPathRenderer.Corner.BOTTOM_RIGHT); |
| CircularCornerPathRenderer.Corner screenTopLeft = getRotatedCorner( |
| CircularCornerPathRenderer.Corner.TOP_LEFT); |
| CircularCornerPathRenderer.Corner screenTopRight = getRotatedCorner( |
| CircularCornerPathRenderer.Corner.TOP_RIGHT); |
| |
| mRegions[Region.BOTTOM_LEFT.ordinal()].path = |
| mCornerPathRenderer.getInsetPath(screenBottomLeft, mEdgeInset); |
| mRegions[Region.BOTTOM_RIGHT.ordinal()].path = |
| mCornerPathRenderer.getInsetPath(screenBottomRight, mEdgeInset); |
| mRegions[Region.TOP_RIGHT.ordinal()].path = |
| mCornerPathRenderer.getInsetPath(screenTopRight, mEdgeInset); |
| mRegions[Region.TOP_LEFT.ordinal()].path = |
| mCornerPathRenderer.getInsetPath(screenTopLeft, mEdgeInset); |
| |
| mRegions[Region.BOTTOM_LEFT.ordinal()].path.transform(matrix); |
| mRegions[Region.BOTTOM_RIGHT.ordinal()].path.transform(matrix); |
| mRegions[Region.TOP_RIGHT.ordinal()].path.transform(matrix); |
| mRegions[Region.TOP_LEFT.ordinal()].path.transform(matrix); |
| |
| |
| Path bottomPath = new Path(); |
| bottomPath.moveTo(getPhysicalCornerRadius(screenBottomLeft), screenHeight - mEdgeInset); |
| bottomPath.lineTo(screenWidth - getPhysicalCornerRadius(screenBottomRight), |
| screenHeight - mEdgeInset); |
| mRegions[Region.BOTTOM.ordinal()].path = bottomPath; |
| |
| Path topPath = new Path(); |
| topPath.moveTo(screenWidth - getPhysicalCornerRadius(screenTopRight), mEdgeInset); |
| topPath.lineTo(getPhysicalCornerRadius(screenTopLeft), mEdgeInset); |
| mRegions[Region.TOP.ordinal()].path = topPath; |
| |
| Path rightPath = new Path(); |
| rightPath.moveTo(screenWidth - mEdgeInset, |
| screenHeight - getPhysicalCornerRadius(screenBottomRight)); |
| rightPath.lineTo(screenWidth - mEdgeInset, getPhysicalCornerRadius(screenTopRight)); |
| mRegions[Region.RIGHT.ordinal()].path = rightPath; |
| |
| Path leftPath = new Path(); |
| leftPath.moveTo(mEdgeInset, |
| getPhysicalCornerRadius(screenTopLeft)); |
| leftPath.lineTo(mEdgeInset, screenHeight - getPhysicalCornerRadius(screenBottomLeft)); |
| mRegions[Region.LEFT.ordinal()].path = leftPath; |
| |
| float perimeterLength = 0; |
| PathMeasure pathMeasure = new PathMeasure(); |
| for (int i = 0; i < mRegions.length; i++) { |
| pathMeasure.setPath(mRegions[i].path, false); |
| mRegions[i].absoluteLength = pathMeasure.getLength(); |
| perimeterLength += mRegions[i].absoluteLength; |
| } |
| |
| float accum = 0; |
| for (int i = 0; i < mRegions.length; i++) { |
| mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength; |
| accum += mRegions[i].normalizedLength; |
| mRegions[i].endCoordinate = accum; |
| } |
| } |
| |
| private CircularCornerPathRenderer.Corner getRotatedCorner( |
| CircularCornerPathRenderer.Corner screenCorner) { |
| int corner = screenCorner.ordinal(); |
| switch (mRotation) { |
| case ROTATION_90: |
| corner += 3; |
| break; |
| case ROTATION_180: |
| corner += 2; |
| break; |
| case Surface.ROTATION_270: |
| corner += 1; |
| break; |
| } |
| return CircularCornerPathRenderer.Corner.values()[corner % 4]; |
| } |
| |
| private void strokeSegmentInternal(Path path, float startCoord, float endCoord) { |
| Pair<Region, Float> startPoint = placePoint(startCoord); |
| Pair<Region, Float> endPoint = placePoint(endCoord); |
| |
| if (startPoint.first.equals(endPoint.first)) { |
| strokeRegion(path, startPoint.first, startPoint.second, endPoint.second); |
| } else { |
| strokeRegion(path, startPoint.first, startPoint.second, 1f); |
| boolean hitStart = false; |
| for (Region r : Region.values()) { |
| if (r.equals(startPoint.first)) { |
| hitStart = true; |
| continue; |
| } |
| if (hitStart) { |
| if (!r.equals(endPoint.first)) { |
| strokeRegion(path, r, 0f, 1f); |
| } else { |
| strokeRegion(path, r, 0f, endPoint.second); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) { |
| if (relativeStart == relativeEnd) { |
| return; |
| } |
| |
| mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false); |
| mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(), |
| relativeEnd * mScratchPathMeasure.getLength(), path, true); |
| } |
| |
| /** |
| * Return the Region where the point is located, and its relative position within that region |
| * (from 0 to 1). |
| * Note that we move counterclockwise around the perimeter; for example, a relative position of |
| * 0 in |
| * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the |
| * right. |
| */ |
| private Pair<Region, Float> placePoint(float coord) { |
| if (0 > coord || coord > 1) { |
| coord = ((coord % 1) + 1) |
| % 1; // Wrap to the range [0, 1). Inputs of exactly 1 are preserved. |
| } |
| |
| Region r = getRegionForPoint(coord); |
| if (r.equals(Region.BOTTOM)) { |
| return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength); |
| } else { |
| float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate; |
| float coordRelativeToRegion = |
| coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength; |
| return Pair.create(r, coordRelativeToRegion); |
| } |
| } |
| |
| private Region getRegionForPoint(float coord) { |
| // If coord is outside of [0,1], wrap to [0,1). |
| if (coord < 0 || coord > 1) { |
| coord = ((coord % 1) + 1) % 1; |
| } |
| |
| for (Region region : Region.values()) { |
| if (coord <= mRegions[region.ordinal()].endCoordinate) { |
| return region; |
| } |
| } |
| |
| // Should never happen. |
| Log.e(TAG, "Fell out of getRegionForPoint"); |
| return Region.BOTTOM; |
| } |
| } |