New path interpolation to paint vector drawables

am: b9c48d8f49

* commit 'b9c48d8f49d35e2682c7205a9d8d5fcc25d7c736':
  New path interpolation to paint vector drawables
This commit is contained in:
Diego Perez
2016-01-19 10:35:49 +00:00
committed by android-build-merger
9 changed files with 738 additions and 81 deletions

View File

@@ -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 ||

View File

@@ -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() {

View File

@@ -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) {
// 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]);
}
}
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;
}
}
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]);
}
accLength += iterator.getCurrentSegmentLength();
iterator.next();
}
return !isZeroLength;

View File

@@ -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.
*/
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;

View File

@@ -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<Integer> typesArray = Lists.newArrayList();
ArrayList<float[]> 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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="76dp"
android:width="76dp"
android:viewportHeight="48"
android:viewportWidth="48">
<group
android:name="root"
android:translateX="24.0"
android:translateY="24.0">
<!--
This is the same as the material indeterminate progressbar which involves drawing
several cubic segments
-->
<path
android:pathData="M0, 0 m 0, -19 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38"
android:strokeColor="#00FF00"
android:strokeLineCap="square"
android:strokeLineJoin="miter"
android:strokeWidth="1"
android:trimPathEnd="0.8"
android:trimPathStart="0.3" />
<!-- Same figure with reversed end and start -->
<path
android:pathData="M0, 0 m 0, -12 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38"
android:strokeColor="#FFFF00"
android:strokeLineCap="square"
android:strokeLineJoin="miter"
android:strokeWidth="1"
android:trimPathEnd="0.3"
android:trimPathStart="0.8" />
<!--
Draw a few partial quadratic segments
-->
<path
android:strokeWidth="2"
android:strokeColor="#FFFF00"
android:pathData="M2,2 Q 5 30 15 0"
android:trimPathStart="0.1"
android:trimPathEnd="0.9"
/>
<!--
Draw a line
-->
<path
android:strokeWidth="3"
android:strokeColor="#00FFFF"
android:pathData="M-10,-10 L 10, 10"
android:trimPathStart="0.2"
android:trimPathEnd="0.8"
/>
</group>
</vector>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="16dp"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:src="@drawable/multi_path" />
</LinearLayout>

View File

@@ -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.
@@ -379,6 +379,26 @@ public class Main {
renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3));
}
/**
* 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.