mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
394 lines
13 KiB
Java
394 lines
13 KiB
Java
/*
|
|
* Copyright 2018 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 static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
|
import static com.google.android.material.shape.ShapeAppearanceModel.NUM_CORNERS;
|
|
|
|
import android.graphics.Matrix;
|
|
import android.graphics.Path;
|
|
import android.graphics.Path.Direction;
|
|
import android.graphics.Path.Op;
|
|
import android.graphics.PointF;
|
|
import android.graphics.RectF;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.RestrictTo;
|
|
import androidx.annotation.UiThread;
|
|
|
|
/** A class to convert a {@link ShapeAppearanceModel} to a {@link android.graphics.Path}. */
|
|
public class ShapeAppearancePathProvider {
|
|
|
|
protected static final int TOP_RIGHT_CORNER_INDEX = 0;
|
|
protected static final int BOTTOM_RIGHT_CORNER_INDEX = 1;
|
|
protected static final int BOTTOM_LEFT_CORNER_INDEX = 2;
|
|
protected static final int TOP_LEFT_CORNER_INDEX = 3;
|
|
|
|
private static class Lazy {
|
|
static final ShapeAppearancePathProvider INSTANCE = new ShapeAppearancePathProvider();
|
|
}
|
|
|
|
/**
|
|
* Listener called every time a {@link ShapePath} is created for a corner or an edge treatment.
|
|
*
|
|
* @hide
|
|
*/
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
public interface PathListener {
|
|
|
|
void onCornerPathCreated(ShapePath cornerPath, Matrix transform, int count);
|
|
|
|
void onEdgePathCreated(ShapePath edgePath, Matrix transform, int count);
|
|
}
|
|
|
|
// Inter-method state. This class works under the assumption that there is only one exposed
|
|
// method, the method is responsible for correctly reset state.
|
|
private final ShapePath[] cornerPaths = new ShapePath[NUM_CORNERS];
|
|
private final Matrix[] cornerTransforms = new Matrix[NUM_CORNERS];
|
|
private final Matrix[] edgeTransforms = new Matrix[NUM_CORNERS];
|
|
|
|
// Pre-allocated objects that are re-used several times during path computation and rendering.
|
|
private final PointF pointF = new PointF();
|
|
private final Path overlappedEdgePath = new Path();
|
|
private final Path boundsPath = new Path();
|
|
private final ShapePath shapePath = new ShapePath();
|
|
private final float[] scratch = new float[2];
|
|
private final float[] scratch2 = new float[2];
|
|
private final Path edgePath = new Path();
|
|
private final Path cornerPath = new Path();
|
|
|
|
private boolean edgeIntersectionCheckEnabled = true;
|
|
|
|
public ShapeAppearancePathProvider() {
|
|
for (int i = 0; i < NUM_CORNERS; i++) {
|
|
cornerPaths[i] = new ShapePath();
|
|
cornerTransforms[i] = new Matrix();
|
|
edgeTransforms[i] = new Matrix();
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
@UiThread
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
@NonNull
|
|
public static ShapeAppearancePathProvider getInstance() {
|
|
return Lazy.INSTANCE;
|
|
}
|
|
|
|
/**
|
|
* Writes the given {@link ShapeAppearanceModel} to {@code path}
|
|
*
|
|
* @param shapeAppearanceModel The shape to be applied in the path.
|
|
* @param interpolation the desired interpolation.
|
|
* @param bounds the desired bounds for the path.
|
|
* @param path the returned path out-var.
|
|
*/
|
|
public void calculatePath(
|
|
ShapeAppearanceModel shapeAppearanceModel,
|
|
float interpolation,
|
|
RectF bounds,
|
|
@NonNull Path path) {
|
|
calculatePath(shapeAppearanceModel, interpolation, bounds, null, path);
|
|
}
|
|
|
|
/**
|
|
* Writes the given {@link ShapeAppearanceModel} to {@code path}
|
|
*
|
|
* @param shapeAppearanceModel The shape to be applied in the path.
|
|
* @param interpolation the desired interpolation.
|
|
* @param bounds the desired bounds for the path.
|
|
* @param pathListener the path
|
|
* @param path the returned path out-var.
|
|
* @hide
|
|
*/
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
public void calculatePath(
|
|
ShapeAppearanceModel shapeAppearanceModel,
|
|
float interpolation,
|
|
RectF bounds,
|
|
PathListener pathListener,
|
|
@NonNull Path path) {
|
|
calculatePath(
|
|
shapeAppearanceModel,
|
|
/* cornerSizeOverrides= */ null,
|
|
interpolation,
|
|
bounds,
|
|
pathListener,
|
|
path);
|
|
}
|
|
|
|
/**
|
|
* Writes the given {@link ShapeAppearanceModel} to {@code path}
|
|
*
|
|
* @param shapeAppearanceModel The shape to be applied in the path.
|
|
* @param cornerSizeOverrides the corner sizes to overload the ones from shapeAppearanceModel.
|
|
* @param interpolation the desired interpolation.
|
|
* @param bounds the desired bounds for the path.
|
|
* @param pathListener the path
|
|
* @param path the returned path out-var.
|
|
* @hide
|
|
*/
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
public void calculatePath(
|
|
@NonNull ShapeAppearanceModel shapeAppearanceModel,
|
|
@Nullable float[] cornerSizeOverrides,
|
|
float interpolation,
|
|
RectF bounds,
|
|
PathListener pathListener,
|
|
@NonNull Path path) {
|
|
path.rewind();
|
|
overlappedEdgePath.rewind();
|
|
boundsPath.rewind();
|
|
boundsPath.addRect(bounds, Direction.CW);
|
|
ShapeAppearancePathSpec spec =
|
|
new ShapeAppearancePathSpec(
|
|
shapeAppearanceModel, interpolation, bounds, pathListener, path);
|
|
|
|
// Calculate the transformations (rotations and translations) necessary for each edge and
|
|
// corner treatment.
|
|
for (int index = 0; index < NUM_CORNERS; index++) {
|
|
setCornerPathAndTransform(spec, index, cornerSizeOverrides);
|
|
setEdgePathAndTransform(index);
|
|
}
|
|
|
|
for (int index = 0; index < NUM_CORNERS; index++) {
|
|
appendCornerPath(spec, index);
|
|
appendEdgePath(spec, index);
|
|
}
|
|
|
|
path.close();
|
|
overlappedEdgePath.close();
|
|
|
|
// Union with the edge paths that had an intersection to handle overlaps.
|
|
if (!overlappedEdgePath.isEmpty()) {
|
|
path.op(overlappedEdgePath, Op.UNION);
|
|
}
|
|
}
|
|
|
|
private void setCornerPathAndTransform(
|
|
@NonNull ShapeAppearancePathSpec spec, int index, @Nullable float[] cornerSizes) {
|
|
CornerSize cornerSize =
|
|
cornerSizes == null
|
|
? getCornerSizeForIndex(index, spec.shapeAppearanceModel)
|
|
: new ClampedCornerSize(cornerSizes[index]);
|
|
getCornerTreatmentForIndex(index, spec.shapeAppearanceModel)
|
|
.getCornerPath(cornerPaths[index], 90, spec.interpolation, spec.bounds, cornerSize);
|
|
|
|
float edgeAngle = angleOfEdge(index);
|
|
cornerTransforms[index].reset();
|
|
getCoordinatesOfCorner(index, spec.bounds, pointF);
|
|
cornerTransforms[index].setTranslate(pointF.x, pointF.y);
|
|
cornerTransforms[index].preRotate(edgeAngle);
|
|
}
|
|
|
|
private void setEdgePathAndTransform(int index) {
|
|
scratch[0] = cornerPaths[index].getEndX();
|
|
scratch[1] = cornerPaths[index].getEndY();
|
|
cornerTransforms[index].mapPoints(scratch);
|
|
float edgeAngle = angleOfEdge(index);
|
|
edgeTransforms[index].reset();
|
|
edgeTransforms[index].setTranslate(scratch[0], scratch[1]);
|
|
edgeTransforms[index].preRotate(edgeAngle);
|
|
}
|
|
|
|
private void appendCornerPath(@NonNull ShapeAppearancePathSpec spec, int index) {
|
|
scratch[0] = cornerPaths[index].getStartX();
|
|
scratch[1] = cornerPaths[index].getStartY();
|
|
cornerTransforms[index].mapPoints(scratch);
|
|
if (index == 0) {
|
|
spec.path.moveTo(scratch[0], scratch[1]);
|
|
} else {
|
|
spec.path.lineTo(scratch[0], scratch[1]);
|
|
}
|
|
cornerPaths[index].applyToPath(cornerTransforms[index], spec.path);
|
|
if (spec.pathListener != null) {
|
|
spec.pathListener.onCornerPathCreated(cornerPaths[index], cornerTransforms[index], index);
|
|
}
|
|
}
|
|
|
|
private void appendEdgePath(@NonNull ShapeAppearancePathSpec spec, int index) {
|
|
int nextIndex = (index + 1) % 4;
|
|
scratch[0] = cornerPaths[index].getEndX();
|
|
scratch[1] = cornerPaths[index].getEndY();
|
|
cornerTransforms[index].mapPoints(scratch);
|
|
|
|
scratch2[0] = cornerPaths[nextIndex].getStartX();
|
|
scratch2[1] = cornerPaths[nextIndex].getStartY();
|
|
cornerTransforms[nextIndex].mapPoints(scratch2);
|
|
|
|
float edgeLength = (float) Math.hypot(scratch[0] - scratch2[0], scratch[1] - scratch2[1]);
|
|
// TODO(b/121352029): Remove this -.001f that is currently needed to handle rounding errors
|
|
edgeLength = Math.max(edgeLength - .001f, 0);
|
|
float center = getEdgeCenterForIndex(spec.bounds, index);
|
|
shapePath.reset(0, 0);
|
|
EdgeTreatment edgeTreatment = getEdgeTreatmentForIndex(index, spec.shapeAppearanceModel);
|
|
edgeTreatment.getEdgePath(edgeLength, center, spec.interpolation, shapePath);
|
|
edgePath.reset();
|
|
shapePath.applyToPath(edgeTransforms[index], edgePath);
|
|
|
|
if (edgeIntersectionCheckEnabled
|
|
&& (edgeTreatment.forceIntersection()
|
|
|| pathOverlapsCorner(edgePath, index)
|
|
|| pathOverlapsCorner(edgePath, nextIndex))) {
|
|
|
|
// Calculate the difference between the edge and the bounds to calculate the part of the edge
|
|
// outside of the bounds of the shape.
|
|
edgePath.op(edgePath, boundsPath, Op.DIFFERENCE);
|
|
|
|
// Add a line to the path between the previous corner and this edge.
|
|
// TODO(b/144784590): handle the shadow as well.
|
|
scratch[0] = shapePath.getStartX();
|
|
scratch[1] = shapePath.getStartY();
|
|
edgeTransforms[index].mapPoints(scratch);
|
|
overlappedEdgePath.moveTo(scratch[0], scratch[1]);
|
|
|
|
// Add this to the overlappedEdgePath which will be unioned later.
|
|
shapePath.applyToPath(edgeTransforms[index], overlappedEdgePath);
|
|
} else {
|
|
shapePath.applyToPath(edgeTransforms[index], spec.path);
|
|
}
|
|
|
|
if (spec.pathListener != null) {
|
|
spec.pathListener.onEdgePathCreated(shapePath, edgeTransforms[index], index);
|
|
}
|
|
}
|
|
|
|
private boolean pathOverlapsCorner(Path edgePath, int index) {
|
|
cornerPath.reset();
|
|
cornerPaths[index].applyToPath(cornerTransforms[index], cornerPath);
|
|
|
|
RectF bounds = new RectF();
|
|
edgePath.computeBounds(bounds, /* exact= */ true);
|
|
cornerPath.computeBounds(bounds, /* exact= */ true);
|
|
edgePath.op(cornerPath, Op.INTERSECT);
|
|
edgePath.computeBounds(bounds, /* exact= */ true);
|
|
|
|
return !bounds.isEmpty() || (bounds.width() > 1 && bounds.height() > 1);
|
|
}
|
|
|
|
private float getEdgeCenterForIndex(@NonNull RectF bounds, int index) {
|
|
scratch[0] = cornerPaths[index].endX;
|
|
scratch[1] = cornerPaths[index].endY;
|
|
cornerTransforms[index].mapPoints(scratch);
|
|
switch (index) {
|
|
case 1:
|
|
case 3:
|
|
return Math.abs(bounds.centerX() - scratch[0]);
|
|
case 2:
|
|
case 0:
|
|
default:
|
|
return Math.abs(bounds.centerY() - scratch[1]);
|
|
}
|
|
}
|
|
|
|
private CornerTreatment getCornerTreatmentForIndex(
|
|
int index, @NonNull ShapeAppearanceModel shapeAppearanceModel) {
|
|
switch (index) {
|
|
case 1:
|
|
return shapeAppearanceModel.getBottomRightCorner();
|
|
case 2:
|
|
return shapeAppearanceModel.getBottomLeftCorner();
|
|
case 3:
|
|
return shapeAppearanceModel.getTopLeftCorner();
|
|
case 0:
|
|
default:
|
|
return shapeAppearanceModel.getTopRightCorner();
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
CornerSize getCornerSizeForIndex(int index, @NonNull ShapeAppearanceModel shapeAppearanceModel) {
|
|
switch (index) {
|
|
case 1:
|
|
return shapeAppearanceModel.getBottomRightCornerSize();
|
|
case 2:
|
|
return shapeAppearanceModel.getBottomLeftCornerSize();
|
|
case 3:
|
|
return shapeAppearanceModel.getTopLeftCornerSize();
|
|
case 0:
|
|
default:
|
|
return shapeAppearanceModel.getTopRightCornerSize();
|
|
}
|
|
}
|
|
|
|
private EdgeTreatment getEdgeTreatmentForIndex(
|
|
int index, @NonNull ShapeAppearanceModel shapeAppearanceModel) {
|
|
switch (index) {
|
|
case 1:
|
|
return shapeAppearanceModel.getBottomEdge();
|
|
case 2:
|
|
return shapeAppearanceModel.getLeftEdge();
|
|
case 3:
|
|
return shapeAppearanceModel.getTopEdge();
|
|
case 0:
|
|
default:
|
|
return shapeAppearanceModel.getRightEdge();
|
|
}
|
|
}
|
|
|
|
private void getCoordinatesOfCorner(int index, @NonNull RectF bounds, @NonNull PointF pointF) {
|
|
switch (index) {
|
|
case 1: // bottom-right
|
|
pointF.set(bounds.right, bounds.bottom);
|
|
break;
|
|
case 2: // bottom-left
|
|
pointF.set(bounds.left, bounds.bottom);
|
|
break;
|
|
case 3: // top-left
|
|
pointF.set(bounds.left, bounds.top);
|
|
break;
|
|
case 0: // top-right
|
|
default:
|
|
pointF.set(bounds.right, bounds.top);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private float angleOfEdge(int index) {
|
|
return 90 * ((index + 1) % 4);
|
|
}
|
|
|
|
void setEdgeIntersectionCheckEnable(boolean enable) {
|
|
edgeIntersectionCheckEnabled = enable;
|
|
}
|
|
|
|
/** Necessary information to map a {@link ShapeAppearanceModel} into a Path. */
|
|
static final class ShapeAppearancePathSpec {
|
|
|
|
@NonNull public final ShapeAppearanceModel shapeAppearanceModel;
|
|
@NonNull public final Path path;
|
|
@NonNull public final RectF bounds;
|
|
|
|
@Nullable public final PathListener pathListener;
|
|
|
|
public final float interpolation;
|
|
|
|
ShapeAppearancePathSpec(
|
|
@NonNull ShapeAppearanceModel shapeAppearanceModel,
|
|
float interpolation,
|
|
RectF bounds,
|
|
@Nullable PathListener pathListener,
|
|
Path path) {
|
|
this.pathListener = pathListener;
|
|
this.shapeAppearanceModel = shapeAppearanceModel;
|
|
this.interpolation = interpolation;
|
|
this.bounds = bounds;
|
|
this.path = path;
|
|
}
|
|
}
|
|
}
|