Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b9c48d8f authored by Diego Perez's avatar Diego Perez
Browse files

New path interpolation to paint vector drawables

Before this CL, PathMeasure_Delegate would use Path_Delegate.approximate
to get a path segment to draw. Path_Delegate.approximate uses a
flattening iterator to do the path approximation.
Unfortunately, because we do not control the stroke mode while painting,
in some cases the approximation would draw unwanted artifacts caused by
the rough approximation and the use of wrong miter values.
This CL does a much better calculation of the path and interpolates the
segments of the curves instead of replacing them with line segments.

This also fixes an issue with the calculation of empty paths.

Bug: http://b.android.com/187256

Change-Id: I450f7aa4c3d9efcbf902a40c3b4d6d388546893f
parent f5984d5f
Loading
Loading
Loading
Loading
+20 −2
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

@@ -713,8 +712,27 @@ public final class Canvas_Delegate {
                        if (bounds.isEmpty()) {
                            // Apple JRE 1.6 doesn't like drawing empty shapes.
                            // http://b.android.com/178278

                            if (pathDelegate.isEmpty()) {
                                // This means that the path doesn't have any lines or curves so
                                // nothing to draw.
                                return;
                            }

                            // The stroke width is not consider for the size of the bounds so,
                            // for example, a horizontal line, would be considered as an empty
                            // rectangle.
                            // If the strokeWidth is not 0, we use it to consider the size of the
                            // path as well.
                            float strokeWidth = paintDelegate.getStrokeWidth();
                            if (strokeWidth <= 0.0f) {
                                return;
                            }
                            bounds.setRect(bounds.getX(), bounds.getY(),
                                    Math.max(strokeWidth, bounds.getWidth()),
                                    Math.max(strokeWidth, bounds.getHeight()));
                        }

                        int style = paintDelegate.getStyle();

                        if (style == Paint.Style.FILL.nativeInt ||
+1 −5
Original line number Diff line number Diff line
@@ -152,11 +152,7 @@ public class Paint_Delegate {
     * returns the value of stroke miter needed by the java api.
     */
    public float getJavaStrokeMiter() {
        float miter = mStrokeMiter * mStrokeWidth;
        if (miter < 1.f) {
            miter = 1.f;
        }
        return miter;
        return mStrokeMiter;
    }

    public int getJavaCap() {
+76 −63
Original line number Diff line number Diff line
@@ -19,10 +19,12 @@ package android.graphics;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.layoutlib.bridge.util.CachedPathIteratorFactory;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;

import com.android.layoutlib.bridge.util.CachedPathIteratorFactory.CachedPathIterator;

import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;

/**
 * Delegate implementing the native methods of {@link android.graphics.PathMeasure}
@@ -38,35 +40,30 @@ import java.awt.geom.Point2D;
 * @see DelegateManager
 */
public final class PathMeasure_Delegate {

    // ---- delegate manager ----
    private static final DelegateManager<PathMeasure_Delegate> sManager =
            new DelegateManager<PathMeasure_Delegate>(PathMeasure_Delegate.class);

    // ---- delegate data ----
    // This governs how accurate the approximation of the Path is.
    private static final float PRECISION = 0.0002f;
    private CachedPathIteratorFactory mOriginalPathIterator;

    /**
     * Array containing the path points components. There are three components for each point:
     * <ul>
     *     <li>Fraction along the length of the path that the point resides</li>
     *     <li>The x coordinate of the point</li>
     *     <li>The y coordinate of the point</li>
     * </ul>
     */
    private float mPathPoints[];
    private long mNativePath;


    private PathMeasure_Delegate(long native_path, boolean forceClosed) {
        mNativePath = native_path;
        if (forceClosed && mNativePath != 0) {
        if (native_path != 0) {
            if (forceClosed) {
                // Copy the path and call close
            mNativePath = Path_Delegate.init2(native_path);
            Path_Delegate.native_close(mNativePath);
                native_path = Path_Delegate.init2(native_path);
                Path_Delegate.native_close(native_path);
            }

        mPathPoints =
                mNativePath != 0 ? Path_Delegate.native_approximate(mNativePath, PRECISION) : null;
            Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path);
            mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape()
                    .getPathIterator(null));
        }
    }

    @LayoutlibDelegate
@@ -108,13 +105,19 @@ public final class PathMeasure_Delegate {
        PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
        assert pathMeasure != null;

        if (forceClosed && native_path != 0) {
        if (native_path != 0) {
            if (forceClosed) {
                // Copy the path and call close
                native_path = Path_Delegate.init2(native_path);
                Path_Delegate.native_close(native_path);
            }

            Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path);
            pathMeasure.mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape()
                    .getPathIterator(null));
        }

        pathMeasure.mNativePath = native_path;
        pathMeasure.mPathPoints = Path_Delegate.native_approximate(native_path, PRECISION);
    }

    @LayoutlibDelegate
@@ -122,21 +125,11 @@ public final class PathMeasure_Delegate {
        PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
        assert pathMeasure != null;

        if (pathMeasure.mPathPoints == null) {
        if (pathMeasure.mOriginalPathIterator == null) {
            return 0;
        }

        float length = 0;
        int nPoints = pathMeasure.mPathPoints.length / 3;
        for (int i = 1; i < nPoints; i++) {
            length += Point2D.distance(
                    pathMeasure.mPathPoints[(i - 1) * 3 + 1],
                    pathMeasure.mPathPoints[(i - 1) * 3 + 2],
                    pathMeasure.mPathPoints[i*3 + 1],
                    pathMeasure.mPathPoints[i*3 + 2]);
        }

        return length;
        return pathMeasure.mOriginalPathIterator.iterator().getTotalLength();
    }

    @LayoutlibDelegate
@@ -149,13 +142,10 @@ public final class PathMeasure_Delegate {
            return false;
        }

        PathIterator pathIterator = path.getJavaShape().getPathIterator(null);

        int type = 0;
        float segment[] = new float[6];
        while (!pathIterator.isDone()) {
            type = pathIterator.currentSegment(segment);
            pathIterator.next();
        for (PathIterator pi = path.getJavaShape().getPathIterator(null); !pi.isDone(); pi.next()) {
            type = pi.currentSegment(segment);
        }

        // A path is a closed path if the last element is SEG_CLOSE
@@ -176,33 +166,56 @@ public final class PathMeasure_Delegate {
        PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance);
        assert pathMeasure != null;

        if (pathMeasure.mPathPoints == null) {
            return false;
        }

        float accLength = 0;
        CachedPathIterator iterator = pathMeasure.mOriginalPathIterator.iterator();
        float accLength = startD;
        boolean isZeroLength = true; // Whether the output has zero length or not
        int nPoints = pathMeasure.mPathPoints.length / 3;
        for (int i = 0; i < nPoints; i++) {
            float x = pathMeasure.mPathPoints[i * 3 + 1];
            float y = pathMeasure.mPathPoints[i * 3 + 2];
            if (accLength >= startD && accLength <= stopD) {
        float[] points = new float[6];

        iterator.jumpToSegment(accLength);
        while (!iterator.isDone() && (stopD - accLength > 0.1f)) {
            int type = iterator.currentSegment(points, stopD - accLength);

            if (accLength - iterator.getCurrentSegmentLength() <= stopD) {
                if (startWithMoveTo) {
                    startWithMoveTo = false;
                    Path_Delegate.native_moveTo(native_dst_path, x, y);
                } else {
                    isZeroLength = false;
                    Path_Delegate.native_lineTo(native_dst_path, x, y);
                }
            }

            if (i > 0) {
                accLength += Point2D.distance(
                        pathMeasure.mPathPoints[(i - 1) * 3 + 1],
                        pathMeasure.mPathPoints[(i - 1) * 3 + 2],
                        pathMeasure.mPathPoints[i * 3 + 1],
                        pathMeasure.mPathPoints[i * 3 + 2]);
            }
                    // If this segment is a MOVETO, then we just use that one. If not, then we issue
                    // a first moveto
                    if (type != PathIterator.SEG_MOVETO) {
                        float[] lastPoint = new float[2];
                        iterator.getCurrentSegmentEnd(lastPoint);
                        Path_Delegate.native_moveTo(native_dst_path, lastPoint[0], lastPoint[1]);
                    }
                }

                isZeroLength = isZeroLength && iterator.getCurrentSegmentLength() > 0;
                switch (type) {
                    case PathIterator.SEG_MOVETO:
                        Path_Delegate.native_moveTo(native_dst_path, points[0], points[1]);
                        break;
                    case PathIterator.SEG_LINETO:
                        Path_Delegate.native_lineTo(native_dst_path, points[0], points[1]);
                        break;
                    case PathIterator.SEG_CLOSE:
                        Path_Delegate.native_close(native_dst_path);
                        break;
                    case PathIterator.SEG_CUBICTO:
                        Path_Delegate.native_cubicTo(native_dst_path, points[0], points[1],
                                points[2], points[3],
                                points[4], points[5]);
                        break;
                    case PathIterator.SEG_QUADTO:
                        Path_Delegate.native_quadTo(native_dst_path, points[0], points[1],
                                points[2],
                                points[3]);
                        break;
                    default:
                        assert false;
                }
            }

            accLength += iterator.getCurrentSegmentLength();
            iterator.next();
        }

        return !isZeroLength;
+48 −11
Original line number Diff line number Diff line
@@ -57,6 +57,8 @@ public final class Path_Delegate {
    private static final DelegateManager<Path_Delegate> sManager =
            new DelegateManager<Path_Delegate>(Path_Delegate.class);

    private static final float EPSILON = 1e-4f;

    // ---- delegate data ----
    private FillType mFillType = FillType.WINDING;
    private Path2D mPath = new Path2D.Double();
@@ -64,6 +66,9 @@ public final class Path_Delegate {
    private float mLastX = 0;
    private float mLastY = 0;

    // true if the path contains does not contain a curve or line.
    private boolean mCachedIsEmpty = true;

    // ---- Public Helper methods ----

    public static Path_Delegate getDelegate(long nPath) {
@@ -75,7 +80,7 @@ public final class Path_Delegate {
    }

    public void setJavaShape(Shape shape) {
        mPath.reset();
        reset();
        mPath.append(shape, false /*connect*/);
    }

@@ -84,7 +89,7 @@ public final class Path_Delegate {
    }

    public void setPathIterator(PathIterator iterator) {
        mPath.reset();
        reset();
        mPath.append(iterator, false /*connect*/);
    }

@@ -591,11 +596,37 @@ public final class Path_Delegate {


    /**
     * Returns whether the path is empty.
     * @return true if the path is empty.
     * Returns whether the path already contains any points.
     * Note that this is different to
     * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO},
     * {@link #isEmpty} will return true while hasPoints will return false.
     */
    public boolean hasPoints() {
        return !mPath.getPathIterator(null).isDone();
    }

    /**
     * Returns whether the path is empty (contains no lines or curves).
     * @see Path#isEmpty
     */
    private boolean isEmpty() {
        return mPath.getCurrentPoint() == null;
    public boolean isEmpty() {
        if (!mCachedIsEmpty) {
            return false;
        }

        float[] coords = new float[6];
        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;
    }

    /**
@@ -645,7 +676,7 @@ public final class Path_Delegate {
     * @param y The y-coordinate of the end of a line
     */
    private void lineTo(float x, float y) {
        if (isEmpty()) {
        if (!hasPoints()) {
            mPath.moveTo(mLastX = 0, mLastY = 0);
        }
        mPath.lineTo(mLastX = x, mLastY = y);
@@ -662,9 +693,15 @@ public final class Path_Delegate {
     *           this contour, to specify a line
     */
    private void rLineTo(float dx, float dy) {
        if (isEmpty()) {
        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);
@@ -699,7 +736,7 @@ public final class Path_Delegate {
     *            this contour, for the end point of a quadratic curve
     */
    private void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
        if (isEmpty()) {
        if (!hasPoints()) {
            mPath.moveTo(mLastX = 0, mLastY = 0);
        }
        dx1 += mLastX;
@@ -723,7 +760,7 @@ public final class Path_Delegate {
     */
    private void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3) {
        if (isEmpty()) {
        if (!hasPoints()) {
            mPath.moveTo(0, 0);
        }
        mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
@@ -736,7 +773,7 @@ public final class Path_Delegate {
     */
    private void rCubicTo(float dx1, float dy1, float dx2, float dy2,
                         float dx3, float dy3) {
        if (isEmpty()) {
        if (!hasPoints()) {
            mPath.moveTo(mLastX = 0, mLastY = 0);
        }
        dx1 += mLastX;
+485 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading