org.robolectric.shadows.ShadowLegacyPath Maven / Gradle / Ivy
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO;
import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.RectF;
import android.util.Log;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.List;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
/** The shadow only supports straight-line paths. */
@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = Path.class, isInAndroidSdk = false)
public class ShadowLegacyPath extends ShadowPath {
private static final String TAG = ShadowLegacyPath.class.getSimpleName();
private static final float EPSILON = 1e-4f;
@RealObject private Path realObject;
private List points = new ArrayList<>();
private float mLastX = 0;
private float mLastY = 0;
private Path2D mPath = new Path2D.Double();
private boolean mCachedIsEmpty = true;
private Path.FillType mFillType = Path.FillType.WINDING;
protected boolean isSimplePath;
@Implementation
protected void __constructor__(Path path) {
ShadowLegacyPath shadowPath = extract(path);
points = new ArrayList<>(shadowPath.getPoints());
mPath.append(shadowPath.mPath, /*connect=*/ false);
mFillType = shadowPath.getFillType();
}
Path2D getJavaShape() {
return mPath;
}
@Implementation
protected void moveTo(float x, float y) {
mPath.moveTo(mLastX = x, mLastY = y);
// Legacy recording behavior
Point p = new Point(x, y, MOVE_TO);
points.add(p);
}
@Implementation
protected void lineTo(float x, float y) {
if (!hasPoints()) {
mPath.moveTo(mLastX = 0, mLastY = 0);
}
mPath.lineTo(mLastX = x, mLastY = y);
// Legacy recording behavior
Point point = new Point(x, y, LINE_TO);
points.add(point);
}
@Implementation
protected void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
if (!hasPoints()) {
moveTo(0, 0);
}
mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
}
@Implementation
protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
if (!hasPoints()) {
mPath.moveTo(0, 0);
}
mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
}
private boolean hasPoints() {
return !mPath.getPathIterator(null).isDone();
}
@Implementation
protected void reset() {
mPath.reset();
mLastX = 0;
mLastY = 0;
// Legacy recording behavior
points.clear();
}
@Implementation(minSdk = LOLLIPOP)
protected float[] approximate(float acceptableError) {
PathIterator iterator = mPath.getPathIterator(null, acceptableError);
float segment[] = new float[6];
float totalLength = 0;
ArrayList points = new ArrayList();
Point2D.Float previousPoint = null;
while (!iterator.isDone()) {
int type = iterator.currentSegment(segment);
Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
// MoveTo shouldn't affect the length
if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
totalLength += (float) currentPoint.distance(previousPoint);
}
previousPoint = currentPoint;
points.add(currentPoint);
iterator.next();
}
int nPoints = points.size();
float[] result = new float[nPoints * 3];
previousPoint = null;
// Distance that we've covered so far. Used to calculate the fraction of the path that
// we've covered up to this point.
float walkedDistance = .0f;
for (int i = 0; i < nPoints; i++) {
Point2D.Float point = points.get(i);
float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
walkedDistance += distance;
result[i * 3] = walkedDistance / totalLength;
result[i * 3 + 1] = point.x;
result[i * 3 + 2] = point.y;
previousPoint = point;
}
return result;
}
/**
* @return all the points that have been added to the {@code Path}
*/
@Override
public List getPoints() {
return points;
}
@Implementation
protected void rewind() {
// call out to reset since there's nothing to optimize in
// terms of data structs.
reset();
}
@Implementation
protected void set(Path src) {
mPath.reset();
ShadowLegacyPath shadowSrc = extract(src);
setFillType(shadowSrc.mFillType);
mPath.append(shadowSrc.mPath, false /*connect*/);
}
@Implementation(minSdk = KITKAT)
protected boolean op(Path path1, Path path2, Path.Op op) {
Log.w(TAG, "android.graphics.Path#op() not supported yet.");
return false;
}
@Implementation(minSdk = LOLLIPOP)
protected boolean isConvex() {
Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
return true;
}
@Implementation
protected Path.FillType getFillType() {
return mFillType;
}
@Implementation
protected void setFillType(Path.FillType fillType) {
mFillType = fillType;
mPath.setWindingRule(getWindingRule(fillType));
}
/**
* Returns the Java2D winding rules matching a given Android {@link
* android.graphics.Path.FillType}.
*
* @param type the android fill type
* @return the matching java2d winding rule.
*/
private static int getWindingRule(Path.FillType type) {
switch (type) {
case WINDING:
case INVERSE_WINDING:
return GeneralPath.WIND_NON_ZERO;
case EVEN_ODD:
case INVERSE_EVEN_ODD:
return GeneralPath.WIND_EVEN_ODD;
default:
assert false;
return GeneralPath.WIND_NON_ZERO;
}
}
@Implementation
protected boolean isInverseFillType() {
throw new UnsupportedOperationException("isInverseFillType");
}
@Implementation
protected void toggleInverseFillType() {
throw new UnsupportedOperationException("toggleInverseFillType");
}
@Implementation
protected boolean isEmpty() {
if (!mCachedIsEmpty) {
return false;
}
mCachedIsEmpty = Boolean.TRUE;
for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
// int type = it.currentSegment(coords);
// if (type != PathIterator.SEG_MOVETO) {
// Once we know that the path is not empty, we do not need to check again unless
// Path#reset is called.
mCachedIsEmpty = false;
return false;
// }
}
return true;
}
@Implementation
protected boolean isRect(RectF rect) {
// create an Area that can test if the path is a rect
Area area = new Area(mPath);
if (area.isRectangular()) {
if (rect != null) {
fillBounds(rect);
}
return true;
}
return false;
}
@Implementation
protected void computeBounds(RectF bounds, boolean exact) {
fillBounds(bounds);
}
@Implementation
protected void incReserve(int extraPtCount) {
throw new UnsupportedOperationException("incReserve");
}
@Implementation
protected void rMoveTo(float dx, float dy) {
dx += mLastX;
dy += mLastY;
mPath.moveTo(mLastX = dx, mLastY = dy);
}
@Implementation
protected void rLineTo(float dx, float dy) {
if (!hasPoints()) {
mPath.moveTo(mLastX = 0, mLastY = 0);
}
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
// The delta is so small that this shouldn't generate a line
return;
}
dx += mLastX;
dy += mLastY;
mPath.lineTo(mLastX = dx, mLastY = dy);
}
@Implementation
protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
if (!hasPoints()) {
mPath.moveTo(mLastX = 0, mLastY = 0);
}
dx1 += mLastX;
dy1 += mLastY;
dx2 += mLastX;
dy2 += mLastY;
mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
}
@Implementation
protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
if (!hasPoints()) {
mPath.moveTo(mLastX = 0, mLastY = 0);
}
x1 += mLastX;
y1 += mLastY;
x2 += mLastX;
y2 += mLastY;
x3 += mLastX;
y3 += mLastY;
mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
}
@Implementation
protected void arcTo(RectF oval, float startAngle, float sweepAngle) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
}
@Implementation
protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
}
@Implementation(minSdk = LOLLIPOP)
protected void arcTo(
float left,
float top,
float right,
float bottom,
float startAngle,
float sweepAngle,
boolean forceMoveTo) {
isSimplePath = false;
Arc2D arc =
new Arc2D.Float(
left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN);
mPath.append(arc, true /*connect*/);
if (hasPoints()) {
resetLastPointFromPath();
}
}
@Implementation
protected void close() {
if (!hasPoints()) {
mPath.moveTo(mLastX = 0, mLastY = 0);
}
mPath.closePath();
}
@Implementation
protected void addRect(RectF rect, Direction dir) {
addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
}
@Implementation
protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) {
moveTo(left, top);
switch (dir) {
case CW:
lineTo(right, top);
lineTo(right, bottom);
lineTo(left, bottom);
break;
case CCW:
lineTo(left, bottom);
lineTo(right, bottom);
lineTo(right, top);
break;
}
close();
resetLastPointFromPath();
}
@Implementation(minSdk = LOLLIPOP)
protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) {
mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false);
}
@Implementation
protected void addCircle(float x, float y, float radius, Path.Direction dir) {
mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false);
}
@Implementation(minSdk = LOLLIPOP)
protected void addArc(
float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
mPath.append(
new Arc2D.Float(
left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN),
false);
}
@Implementation(minSdk = JELLY_BEAN)
protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
}
@Implementation(minSdk = JELLY_BEAN)
protected void addRoundRect(RectF rect, float[] radii, Direction dir) {
if (rect == null) {
throw new NullPointerException("need rect parameter");
}
addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
}
@Implementation(minSdk = LOLLIPOP)
protected void addRoundRect(
float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) {
mPath.append(
new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false);
}
@Implementation(minSdk = LOLLIPOP)
protected void addRoundRect(
float left, float top, float right, float bottom, float[] radii, Path.Direction dir) {
if (radii.length < 8) {
throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
}
isSimplePath = false;
float[] cornerDimensions = new float[radii.length];
for (int i = 0; i < radii.length; i++) {
cornerDimensions[i] = 2 * radii[i];
}
mPath.append(
new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false);
}
@Implementation
protected void addPath(Path src, float dx, float dy) {
isSimplePath = false;
ShadowLegacyPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy));
}
@Implementation
protected void addPath(Path src) {
isSimplePath = false;
ShadowLegacyPath.addPath(realObject, src, null);
}
@Implementation
protected void addPath(Path src, Matrix matrix) {
if (matrix == null) {
return;
}
ShadowLegacyPath shadowSrc = extract(src);
if (!shadowSrc.isSimplePath) isSimplePath = false;
ShadowLegacyMatrix shadowMatrix = extract(matrix);
ShadowLegacyPath.addPath(realObject, src, shadowMatrix.getAffineTransform());
}
private static void addPath(Path destPath, Path srcPath, AffineTransform transform) {
if (destPath == null) {
return;
}
if (srcPath == null) {
return;
}
ShadowLegacyPath shadowDestPath = extract(destPath);
ShadowLegacyPath shadowSrcPath = extract(srcPath);
if (transform != null) {
shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false);
} else {
shadowDestPath.mPath.append(shadowSrcPath.mPath, false);
}
}
@Implementation
protected void offset(float dx, float dy, Path dst) {
if (dst != null) {
dst.set(realObject);
} else {
dst = realObject;
}
dst.offset(dx, dy);
}
@Implementation
protected void offset(float dx, float dy) {
GeneralPath newPath = new GeneralPath();
PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
newPath.append(iterator, false /*connect*/);
mPath = newPath;
}
@Implementation
protected void setLastPoint(float dx, float dy) {
mLastX = dx;
mLastY = dy;
}
@Implementation
protected void transform(Matrix matrix, Path dst) {
ShadowLegacyMatrix shadowMatrix = extract(matrix);
if (shadowMatrix.hasPerspective()) {
Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations.");
}
GeneralPath newPath = new GeneralPath();
PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform());
newPath.append(iterator, false /*connect*/);
if (dst != null) {
ShadowLegacyPath shadowPath = extract(dst);
shadowPath.mPath = newPath;
} else {
mPath = newPath;
}
}
@Implementation
protected void transform(Matrix matrix) {
transform(matrix, null);
}
/**
* Fills the given {@link RectF} with the path bounds.
*
* @param bounds the RectF to be filled.
*/
@Override
public void fillBounds(RectF bounds) {
Rectangle2D rect = mPath.getBounds2D();
bounds.left = (float) rect.getMinX();
bounds.right = (float) rect.getMaxX();
bounds.top = (float) rect.getMinY();
bounds.bottom = (float) rect.getMaxY();
}
private void resetLastPointFromPath() {
Point2D last = mPath.getCurrentPoint();
mLastX = (float) last.getX();
mLastY = (float) last.getY();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy