1 package org.robolectric.shadows; 2 3 import java.awt.geom.CubicCurve2D; 4 import java.awt.geom.PathIterator; 5 import java.awt.geom.Point2D; 6 import java.awt.geom.QuadCurve2D; 7 import java.util.ArrayList; 8 9 /** 10 * Class that returns iterators for a given path. These iterators are lightweight and can be reused 11 * multiple times to iterate over the path. 12 * 13 * <p>copied from 14 * https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java 15 */ 16 public class CachedPathIteratorFactory { 17 /* 18 * A few conventions used in the code: 19 * Coordinates or coords arrays store segment coordinates. They use the same format as 20 * PathIterator#currentSegment coordinates array. 21 * float arrays store always points where the first element is X and the second is Y. 22 */ 23 24 // This governs how accurate the approximation of the Path is. 25 private static final float PRECISION = 0.002f; 26 27 private final int mWindingRule; 28 private final int[] mTypes; 29 private final float[][] mCoordinates; 30 private final float[] mSegmentsLength; 31 private final float mTotalLength; 32 33 public CachedPathIteratorFactory(PathIterator iterator) { 34 mWindingRule = iterator.getWindingRule(); 35 36 ArrayList<Integer> typesArray = new ArrayList<>(); 37 ArrayList<float[]> pointsArray = new ArrayList<>(); 38 float[] points = new float[6]; 39 while (!iterator.isDone()) { 40 int type = iterator.currentSegment(points); 41 int nPoints = getNumberOfPoints(type) * 2; // 2 coordinates per point 42 43 typesArray.add(type); 44 float[] itemPoints = new float[nPoints]; 45 System.arraycopy(points, 0, itemPoints, 0, nPoints); 46 pointsArray.add(itemPoints); 47 iterator.next(); 48 } 49 50 mTypes = new int[typesArray.size()]; 51 mCoordinates = new float[mTypes.length][]; 52 for (int i = 0; i < typesArray.size(); i++) { 53 mTypes[i] = typesArray.get(i); 54 mCoordinates[i] = pointsArray.get(i); 55 } 56 57 // Do measurement 58 mSegmentsLength = new float[mTypes.length]; 59 60 // Curves that we can reuse to estimate segments length 61 CubicCurve2D.Float cubicCurve = new CubicCurve2D.Float(); 62 QuadCurve2D.Float quadCurve = new QuadCurve2D.Float(); 63 float lastX = 0; 64 float lastY = 0; 65 float totalLength = 0; 66 for (int i = 0; i < mTypes.length; i++) { 67 switch (mTypes[i]) { 68 case PathIterator.SEG_CUBICTO: 69 cubicCurve.setCurve( 70 lastX, 71 lastY, 72 mCoordinates[i][0], 73 mCoordinates[i][1], 74 mCoordinates[i][2], 75 mCoordinates[i][3], 76 lastX = mCoordinates[i][4], 77 lastY = mCoordinates[i][5]); 78 mSegmentsLength[i] = getFlatPathLength(cubicCurve.getPathIterator(null, PRECISION)); 79 break; 80 case PathIterator.SEG_QUADTO: 81 quadCurve.setCurve( 82 lastX, 83 lastY, 84 mCoordinates[i][0], 85 mCoordinates[i][1], 86 lastX = mCoordinates[i][2], 87 lastY = mCoordinates[i][3]); 88 mSegmentsLength[i] = getFlatPathLength(quadCurve.getPathIterator(null, PRECISION)); 89 break; 90 case PathIterator.SEG_CLOSE: 91 mSegmentsLength[i] = 92 (float) 93 Point2D.distance( 94 lastX, lastY, lastX = mCoordinates[0][0], lastY = mCoordinates[0][1]); 95 mCoordinates[i] = new float[2]; 96 // We convert a SEG_CLOSE segment to a SEG_LINETO so we do not have to worry 97 // about this special case in the rest of the code. 98 mTypes[i] = PathIterator.SEG_LINETO; 99 mCoordinates[i][0] = mCoordinates[0][0]; 100 mCoordinates[i][1] = mCoordinates[0][1]; 101 break; 102 case PathIterator.SEG_MOVETO: 103 mSegmentsLength[i] = 0; 104 lastX = mCoordinates[i][0]; 105 lastY = mCoordinates[i][1]; 106 break; 107 case PathIterator.SEG_LINETO: 108 mSegmentsLength[i] = 109 (float) Point2D.distance(lastX, lastY, mCoordinates[i][0], mCoordinates[i][1]); 110 lastX = mCoordinates[i][0]; 111 lastY = mCoordinates[i][1]; 112 break; 113 default: 114 } 115 totalLength += mSegmentsLength[i]; 116 } 117 118 mTotalLength = totalLength; 119 } 120 121 private static void quadCurveSegment(float[] coords, float t0, float t1) { 122 // Calculate X and Y at 0.5 (We'll use this to reconstruct the control point later) 123 float mt = t0 + (t1 - t0) / 2; 124 float mu = 1 - mt; 125 float mx = mu * mu * coords[0] + 2 * mu * mt * coords[2] + mt * mt * coords[4]; 126 float my = mu * mu * coords[1] + 2 * mu * mt * coords[3] + mt * mt * coords[5]; 127 128 float u0 = 1 - t0; 129 float u1 = 1 - t1; 130 131 // coords at t0 132 coords[0] = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; 133 coords[1] = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; 134 135 // coords at t1 136 coords[4] = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; 137 coords[5] = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; 138 139 // estimated control point at t'=0.5 140 coords[2] = 2 * mx - coords[0] / 2 - coords[4] / 2; 141 coords[3] = 2 * my - coords[1] / 2 - coords[5] / 2; 142 } 143 144 private static void cubicCurveSegment(float[] coords, float t0, float t1) { 145 // http://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment 146 float u0 = 1 - t0; 147 float u1 = 1 - t1; 148 149 // Calculate the points at t0 and t1 for the quadratic curves formed by (P0, P1, P2) and 150 // (P1, P2, P3) 151 float qxa = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; 152 float qxb = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; 153 float qxc = coords[2] * u0 * u0 + coords[4] * 2 * t0 * u0 + coords[6] * t0 * t0; 154 float qxd = coords[2] * u1 * u1 + coords[4] * 2 * t1 * u1 + coords[6] * t1 * t1; 155 156 float qya = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; 157 float qyb = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; 158 float qyc = coords[3] * u0 * u0 + coords[5] * 2 * t0 * u0 + coords[7] * t0 * t0; 159 float qyd = coords[3] * u1 * u1 + coords[5] * 2 * t1 * u1 + coords[7] * t1 * t1; 160 161 // Linear interpolation 162 coords[0] = qxa * u0 + qxc * t0; 163 coords[1] = qya * u0 + qyc * t0; 164 165 coords[2] = qxa * u1 + qxc * t1; 166 coords[3] = qya * u1 + qyc * t1; 167 168 coords[4] = qxb * u0 + qxd * t0; 169 coords[5] = qyb * u0 + qyd * t0; 170 171 coords[6] = qxb * u1 + qxd * t1; 172 coords[7] = qyb * u1 + qyd * t1; 173 } 174 175 /** 176 * Returns the end point of a given segment 177 * 178 * @param type the segment type 179 * @param coords the segment coordinates array 180 * @param point the return array where the point will be stored 181 */ 182 private static void getShapeEndPoint(int type, float[] coords, float[] point) { 183 // start index of the end point for the segment type 184 int pointIndex = (getNumberOfPoints(type) - 1) * 2; 185 point[0] = coords[pointIndex]; 186 point[1] = coords[pointIndex + 1]; 187 } 188 189 /** Returns the number of points stored in a coordinates array for the given segment type. */ 190 private static int getNumberOfPoints(int segmentType) { 191 switch (segmentType) { 192 case PathIterator.SEG_QUADTO: 193 return 2; 194 case PathIterator.SEG_CUBICTO: 195 return 3; 196 case PathIterator.SEG_CLOSE: 197 return 0; 198 default: 199 return 1; 200 } 201 } 202 203 /** 204 * Returns the estimated length of a flat path. If the passed path is not flat (i.e. contains a 205 * segment that is not {@link PathIterator#SEG_CLOSE}, {@link PathIterator#SEG_MOVETO} or {@link 206 * PathIterator#SEG_LINETO} this method will fail. 207 */ 208 private static float getFlatPathLength(PathIterator iterator) { 209 float segment[] = new float[6]; 210 float totalLength = 0; 211 float[] previousPoint = new float[2]; 212 boolean isFirstPoint = true; 213 214 while (!iterator.isDone()) { 215 int type = iterator.currentSegment(segment); 216 assert type == PathIterator.SEG_LINETO 217 || type == PathIterator.SEG_CLOSE 218 || type == PathIterator.SEG_MOVETO; 219 220 // MoveTo shouldn't affect the length 221 if (!isFirstPoint && type != PathIterator.SEG_MOVETO) { 222 totalLength += 223 (float) Point2D.distance(previousPoint[0], previousPoint[1], segment[0], segment[1]); 224 } else { 225 isFirstPoint = false; 226 } 227 previousPoint[0] = segment[0]; 228 previousPoint[1] = segment[1]; 229 iterator.next(); 230 } 231 232 return totalLength; 233 } 234 235 /** Returns the estimated position along a path of the given length. */ 236 private void getPointAtLength( 237 int type, float[] coords, float lastX, float lastY, float t, float[] point) { 238 if (type == PathIterator.SEG_LINETO) { 239 point[0] = lastX + (coords[0] - lastX) * t; 240 point[1] = lastY + (coords[1] - lastY) * t; 241 // Return here, since we do not need a shape to estimate 242 return; 243 } 244 245 float[] curve = new float[8]; 246 int lastPointIndex = (getNumberOfPoints(type) - 1) * 2; 247 248 System.arraycopy(coords, 0, curve, 2, coords.length); 249 curve[0] = lastX; 250 curve[1] = lastY; 251 if (type == PathIterator.SEG_CUBICTO) { 252 cubicCurveSegment(curve, 0f, t); 253 } else { 254 quadCurveSegment(curve, 0f, t); 255 } 256 257 point[0] = curve[2 + lastPointIndex]; 258 point[1] = curve[2 + lastPointIndex + 1]; 259 } 260 261 public CachedPathIterator iterator() { 262 return new CachedPathIterator(); 263 } 264 265 /** Class that allows us to iterate over a path multiple times */ 266 public class CachedPathIterator implements PathIterator { 267 private int mNextIndex; 268 269 /** 270 * Current segment type. 271 * 272 * @see PathIterator 273 */ 274 private int mCurrentType; 275 276 /** 277 * Stores the coordinates array of the current segment. The number of points stored depends on 278 * the segment type. 279 * 280 * @see PathIterator 281 */ 282 private float[] mCurrentCoords = new float[6]; 283 284 private float mCurrentSegmentLength; 285 286 /** 287 * Current segment length offset. When asking for the length of the current segment, the length 288 * will be reduced by this amount. This is useful when we are only using portions of the 289 * segment. 290 * 291 * @see #jumpToSegment(float) 292 */ 293 private float mOffsetLength; 294 295 /** Point where the current segment started */ 296 private float[] mLastPoint = new float[2]; 297 298 private boolean isIteratorDone; 299 300 private CachedPathIterator() { 301 next(); 302 } 303 304 public float getCurrentSegmentLength() { 305 return mCurrentSegmentLength; 306 } 307 308 @Override 309 public int getWindingRule() { 310 return mWindingRule; 311 } 312 313 @Override 314 public boolean isDone() { 315 return isIteratorDone; 316 } 317 318 @Override 319 public void next() { 320 if (mNextIndex >= mTypes.length) { 321 isIteratorDone = true; 322 return; 323 } 324 325 if (mNextIndex >= 1) { 326 // We've already called next() once so there is a previous segment in this path. 327 // We want to get the coordinates where the path ends. 328 getShapeEndPoint(mCurrentType, mCurrentCoords, mLastPoint); 329 } else { 330 // This is the first segment, no previous point so initialize to 0, 0 331 mLastPoint[0] = mLastPoint[1] = 0f; 332 } 333 mCurrentType = mTypes[mNextIndex]; 334 mCurrentSegmentLength = mSegmentsLength[mNextIndex] - mOffsetLength; 335 336 if (mOffsetLength > 0f && (mCurrentType == SEG_CUBICTO || mCurrentType == SEG_QUADTO)) { 337 // We need to skip part of the start of the current segment (because 338 // mOffsetLength > 0) 339 float[] points = new float[8]; 340 341 if (mNextIndex < 1) { 342 points[0] = points[1] = 0f; 343 } else { 344 getShapeEndPoint(mTypes[mNextIndex - 1], mCoordinates[mNextIndex - 1], points); 345 } 346 347 System.arraycopy(mCoordinates[mNextIndex], 0, points, 2, mCoordinates[mNextIndex].length); 348 float t0 = 349 (mSegmentsLength[mNextIndex] - mCurrentSegmentLength) / mSegmentsLength[mNextIndex]; 350 if (mCurrentType == SEG_CUBICTO) { 351 cubicCurveSegment(points, t0, 1f); 352 } else { 353 quadCurveSegment(points, t0, 1f); 354 } 355 System.arraycopy(points, 2, mCurrentCoords, 0, mCoordinates[mNextIndex].length); 356 } else { 357 System.arraycopy( 358 mCoordinates[mNextIndex], 0, mCurrentCoords, 0, mCoordinates[mNextIndex].length); 359 } 360 361 mOffsetLength = 0f; 362 mNextIndex++; 363 } 364 365 @Override 366 public int currentSegment(float[] coords) { 367 System.arraycopy(mCurrentCoords, 0, coords, 0, getNumberOfPoints(mCurrentType) * 2); 368 return mCurrentType; 369 } 370 371 @Override 372 public int currentSegment(double[] coords) { 373 throw new UnsupportedOperationException(); 374 } 375 376 /** Returns the point where the current segment ends */ 377 public void getCurrentSegmentEnd(float[] point) { 378 point[0] = mLastPoint[0]; 379 point[1] = mLastPoint[1]; 380 } 381 382 /** Restarts the iterator and jumps all the segments of this path up to the length value. */ 383 public void jumpToSegment(float length) { 384 isIteratorDone = false; 385 if (length <= 0f) { 386 mNextIndex = 0; 387 return; 388 } 389 390 float accLength = 0; 391 float lastPoint[] = new float[2]; 392 for (mNextIndex = 0; mNextIndex < mTypes.length; mNextIndex++) { 393 float segmentLength = mSegmentsLength[mNextIndex]; 394 if (accLength + segmentLength >= length && mTypes[mNextIndex] != SEG_MOVETO) { 395 float[] estimatedPoint = new float[2]; 396 getPointAtLength( 397 mTypes[mNextIndex], 398 mCoordinates[mNextIndex], 399 lastPoint[0], 400 lastPoint[1], 401 (length - accLength) / segmentLength, 402 estimatedPoint); 403 404 // This segment makes us go further than length so we go back one step, 405 // set a moveto and offset the length of the next segment by the length 406 // of this segment that we've already used. 407 mCurrentType = PathIterator.SEG_MOVETO; 408 mCurrentCoords[0] = estimatedPoint[0]; 409 mCurrentCoords[1] = estimatedPoint[1]; 410 mCurrentSegmentLength = 0; 411 412 // We need to offset next path length to account for the segment we've just 413 // skipped. 414 mOffsetLength = length - accLength; 415 return; 416 } 417 accLength += segmentLength; 418 getShapeEndPoint(mTypes[mNextIndex], mCoordinates[mNextIndex], lastPoint); 419 } 420 } 421 422 /** 423 * Returns the current segment up to certain length. If the current segment is shorter than 424 * length, then the whole segment is returned. The segment coordinates are copied into the 425 * coords array. 426 * 427 * @return the segment type 428 */ 429 public int currentSegment(float[] coords, float length) { 430 int type = currentSegment(coords); 431 // If the length is greater than the current segment length, no need to find 432 // the cut point. Same if this is a SEG_MOVETO. 433 if (mCurrentSegmentLength <= length || type == SEG_MOVETO) { 434 return type; 435 } 436 437 float t = length / getCurrentSegmentLength(); 438 439 // We find at which offset the end point is located within the coords array and set 440 // a new end point to cut the segment short 441 switch (type) { 442 case SEG_CUBICTO: 443 case SEG_QUADTO: 444 float[] curve = new float[8]; 445 curve[0] = mLastPoint[0]; 446 curve[1] = mLastPoint[1]; 447 System.arraycopy(coords, 0, curve, 2, coords.length); 448 if (type == SEG_CUBICTO) { 449 cubicCurveSegment(curve, 0f, t); 450 } else { 451 quadCurveSegment(curve, 0f, t); 452 } 453 System.arraycopy(curve, 2, coords, 0, coords.length); 454 break; 455 default: 456 float[] point = new float[2]; 457 getPointAtLength(type, coords, mLastPoint[0], mLastPoint[1], t, point); 458 coords[0] = point[0]; 459 coords[1] = point[1]; 460 } 461 462 return type; 463 } 464 465 /** Returns the total length of the path */ 466 public float getTotalLength() { 467 return mTotalLength; 468 } 469 } 470 } 471