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

Commit 0834e9f2 authored by Chet Haase's avatar Chet Haase
Browse files

Add APIs to make Android Path queryable and more useful

The underlying Skia implementation of Path has APIs for
querying the data in a path (via SkPath::Iter). It also exposes
the ability to add conic sections (conicTo()), which is useful
since a Path can contain conics (even if it was not created with
conics directly).  And it adds a method to get the generationId
from a Path, used to see whether a Path changed since the
last time that Id was retrieved. Finally, it exposes a simple
interpolate() function to allow linear interpolation between
two paths.

This CL adds PathIterator to enable querying Path data, and
adds conicTo, isInterpolatable(), and interpolate() to Path,
all of which wrap the underlying, existing Skia functionality.

Bug: 157391114
Bug: 223586753
Test: Added PathIteratorTest plus new tests on PathTest
Change-Id: Iddadfde24dd75ac04d0a2f3ebca767613af25360
parent 36a571bf
Loading
Loading
Loading
Loading
+112 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.graphics.perftests;

import static android.graphics.PathIterator.VERB_DONE;

import android.graphics.Path;
import android.graphics.PathIterator;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;

import androidx.test.filters.LargeTest;

import org.junit.Rule;
import org.junit.Test;

@LargeTest
public class PathIteratorPerfTest {
    @Rule
    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();

    private Path constructCircularPath(int numSegments) {
        Path path = new Path();
        float angleIncrement = (float) (2 * Math.PI / numSegments);
        float radius = 200f;
        float prevX = 0f, prevY = 0f;
        float angle = 0f;
        for (int i = 0; i <= numSegments; ++i) {
            float x = (float) Math.cos(angle) * radius;
            float y = (float) Math.sin(angle) * radius;
            if (i > 0) {
                path.cubicTo(prevX, prevY, x, y, x, y);
            } else {
                path.moveTo(x, y);
            }
            prevX = x;
            prevY = y;
            angle += angleIncrement;
        }
        return path;
    }

    private void testNextSegmentImpl(int numSegments) {
        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        Path path = constructCircularPath(numSegments);
        while (state.keepRunning()) {
            PathIterator iterator = path.iterator();
            PathIterator.Segment segment = iterator.next();
            while (segment.getVerb() != VERB_DONE) {
                segment = iterator.next();
            }
        }
    }

    private void testNextFloatsImpl(int numSegments) {
        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        float[] points = new float[8];
        Path path = constructCircularPath(numSegments);
        while (state.keepRunning()) {
            PathIterator iterator = path.iterator();
            int verb = iterator.next(points, 0);
            while (verb != VERB_DONE) {
                verb = iterator.next(points, 0);
            }
        }
    }

    @Test
    public void testNextSegment10() {
        testNextSegmentImpl(10);
    }

    @Test
    public void testNextSegment100() {
        testNextSegmentImpl(100);
    }

    @Test
    public void testNextSegment1000() {
        testNextSegmentImpl(1000);
    }

    @Test
    public void testNextArray10() {
        testNextFloatsImpl(10);
    }

    @Test
    public void testNextArray100() {
        testNextFloatsImpl(100);
    }

    @Test
    public void testNextArray1000() {
        testNextFloatsImpl(1000);
    }

}
+28 −1
Original line number Diff line number Diff line
@@ -15111,7 +15111,7 @@ package android.graphics {
    field @NonNull public static final android.os.Parcelable.Creator<android.graphics.ParcelableColorSpace> CREATOR;
  }
  public class Path {
  public class Path implements java.lang.Iterable<android.graphics.PathIterator.Segment> {
    ctor public Path();
    ctor public Path(@Nullable android.graphics.Path);
    method public void addArc(@NonNull android.graphics.RectF, float, float);
@@ -15134,13 +15134,18 @@ package android.graphics {
    method public void arcTo(float, float, float, float, float, float, boolean);
    method public void close();
    method public void computeBounds(@NonNull android.graphics.RectF, boolean);
    method public void conicTo(float, float, float, float, float);
    method public void cubicTo(float, float, float, float, float, float);
    method @NonNull public android.graphics.Path.FillType getFillType();
    method public int getGenerationId();
    method public void incReserve(int);
    method public boolean interpolate(@NonNull android.graphics.Path, float, @NonNull android.graphics.Path);
    method @Deprecated public boolean isConvex();
    method public boolean isEmpty();
    method public boolean isInterpolatable(@NonNull android.graphics.Path);
    method public boolean isInverseFillType();
    method public boolean isRect(@Nullable android.graphics.RectF);
    method @NonNull public android.graphics.PathIterator iterator();
    method public void lineTo(float, float);
    method public void moveTo(float, float);
    method public void offset(float, float, @Nullable android.graphics.Path);
@@ -15148,6 +15153,7 @@ package android.graphics {
    method public boolean op(@NonNull android.graphics.Path, @NonNull android.graphics.Path.Op);
    method public boolean op(@NonNull android.graphics.Path, @NonNull android.graphics.Path, @NonNull android.graphics.Path.Op);
    method public void quadTo(float, float, float, float);
    method public void rConicTo(float, float, float, float, float);
    method public void rCubicTo(float, float, float, float, float, float);
    method public void rLineTo(float, float);
    method public void rMoveTo(float, float);
@@ -15196,6 +15202,27 @@ package android.graphics {
    ctor public PathEffect();
  }
  public class PathIterator implements java.util.Iterator<android.graphics.PathIterator.Segment> {
    method public boolean hasNext();
    method @NonNull public int next(@NonNull float[], int);
    method @NonNull public android.graphics.PathIterator.Segment next();
    method @NonNull public int peek();
    field public static final int VERB_CLOSE = 5; // 0x5
    field public static final int VERB_CONIC = 3; // 0x3
    field public static final int VERB_CUBIC = 4; // 0x4
    field public static final int VERB_DONE = 6; // 0x6
    field public static final int VERB_LINE = 1; // 0x1
    field public static final int VERB_MOVE = 0; // 0x0
    field public static final int VERB_QUAD = 2; // 0x2
  }
  public static class PathIterator.Segment {
    ctor public PathIterator.Segment(@NonNull int, @NonNull float[], float);
    method public float getConicWeight();
    method @NonNull public float[] getPoints();
    method @NonNull public int getVerb();
  }
  public class PathMeasure {
    ctor public PathMeasure();
    ctor public PathMeasure(android.graphics.Path, boolean);
+105 −1
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import libcore.util.NativeAllocationRegistry;
 * (based on the paint's Style), or it can be used for clipping or to draw
 * text on a path.
 */
public class Path {
public class Path implements Iterable<PathIterator.Segment> {

    private static final NativeAllocationRegistry sRegistry =
            NativeAllocationRegistry.createMalloced(
@@ -91,6 +91,17 @@ public class Path {
        nSet(mNativePath, src.mNativePath);
    }

    /**
     * Returns an iterator over the segments of this path.
     *
     * @return the Iterator object
     */
    @NonNull
    @Override
    public PathIterator iterator() {
        return new PathIterator(this);
    }

    /**
     * The logical operations that can be performed when combining two paths.
     *
@@ -382,6 +393,49 @@ public class Path {
        nRQuadTo(mNativePath, dx1, dy1, dx2, dy2);
    }

    /**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2), weighted by <code>weight</code>. If no
     * moveTo() call has been made for this contour, the first point is
     * automatically set to (0,0).
     *
     * A weight of 1 is equivalent to calling {@link #quadTo(float, float, float, float)}.
     * A weight of 0 is equivalent to calling {@link #lineTo(float, float)} to
     * <code>(x1, y1)</code> followed by {@link #lineTo(float, float)} to <code>(x2, y2)</code>.
     *
     * @param x1 The x-coordinate of the control point on a conic curve
     * @param y1 The y-coordinate of the control point on a conic curve
     * @param x2 The x-coordinate of the end point on a conic curve
     * @param y2 The y-coordinate of the end point on a conic curve
     * @param weight The weight of the conic applied to the curve. A value of 1 is equivalent
     *               to a quadratic with the given control and anchor points and a value of 0 is
     *               equivalent to a line to the first and another line to the second point.
     */
    public void conicTo(float x1, float y1, float x2, float y2, float weight) {
        nConicTo(mNativePath, x1, y1, x2, y2, weight);
    }

    /**
     * Same as conicTo, but the coordinates are considered relative to the last
     * point on this contour. If there is no previous point, then a moveTo(0,0)
     * is inserted automatically.
     *
     * @param dx1 The amount to add to the x-coordinate of the last point on
     *            this contour, for the control point of a conic curve
     * @param dy1 The amount to add to the y-coordinate of the last point on
     *            this contour, for the control point of a conic curve
     * @param dx2 The amount to add to the x-coordinate of the last point on
     *            this contour, for the end point of a conic curve
     * @param dy2 The amount to add to the y-coordinate of the last point on
     *            this contour, for the end point of a conic curve
     * @param weight The weight of the conic applied to the curve. A value of 1 is equivalent
     *               to a quadratic with the given control and anchor points and a value of 0 is
     *               equivalent to a line to the first and another line to the second point.
     */
    public void rConicTo(float dx1, float dy1, float dx2, float dy2, float weight) {
        nRConicTo(mNativePath, dx1, dy1, dx2, dy2, weight);
    }

    /**
     * Add a cubic bezier from the last point, approaching control points
     * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
@@ -736,6 +790,46 @@ public class Path {
        return nApproximate(mNativePath, acceptableError);
    }

    /**
     * Returns the generation ID of this path. The generation ID changes
     * whenever the path is modified. This can be used as an efficient way to
     * check if a path has changed.
     *
     * @return The current generation ID for this path
     */
    public int getGenerationId()  {
        return nGetGenerationID(mNativePath);
    }

    /**
     * Two paths can be interpolated, by calling {@link #interpolate(Path, float, Path)}, if they
     * have exactly the same structure. That is, both paths must have the same
     * operations, in the same order. If any of the operations are
     * of type {@link PathIterator#VERB_CONIC}, then the weights of those conics must also match.
     *
     * @param otherPath The other <code>Path</code> being interpolated to from this one.
     * @return true if interpolation is possible, false otherwise
     */
    public boolean isInterpolatable(@NonNull Path otherPath) {
        return nIsInterpolatable(mNativePath, otherPath.mNativePath);
    }

    /**
     * This method will linearly interpolate from this path to <code>otherPath</code> given
     * the interpolation parameter <code>t</code>, returning the result in
     * <code>interpolatedPath</code>. Interpolation will only succeed if the structures of the
     * two paths match exactly, as discussed in {@link #isInterpolatable(Path)}.
     *
     * @param otherPath The other <code>Path</code> being interpolated to.
     * @param t The interpolation parameter. A value of 0 results in a <code>Path</code>
     *          equivalent to this path, a value of 1 results in one equivalent to
     *          <code>otherPath</code>.
     * @param interpolatedPath The interpolated results.
     */
    public boolean interpolate(@NonNull Path otherPath, float t, @NonNull Path interpolatedPath) {
        return nInterpolate(mNativePath, otherPath.mNativePath, t, interpolatedPath.mNativePath);
    }

    // ------------------ Regular JNI ------------------------

    private static native long nInit();
@@ -750,6 +844,10 @@ public class Path {
    private static native void nRLineTo(long nPath, float dx, float dy);
    private static native void nQuadTo(long nPath, float x1, float y1, float x2, float y2);
    private static native void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2);
    private static native void nConicTo(long nPath, float x1, float y1, float x2, float y2,
            float weight);
    private static native void nRConicTo(long nPath, float dx1, float dy1, float dx2, float dy2,
            float weight);
    private static native void nCubicTo(long nPath, float x1, float y1, float x2, float y2,
            float x3, float y3);
    private static native void nRCubicTo(long nPath, float x1, float y1, float x2, float y2,
@@ -777,6 +875,8 @@ public class Path {
    private static native void nTransform(long nPath, long matrix);
    private static native boolean nOp(long path1, long path2, int op, long result);
    private static native float[] nApproximate(long nPath, float error);
    private static native boolean nInterpolate(long startPath, long endPath, float t,
            long interpolatedPath);

    // ------------------ Fast JNI ------------------------

@@ -785,6 +885,10 @@ public class Path {

    // ------------------ Critical JNI ------------------------

    @CriticalNative
    private static native int nGetGenerationID(long nativePath);
    @CriticalNative
    private static native boolean nIsInterpolatable(long startPath, long endPath);
    @CriticalNative
    private static native void nReset(long nPath);
    @CriticalNative
+303 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -345,6 +345,7 @@ cc_defaults {
        "jni/PaintFilter.cpp",
        "jni/Path.cpp",
        "jni/PathEffect.cpp",
        "jni/PathIterator.cpp",
        "jni/PathMeasure.cpp",
        "jni/Picture.cpp",
        "jni/Region.cpp",
Loading