/* * Copyright 2017 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.google.android.material.shape; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Path; import android.graphics.RectF; import android.os.Build.VERSION_CODES; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.google.android.material.shadow.ShadowRenderer; import java.util.ArrayList; import java.util.List; /** * Represents the descriptive path of a shape. Path segments are stored in sequence so that * transformations can be applied to them when the {@link android.graphics.Path} is produced by the * {@link MaterialShapeDrawable}. */ public class ShapePath { private static final float ANGLE_UP = 270; /** * Degrees measured from the vector [0,1]. * * @hide */ protected static final float ANGLE_LEFT = 180; /** * The x coordinate for the start of the path. Does not change. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float startX; /** * The y coordinate for the start of the path. Does not change. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float startY; /** * The x coordinate for the current end of the path given the previously applied transformation. * Changes internally. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float endX; /** * The y coordinate for the current end of the path given the previously applied transformation. * Changes internally. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float endY; /** * The angle of the start of the last drawn shadow. Changes internally. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * * maintained. */ @Deprecated public float currentShadowAngle; /** * The angle at the end of the final shadow. Changes internally. Do not change. * * @deprecated Use the class methods to interact with this field so internal state can be * * maintained. */ @Deprecated public float endShadowAngle; private final List operations = new ArrayList<>(); private final List shadowCompatOperations = new ArrayList<>(); public ShapePath() { reset(0, 0); } public ShapePath(float startX, float startY) { reset(startX, startY); } /** * Resets the ShapePath using a default shadow. {@link ShapePath#reset(float, float, float, * float)}. */ public void reset(float startX, float startY) { reset(startX, startY, ANGLE_UP, 0); } /** Resets fields given the provided assignment parameters. */ public void reset(float startX, float startY, float shadowStartAngle, float shadowSweepAngle) { setStartX(startX); setStartY(startY); setEndX(startX); setEndY(startY); setCurrentShadowAngle(shadowStartAngle); setEndShadowAngle((shadowStartAngle + shadowSweepAngle) % 360); this.operations.clear(); this.shadowCompatOperations.clear(); } /** * Add a line to the ShapePath. * * @param x the x to which the line should be drawn. * @param y the y to which the line should be drawn. */ public void lineTo(float x, float y) { PathLineOperation operation = new PathLineOperation(); operation.x = x; operation.y = y; operations.add(operation); LineShadowOperation shadowOperation = new LineShadowOperation(operation, getEndX(), getEndY()); // The previous endX and endY is the starting point for this shadow operation. addShadowCompatOperation( shadowOperation, ANGLE_UP + shadowOperation.getAngle(), ANGLE_UP + shadowOperation.getAngle()); setEndX(x); setEndY(y); } /** * Add a quad to the ShapePath. * *

Note: This operation will not draw compatibility shadows. This means no shadow will be drawn * on API < 21 and a shadow will only be drawn on API < 29 if the final path is convex. * * @param controlX the control point x of the arc. * @param controlY the control point y of the arc. * @param toX the end x of the arc. * @param toY the end y of the arc. */ @RequiresApi(VERSION_CODES.LOLLIPOP) public void quadToPoint(float controlX, float controlY, float toX, float toY) { PathQuadOperation operation = new PathQuadOperation(); operation.setControlX(controlX); operation.setControlY(controlY); operation.setEndX(toX); operation.setEndY(toY); operations.add(operation); setEndX(toX); setEndY(toY); } /** * Add an arc to the ShapePath. * * @param left the X coordinate of the left side of the rectangle containing the arc oval. * @param top the Y coordinate of the top of the rectangle containing the arc oval. * @param right the X coordinate of the right side of the rectangle containing the arc oval. * @param bottom the Y coordinate of the bottom of the rectangle containing the arc oval. * @param startAngle start angle of the arc. * @param sweepAngle sweep angle of the arc. */ public void addArc( float left, float top, float right, float bottom, float startAngle, float sweepAngle) { PathArcOperation operation = new PathArcOperation(left, top, right, bottom); operation.setStartAngle(startAngle); operation.setSweepAngle(sweepAngle); operations.add(operation); ArcShadowOperation arcShadowOperation = new ArcShadowOperation(operation); float endAngle = startAngle + sweepAngle; // Flip the startAngle and endAngle when drawing the shadow inside the bounds. They represent // the angles from the center of the circle to the start or end of the arc, respectively. When // the shadow is drawn inside the arc, it is going the opposite direction. boolean drawShadowInsideBounds = sweepAngle < 0; addShadowCompatOperation( arcShadowOperation, drawShadowInsideBounds ? (180 + startAngle) % 360 : startAngle, drawShadowInsideBounds ? (180 + endAngle) % 360 : endAngle); setEndX( (left + right) * 0.5f + (right - left) / 2 * (float) Math.cos(Math.toRadians(startAngle + sweepAngle))); setEndY( (top + bottom) * 0.5f + (bottom - top) / 2 * (float) Math.sin(Math.toRadians(startAngle + sweepAngle))); } /** * Apply the ShapePath sequence to a {@link android.graphics.Path} under a matrix transform. * * @param transform the matrix transform under which this ShapePath is applied * @param path the path to which this ShapePath is applied */ public void applyToPath(Matrix transform, Path path) { for (int i = 0, size = operations.size(); i < size; i++) { PathOperation operation = operations.get(i); operation.applyToPath(transform, path); } } /** * Creates a ShadowCompatOperation to draw compatibility shadow under the matrix transform for the * whole path defined by this ShapePath. */ @NonNull ShadowCompatOperation createShadowCompatOperation(final Matrix transform) { // If the shadowCompatOperations don't end on the desired endShadowAngle, add an arc to do so. addConnectingShadowIfNecessary(getEndShadowAngle()); final List operations = new ArrayList<>(shadowCompatOperations); return new ShadowCompatOperation() { @Override public void draw( Matrix matrix, ShadowRenderer shadowRenderer, int shadowElevation, Canvas canvas) { for (ShadowCompatOperation op : operations) { op.draw(transform, shadowRenderer, shadowElevation, canvas); } } }; } /** * Adds a {@link ShadowCompatOperation}, adding an {@link ArcShadowOperation} if needed in order * to connect the previous shadow end to the new shadow operation's beginning. */ private void addShadowCompatOperation( ShadowCompatOperation shadowOperation, float startShadowAngle, float endShadowAngle) { addConnectingShadowIfNecessary(startShadowAngle); shadowCompatOperations.add(shadowOperation); setCurrentShadowAngle(endShadowAngle); } /** * Create an {@link ArcShadowOperation} to fill in a shadow between the currently drawn shadow and * the next shadow angle, if there would be a gap. */ private void addConnectingShadowIfNecessary(float nextShadowAngle) { if (getCurrentShadowAngle() == nextShadowAngle) { // Previously drawn shadow lines up with the next shadow, so don't draw anything. return; } float shadowSweep = (nextShadowAngle - getCurrentShadowAngle() + 360) % 360; if (shadowSweep > 180) { // Shadows are actually overlapping, so don't draw anything. return; } PathArcOperation pathArcOperation = new PathArcOperation(getEndX(), getEndY(), getEndX(), getEndY()); pathArcOperation.setStartAngle(getCurrentShadowAngle()); pathArcOperation.setSweepAngle(shadowSweep); shadowCompatOperations.add(new ArcShadowOperation(pathArcOperation)); setCurrentShadowAngle(nextShadowAngle); } float getStartX() { return startX; } float getStartY() { return startY; } float getEndX() { return endX; } float getEndY() { return endY; } private float getCurrentShadowAngle() { return currentShadowAngle; } private float getEndShadowAngle() { return endShadowAngle; } private void setStartX(float startX) { this.startX = startX; } private void setStartY(float startY) { this.startY = startY; } private void setEndX(float endX) { this.endX = endX; } private void setEndY(float endY) { this.endY = endY; } private void setCurrentShadowAngle(float currentShadowAngle) { this.currentShadowAngle = currentShadowAngle; } private void setEndShadowAngle(float endShadowAngle) { this.endShadowAngle = endShadowAngle; } /** * Interface to hold operations that will draw a compatible shadow in the case that native shadows * can't be rendered. */ abstract static class ShadowCompatOperation { static final Matrix IDENTITY_MATRIX = new Matrix(); /** Draws the operation on the canvas */ public final void draw(ShadowRenderer shadowRenderer, int shadowElevation, Canvas canvas) { draw(IDENTITY_MATRIX, shadowRenderer, shadowElevation, canvas); } /** Draws the operation with the matrix transform on the canvas */ public abstract void draw( Matrix transform, ShadowRenderer shadowRenderer, int shadowElevation, Canvas canvas); } /** Sets up the correct shadow to be drawn for a line. */ static class LineShadowOperation extends ShadowCompatOperation { private final PathLineOperation operation; private final float startX; private final float startY; public LineShadowOperation(PathLineOperation operation, float startX, float startY) { this.operation = operation; this.startX = startX; this.startY = startY; } @Override public void draw( Matrix transform, @NonNull ShadowRenderer shadowRenderer, int shadowElevation, @NonNull Canvas canvas) { final float height = operation.y - startY; final float width = operation.x - startX; final RectF rect = new RectF(0, 0, (float) Math.hypot(height, width), 0); final Matrix edgeTransform = new Matrix(transform); // transform & rotate the canvas so that the rect passed to drawEdgeShadow is horizontal. edgeTransform.preTranslate(startX, startY); edgeTransform.preRotate(getAngle()); shadowRenderer.drawEdgeShadow(canvas, edgeTransform, rect, shadowElevation); } float getAngle() { return (float) Math.toDegrees(Math.atan((operation.y - startY) / (operation.x - startX))); } } /** Sets up the shadow to be drawn for an arc. */ static class ArcShadowOperation extends ShadowCompatOperation { private final PathArcOperation operation; public ArcShadowOperation(PathArcOperation operation) { this.operation = operation; } @Override public void draw( Matrix transform, @NonNull ShadowRenderer shadowRenderer, int shadowElevation, @NonNull Canvas canvas) { float startAngle = operation.getStartAngle(); float sweepAngle = operation.getSweepAngle(); RectF rect = new RectF( operation.getLeft(), operation.getTop(), operation.getRight(), operation.getBottom()); shadowRenderer.drawCornerShadow( canvas, transform, rect, shadowElevation, startAngle, sweepAngle); } } /** Interface for a path operation to be appended to the operations list. */ public abstract static class PathOperation { /** A usable {@link Matrix} object for transformations. */ protected final Matrix matrix = new Matrix(); /** Applies the given {@code transform} to the provided {@code path}. */ public abstract void applyToPath(Matrix transform, Path path); } /** Straight line operation. */ public static class PathLineOperation extends PathOperation { private float x; private float y; @Override public void applyToPath(@NonNull Matrix transform, @NonNull Path path) { Matrix inverse = matrix; transform.invert(inverse); path.transform(inverse); path.lineTo(x, y); path.transform(transform); } } /** Path quad operation. */ public static class PathQuadOperation extends PathOperation { /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float controlX; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float controlY; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float endX; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float endY; @Override public void applyToPath(@NonNull Matrix transform, @NonNull Path path) { Matrix inverse = matrix; transform.invert(inverse); path.transform(inverse); path.quadTo(getControlX(), getControlY(), getEndX(), getEndY()); path.transform(transform); } private float getEndX() { return endX; } private void setEndX(float endX) { this.endX = endX; } private float getControlY() { return controlY; } private void setControlY(float controlY) { this.controlY = controlY; } private float getEndY() { return endY; } private void setEndY(float endY) { this.endY = endY; } private float getControlX() { return controlX; } private void setControlX(float controlX) { this.controlX = controlX; } } /** Path arc operation. */ public static class PathArcOperation extends PathOperation { private static final RectF rectF = new RectF(); /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float left; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float top; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float right; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float bottom; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float startAngle; /** * @deprecated Use the class methods to interact with this field so internal state can be * maintained. */ @Deprecated public float sweepAngle; public PathArcOperation(float left, float top, float right, float bottom) { setLeft(left); setTop(top); setRight(right); setBottom(bottom); } @Override public void applyToPath(@NonNull Matrix transform, @NonNull Path path) { Matrix inverse = matrix; transform.invert(inverse); path.transform(inverse); rectF.set(getLeft(), getTop(), getRight(), getBottom()); path.arcTo(rectF, getStartAngle(), getSweepAngle(), false); path.transform(transform); } private float getLeft() { return left; } private float getTop() { return top; } private float getRight() { return right; } private float getBottom() { return bottom; } private void setLeft(float left) { this.left = left; } private void setTop(float top) { this.top = top; } private void setRight(float right) { this.right = right; } private void setBottom(float bottom) { this.bottom = bottom; } private float getStartAngle() { return startAngle; } private float getSweepAngle() { return sweepAngle; } private void setStartAngle(float startAngle) { this.startAngle = startAngle; } private void setSweepAngle(float sweepAngle) { this.sweepAngle = sweepAngle; } } }