New path interpolation to paint vector drawables
am: b9c48d8f49
* commit 'b9c48d8f49d35e2682c7205a9d8d5fcc25d7c736':
New path interpolation to paint vector drawables
This commit is contained in:
@@ -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 ||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user