From b9c48d8f49d35e2682c7205a9d8d5fcc25d7c736 Mon Sep 17 00:00:00 2001 From: Diego Perez Date: Fri, 18 Dec 2015 16:01:24 +0000 Subject: [PATCH] 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 --- .../src/android/graphics/Canvas_Delegate.java | 22 +- .../src/android/graphics/Paint_Delegate.java | 6 +- .../src/android/graphics/PathMeasure_Delegate.java | 139 +++--- .../bridge/src/android/graphics/Path_Delegate.java | 59 ++- .../bridge/util/CachedPathIteratorFactory.java | 485 +++++++++++++++++++++ .../MyApplication/golden/vector_drawable.png | Bin 0 -> 4939 bytes .../src/main/res/drawable/multi_path.xml | 58 +++ .../src/main/res/layout/vector_drawable.xml | 30 ++ .../android/layoutlib/bridge/intensive/Main.java | 22 +- 9 files changed, 739 insertions(+), 82 deletions(-) create mode 100644 tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java create mode 100644 tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png create mode 100644 tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml create mode 100644 tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java index 64cd5031346e..ba0d399ce52f 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -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 - return; + + 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 || diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java index a545283ea0ca..dbd45c4f68be 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -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() { diff --git a/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java index 3c71233b1df5..fc9b4f772883 100644 --- a/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java @@ -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 sManager = new DelegateManager(PathMeasure_Delegate.class); // ---- delegate data ---- - // This governs how accurate the approximation of the Path is. - private static final float PRECISION = 0.0002f; - - /** - * Array containing the path points components. There are three components for each point: - *
    - *
  • Fraction along the length of the path that the point resides
  • - *
  • The x coordinate of the point
  • - *
  • The y coordinate of the point
  • - *
- */ - private float mPathPoints[]; + private CachedPathIteratorFactory mOriginalPathIterator; + private long mNativePath; + private PathMeasure_Delegate(long native_path, boolean forceClosed) { mNativePath = native_path; - if (forceClosed && mNativePath != 0) { - // Copy the path and call close - mNativePath = Path_Delegate.init2(native_path); - Path_Delegate.native_close(mNativePath); - } + 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); + } - 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) { - // Copy the path and call close - native_path = Path_Delegate.init2(native_path); - Path_Delegate.native_close(native_path); + 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 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]); + } } - } - 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]); + 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; diff --git a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java index a2a53fef7f1f..d0dd22f8faad 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java @@ -57,6 +57,8 @@ public final class Path_Delegate { private static final DelegateManager sManager = new DelegateManager(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. */ - private boolean isEmpty() { - return mPath.getCurrentPoint() == null; + public boolean hasPoints() { + return !mPath.getPathIterator(null).isDone(); + } + + /** + * Returns whether the path is empty (contains no lines or curves). + * @see Path#isEmpty + */ + 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; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java new file mode 100644 index 000000000000..0a9b9ecde2f8 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2015 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.android.layoutlib.bridge.util; + +import android.annotation.NonNull; + +import java.awt.geom.CubicCurve2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.QuadCurve2D; +import java.util.ArrayList; + +import com.google.android.collect.Lists; + +/** + * Class that returns iterators for a given path. These iterators are lightweight and can be reused + * multiple times to iterate over the path. + */ +public class CachedPathIteratorFactory { + /* + * A few conventions used in the code: + * Coordinates or coords arrays store segment coordinates. They use the same format as + * PathIterator#currentSegment coordinates array. + * float arrays store always points where the first element is X and the second is Y. + */ + + // This governs how accurate the approximation of the Path is. + private static final float PRECISION = 0.002f; + + private final int mWindingRule; + private final int[] mTypes; + private final float[][] mCoordinates; + private final float[] mSegmentsLength; + private final float mTotalLength; + + public CachedPathIteratorFactory(@NonNull PathIterator iterator) { + mWindingRule = iterator.getWindingRule(); + + ArrayList typesArray = Lists.newArrayList(); + ArrayList pointsArray = Lists.newArrayList(); + float[] points = new float[6]; + while (!iterator.isDone()) { + int type = iterator.currentSegment(points); + int nPoints = getNumberOfPoints(type) * 2; // 2 coordinates per point + + typesArray.add(type); + float[] itemPoints = new float[nPoints]; + System.arraycopy(points, 0, itemPoints, 0, nPoints); + pointsArray.add(itemPoints); + iterator.next(); + } + + mTypes = new int[typesArray.size()]; + mCoordinates = new float[mTypes.length][]; + for (int i = 0; i < typesArray.size(); i++) { + mTypes[i] = typesArray.get(i); + mCoordinates[i] = pointsArray.get(i); + } + + // Do measurement + mSegmentsLength = new float[mTypes.length]; + + // Curves that we can reuse to estimate segments length + CubicCurve2D.Float cubicCurve = new CubicCurve2D.Float(); + QuadCurve2D.Float quadCurve = new QuadCurve2D.Float(); + float lastX = 0; + float lastY = 0; + float totalLength = 0; + for (int i = 0; i < mTypes.length; i++) { + switch (mTypes[i]) { + case PathIterator.SEG_CUBICTO: + cubicCurve.setCurve(lastX, lastY, + mCoordinates[i][0], mCoordinates[i][1], mCoordinates[i][2], + mCoordinates[i][3], lastX = mCoordinates[i][4], + lastY = mCoordinates[i][5]); + mSegmentsLength[i] = + getFlatPathLength(cubicCurve.getPathIterator(null, PRECISION)); + break; + case PathIterator.SEG_QUADTO: + quadCurve.setCurve(lastX, lastY, mCoordinates[i][0], mCoordinates[i][1], + lastX = mCoordinates[i][2], lastY = mCoordinates[i][3]); + mSegmentsLength[i] = + getFlatPathLength(quadCurve.getPathIterator(null, PRECISION)); + break; + case PathIterator.SEG_CLOSE: + mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY, + lastX = mCoordinates[0][0], + lastY = mCoordinates[0][1]); + mCoordinates[i] = new float[2]; + // We convert a SEG_CLOSE segment to a SEG_LINETO so we do not have to worry + // about this special case in the rest of the code. + mTypes[i] = PathIterator.SEG_LINETO; + mCoordinates[i][0] = mCoordinates[0][0]; + mCoordinates[i][1] = mCoordinates[0][1]; + break; + case PathIterator.SEG_MOVETO: + mSegmentsLength[i] = 0; + lastX = mCoordinates[i][0]; + lastY = mCoordinates[i][1]; + break; + case PathIterator.SEG_LINETO: + mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY, mCoordinates[i][0], + mCoordinates[i][1]); + lastX = mCoordinates[i][0]; + lastY = mCoordinates[i][1]; + default: + } + totalLength += mSegmentsLength[i]; + } + + mTotalLength = totalLength; + } + + private static void quadCurveSegment(float[] coords, float t0, float t1) { + // Calculate X and Y at 0.5 (We'll use this to reconstruct the control point later) + float mt = t0 + (t1 - t0) / 2; + float mu = 1 - mt; + float mx = mu * mu * coords[0] + 2 * mu * mt * coords[2] + mt * mt * coords[4]; + float my = mu * mu * coords[1] + 2 * mu * mt * coords[3] + mt * mt * coords[5]; + + float u0 = 1 - t0; + float u1 = 1 - t1; + + // coords at t0 + coords[0] = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; + coords[1] = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; + + // coords at t1 + coords[4] = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; + coords[5] = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; + + // estimated control point at t'=0.5 + coords[2] = 2 * mx - coords[0] / 2 - coords[4] / 2; + coords[3] = 2 * my - coords[1] / 2 - coords[5] / 2; + } + + private static void cubicCurveSegment(float[] coords, float t0, float t1) { + // http://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment + float u0 = 1 - t0; + float u1 = 1 - t1; + + // Calculate the points at t0 and t1 for the quadratic curves formed by (P0, P1, P2) and + // (P1, P2, P3) + float qxa = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; + float qxb = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; + float qxc = coords[2] * u0 * u0 + coords[4] * 2 * t0 * u0 + coords[6] * t0 * t0; + float qxd = coords[2] * u1 * u1 + coords[4] * 2 * t1 * u1 + coords[6] * t1 * t1; + + float qya = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; + float qyb = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; + float qyc = coords[3] * u0 * u0 + coords[5] * 2 * t0 * u0 + coords[7] * t0 * t0; + float qyd = coords[3] * u1 * u1 + coords[5] * 2 * t1 * u1 + coords[7] * t1 * t1; + + // Linear interpolation + coords[0] = qxa * u0 + qxc * t0; + coords[1] = qya * u0 + qyc * t0; + + coords[2] = qxa * u1 + qxc * t1; + coords[3] = qya * u1 + qyc * t1; + + coords[4] = qxb * u0 + qxd * t0; + coords[5] = qyb * u0 + qyd * t0; + + coords[6] = qxb * u1 + qxd * t1; + coords[7] = qyb * u1 + qyd * t1; + } + + /** + * Returns the end point of a given segment + * + * @param type the segment type + * @param coords the segment coordinates array + * @param point the return array where the point will be stored + */ + private static void getShapeEndPoint(int type, @NonNull float[] coords, @NonNull float[] + point) { + // start index of the end point for the segment type + int pointIndex = (getNumberOfPoints(type) - 1) * 2; + point[0] = coords[pointIndex]; + point[1] = coords[pointIndex + 1]; + } + + /** + * Returns the number of points stored in a coordinates array for the given segment type. + */ + private static int getNumberOfPoints(int segmentType) { + switch (segmentType) { + case PathIterator.SEG_QUADTO: + return 2; + case PathIterator.SEG_CUBICTO: + return 3; + case PathIterator.SEG_CLOSE: + return 0; + default: + return 1; + } + } + + /** + * Returns the estimated length of a flat path. If the passed path is not flat (i.e. contains a + * segment that is not {@link PathIterator#SEG_CLOSE}, {@link PathIterator#SEG_MOVETO} or {@link + * PathIterator#SEG_LINETO} this method will fail. + */ + private static float getFlatPathLength(@NonNull PathIterator iterator) { + float segment[] = new float[6]; + float totalLength = 0; + float[] previousPoint = new float[2]; + boolean isFirstPoint = true; + + while (!iterator.isDone()) { + int type = iterator.currentSegment(segment); + assert type == PathIterator.SEG_LINETO || type == PathIterator.SEG_CLOSE || type == + PathIterator.SEG_MOVETO; + + // MoveTo shouldn't affect the length + if (!isFirstPoint && type != PathIterator.SEG_MOVETO) { + totalLength += Point2D.distance(previousPoint[0], previousPoint[1], segment[0], + segment[1]); + } else { + isFirstPoint = false; + } + previousPoint[0] = segment[0]; + previousPoint[1] = segment[1]; + iterator.next(); + } + + return totalLength; + } + + /** + * Returns the estimated position along a path of the given length. + */ + private void getPointAtLength(int type, @NonNull float[] coords, float lastX, float + lastY, float t, @NonNull float[] point) { + if (type == PathIterator.SEG_LINETO) { + point[0] = lastX + (coords[0] - lastX) * t; + point[1] = lastY + (coords[1] - lastY) * t; + // Return here, since we do not need a shape to estimate + return; + } + + float[] curve = new float[8]; + int lastPointIndex = (getNumberOfPoints(type) - 1) * 2; + + System.arraycopy(coords, 0, curve, 2, coords.length); + curve[0] = lastX; + curve[1] = lastY; + if (type == PathIterator.SEG_CUBICTO) { + cubicCurveSegment(curve, 0f, t); + } else { + quadCurveSegment(curve, 0f, t); + } + + point[0] = curve[2 + lastPointIndex]; + point[1] = curve[2 + lastPointIndex + 1]; + } + + public CachedPathIterator iterator() { + return new CachedPathIterator(); + } + + /** + * Class that allows us to iterate over a path multiple times + */ + public class CachedPathIterator implements PathIterator { + private int mNextIndex; + + /** + * Current segment type. + * + * @see PathIterator + */ + private int mCurrentType; + + /** + * Stores the coordinates array of the current segment. The number of points stored depends + * on the segment type. + * + * @see PathIterator + */ + private float[] mCurrentCoords = new float[6]; + private float mCurrentSegmentLength; + + /** + * Current segment length offset. When asking for the length of the current segment, the + * length will be reduced by this amount. This is useful when we are only using portions of + * the segment. + * + * @see #jumpToSegment(float) + */ + private float mOffsetLength; + + /** Point where the current segment started */ + private float[] mLastPoint = new float[2]; + private boolean isIteratorDone; + + private CachedPathIterator() { + next(); + } + + public float getCurrentSegmentLength() { + return mCurrentSegmentLength; + } + + @Override + public int getWindingRule() { + return mWindingRule; + } + + @Override + public boolean isDone() { + return isIteratorDone; + } + + @Override + public void next() { + if (mNextIndex >= mTypes.length) { + isIteratorDone = true; + return; + } + + if (mNextIndex >= 1) { + // We've already called next() once so there is a previous segment in this path. + // We want to get the coordinates where the path ends. + getShapeEndPoint(mCurrentType, mCurrentCoords, mLastPoint); + } else { + // This is the first segment, no previous point so initialize to 0, 0 + mLastPoint[0] = mLastPoint[1] = 0f; + } + mCurrentType = mTypes[mNextIndex]; + mCurrentSegmentLength = mSegmentsLength[mNextIndex] - mOffsetLength; + + if (mOffsetLength > 0f && (mCurrentType == SEG_CUBICTO || mCurrentType == SEG_QUADTO)) { + // We need to skip part of the start of the current segment (because + // mOffsetLength > 0) + float[] points = new float[8]; + + if (mNextIndex < 1) { + points[0] = points[1] = 0f; + } else { + getShapeEndPoint(mTypes[mNextIndex - 1], mCoordinates[mNextIndex - 1], points); + } + + System.arraycopy(mCoordinates[mNextIndex], 0, points, 2, + mCoordinates[mNextIndex].length); + float t0 = (mSegmentsLength[mNextIndex] - mCurrentSegmentLength) / + mSegmentsLength[mNextIndex]; + if (mCurrentType == SEG_CUBICTO) { + cubicCurveSegment(points, t0, 1f); + } else { + quadCurveSegment(points, t0, 1f); + } + System.arraycopy(points, 2, mCurrentCoords, 0, mCoordinates[mNextIndex].length); + } else { + System.arraycopy(mCoordinates[mNextIndex], 0, mCurrentCoords, 0, + mCoordinates[mNextIndex].length); + } + + mOffsetLength = 0f; + mNextIndex++; + } + + @Override + public int currentSegment(float[] coords) { + System.arraycopy(mCurrentCoords, 0, coords, 0, getNumberOfPoints(mCurrentType) * 2); + return mCurrentType; + } + + @Override + public int currentSegment(double[] coords) { + throw new UnsupportedOperationException(); + } + + /** + * Returns the point where the current segment ends + */ + public void getCurrentSegmentEnd(float[] point) { + point[0] = mLastPoint[0]; + point[1] = mLastPoint[1]; + } + + /** + * Restarts the iterator and jumps all the segments of this path up to the length value. + */ + public void jumpToSegment(float length) { + isIteratorDone = false; + if (length <= 0f) { + mNextIndex = 0; + return; + } + + float accLength = 0; + float lastPoint[] = new float[2]; + for (mNextIndex = 0; mNextIndex < mTypes.length; mNextIndex++) { + float segmentLength = mSegmentsLength[mNextIndex]; + if (accLength + segmentLength >= length && mTypes[mNextIndex] != SEG_MOVETO) { + float[] estimatedPoint = new float[2]; + getPointAtLength(mTypes[mNextIndex], + mCoordinates[mNextIndex], lastPoint[0], lastPoint[1], + (length - accLength) / segmentLength, + estimatedPoint); + + // This segment makes us go further than length so we go back one step, + // set a moveto and offset the length of the next segment by the length + // of this segment that we've already used. + mCurrentType = PathIterator.SEG_MOVETO; + mCurrentCoords[0] = estimatedPoint[0]; + mCurrentCoords[1] = estimatedPoint[1]; + mCurrentSegmentLength = 0; + + // We need to offset next path length to account for the segment we've just + // skipped. + mOffsetLength = length - accLength; + return; + } + accLength += segmentLength; + getShapeEndPoint(mTypes[mNextIndex], mCoordinates[mNextIndex], lastPoint); + } + } + + /** + * Returns the current segment up to certain length. If the current segment is shorter than + * length, then the whole segment is returned. The segment coordinates are copied into the + * coords array. + * + * @return the segment type + */ + public int currentSegment(@NonNull float[] coords, float length) { + int type = currentSegment(coords); + // If the length is greater than the current segment length, no need to find + // the cut point. Same if this is a SEG_MOVETO. + if (mCurrentSegmentLength <= length || type == SEG_MOVETO) { + return type; + } + + float t = length / getCurrentSegmentLength(); + + // We find at which offset the end point is located within the coords array and set + // a new end point to cut the segment short + switch (type) { + case SEG_CUBICTO: + case SEG_QUADTO: + float[] curve = new float[8]; + curve[0] = mLastPoint[0]; + curve[1] = mLastPoint[1]; + System.arraycopy(coords, 0, curve, 2, coords.length); + if (type == SEG_CUBICTO) { + cubicCurveSegment(curve, 0f, t); + } else { + quadCurveSegment(curve, 0f, t); + } + System.arraycopy(curve, 2, coords, 0, coords.length); + break; + default: + float[] point = new float[2]; + getPointAtLength(type, coords, mLastPoint[0], mLastPoint[1], t, point); + coords[0] = point[0]; + coords[1] = point[1]; + } + + return type; + } + + /** + * Returns the total length of the path + */ + public float getTotalLength() { + return mTotalLength; + } + } +} diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png new file mode 100644 index 0000000000000000000000000000000000000000..72b87abfb91781d2080fdbef32ec17a1e20a24f8 GIT binary patch literal 4939 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 PkPM)!Wu#fF4u1JROrKhT literal 0 HcmV?d00001 diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml new file mode 100644 index 000000000000..ffc70dc1e519 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml new file mode 100644 index 000000000000..2ce4f4cce919 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java index b1b375908dad..fe16a3ed8459 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java @@ -355,7 +355,7 @@ public class Main { renderAndVerify(params, "expand_horz_layout.png"); } - /** Test expand_layout.xml */ + /** Test indeterminate_progressbar.xml */ @Test public void testVectorAnimation() throws ClassNotFoundException { // Create the layout pull parser. @@ -380,6 +380,26 @@ public class Main { } /** + * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives + * for vector drawables (lines, moves and cubic and quadratic curves). + */ + @Test + public void testVectorDrawable() throws ClassNotFoundException { + // Create the layout pull parser. + LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + + "vector_drawable.xml"); + // Create LayoutLibCallback. + LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); + layoutLibCallback.initResources(); + + SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, + layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, + RenderingMode.V_SCROLL, 22); + + renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2)); + } + + /** * Create a new rendering session and test that rendering the given layout doesn't throw any * exceptions and matches the provided image. *

-- 2.11.0