/* * 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; } } }