Home | History | Annotate | Download | only in shadows
      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