Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.N;
      4 
      5 import android.graphics.Path;
      6 import android.util.Log;
      7 import android.util.PathParser;
      8 import android.util.PathParser.PathData;
      9 import java.util.ArrayList;
     10 import java.util.Arrays;
     11 import org.robolectric.annotation.Implementation;
     12 import org.robolectric.annotation.Implements;
     13 
     14 @Implements(value = PathParser.class, minSdk = N, isInAndroidSdk = false)
     15 public class ShadowPathParser {
     16 
     17   static final String LOGTAG = ShadowPathParser.class.getSimpleName();
     18 
     19   @Implementation
     20   protected static Path createPathFromPathData(String pathData) {
     21     Path path = new Path();
     22     PathDataNode[] nodes = createNodesFromPathData(pathData);
     23     if (nodes != null) {
     24       PathDataNode.nodesToPath(nodes, path);
     25       return path;
     26     }
     27     return null;
     28   }
     29 
     30   public static PathDataNode[] createNodesFromPathData(String pathData) {
     31     if (pathData == null) {
     32       return null;
     33     }
     34     int start = 0;
     35     int end = 1;
     36 
     37     ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
     38     while (end < pathData.length()) {
     39       end = nextStart(pathData, end);
     40       String s = pathData.substring(start, end).trim();
     41       if (s.length() > 0) {
     42         float[] val = getFloats(s);
     43         addNode(list, s.charAt(0), val);
     44       }
     45 
     46       start = end;
     47       end++;
     48     }
     49     if ((end - start) == 1 && start < pathData.length()) {
     50       addNode(list, pathData.charAt(start), new float[0]);
     51     }
     52     return list.toArray(new PathDataNode[list.size()]);
     53   }
     54 
     55   @Implementation
     56   protected static boolean interpolatePathData(
     57       PathData outData, PathData fromData, PathData toData, float fraction) {
     58     return true;
     59   }
     60 
     61   @Implementation
     62   public static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
     63     return true;
     64   }
     65 
     66   private static int nextStart(String s, int end) {
     67     char c;
     68 
     69     while (end < s.length()) {
     70       c = s.charAt(end);
     71       if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
     72         return end;
     73       }
     74       end++;
     75     }
     76     return end;
     77   }
     78 
     79   private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
     80     list.add(new PathDataNode(cmd, val));
     81   }
     82 
     83   private static class ExtractFloatResult {
     84     // We need to return the position of the next separator and whether the
     85     // next float starts with a '-'.
     86     int mEndPosition;
     87     boolean mEndWithNegSign;
     88   }
     89 
     90   private static float[] getFloats(String s) {
     91     if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
     92       return new float[0];
     93     }
     94     try {
     95       float[] results = new float[s.length()];
     96       int count = 0;
     97       int startPosition = 1;
     98       int endPosition = 0;
     99 
    100       ExtractFloatResult result = new ExtractFloatResult();
    101       int totalLength = s.length();
    102 
    103       // The startPosition should always be the first character of the
    104       // current number, and endPosition is the character after the current
    105       // number.
    106       while (startPosition < totalLength) {
    107         extract(s, startPosition, result);
    108         endPosition = result.mEndPosition;
    109 
    110         if (startPosition < endPosition) {
    111           results[count++] = Float.parseFloat(s.substring(startPosition, endPosition));
    112         }
    113 
    114         if (result.mEndWithNegSign) {
    115           // Keep the '-' sign with next number.
    116           startPosition = endPosition;
    117         } else {
    118           startPosition = endPosition + 1;
    119         }
    120       }
    121       return Arrays.copyOf(results, count);
    122     } catch (NumberFormatException e) {
    123       Log.e(LOGTAG, "error in parsing \"" + s + "\"");
    124       throw e;
    125     }
    126   }
    127 
    128   private static void extract(String s, int start, ExtractFloatResult result) {
    129     // Now looking for ' ', ',' or '-' from the start.
    130     int currentIndex = start;
    131     boolean foundSeparator = false;
    132     result.mEndWithNegSign = false;
    133     for (; currentIndex < s.length(); currentIndex++) {
    134       char currentChar = s.charAt(currentIndex);
    135       switch (currentChar) {
    136         case ' ':
    137         case ',':
    138           foundSeparator = true;
    139           break;
    140         case '-':
    141           if (currentIndex != start) {
    142             foundSeparator = true;
    143             result.mEndWithNegSign = true;
    144           }
    145           break;
    146       }
    147       if (foundSeparator) {
    148         break;
    149       }
    150     }
    151     // When there is nothing found, then we put the end position to the end
    152     // of the string.
    153     result.mEndPosition = currentIndex;
    154   }
    155 
    156   public static class PathDataNode {
    157     private char mType;
    158     private float[] mParams;
    159 
    160     private PathDataNode(char type, float[] params) {
    161       mType = type;
    162       mParams = params;
    163     }
    164 
    165     private PathDataNode(PathDataNode n) {
    166       mType = n.mType;
    167       mParams = Arrays.copyOf(n.mParams, n.mParams.length);
    168     }
    169 
    170     /**
    171      * Convert an array of PathDataNode to Path.
    172      *
    173      * @param node The source array of PathDataNode.
    174      * @param path The target Path object.
    175      */
    176     public static void nodesToPath(PathDataNode[] node, Path path) {
    177       float[] current = new float[4];
    178       char previousCommand = 'm';
    179       for (int i = 0; i < node.length; i++) {
    180         addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
    181         previousCommand = node[i].mType;
    182       }
    183     }
    184 
    185     /**
    186      * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and <code>
    187      * nodeTo</code> according to the <code>fraction</code>.
    188      *
    189      * @param nodeFrom The start value as a PathDataNode.
    190      * @param nodeTo The end value as a PathDataNode
    191      * @param fraction The fraction to interpolate.
    192      */
    193     public void interpolatePathDataNode(
    194         PathDataNode nodeFrom, PathDataNode nodeTo, float fraction) {
    195       for (int i = 0; i < nodeFrom.mParams.length; i++) {
    196         mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + nodeTo.mParams[i] * fraction;
    197       }
    198     }
    199 
    200     private static void addCommand(
    201         Path path, float[] current, char previousCmd, char cmd, float[] val) {
    202 
    203       int incr = 2;
    204       float currentX = current[0];
    205       float currentY = current[1];
    206       float ctrlPointX = current[2];
    207       float ctrlPointY = current[3];
    208       float reflectiveCtrlPointX;
    209       float reflectiveCtrlPointY;
    210 
    211       switch (cmd) {
    212         case 'z':
    213         case 'Z':
    214           path.close();
    215           return;
    216         case 'm':
    217         case 'M':
    218         case 'l':
    219         case 'L':
    220         case 't':
    221         case 'T':
    222           incr = 2;
    223           break;
    224         case 'h':
    225         case 'H':
    226         case 'v':
    227         case 'V':
    228           incr = 1;
    229           break;
    230         case 'c':
    231         case 'C':
    232           incr = 6;
    233           break;
    234         case 's':
    235         case 'S':
    236         case 'q':
    237         case 'Q':
    238           incr = 4;
    239           break;
    240         case 'a':
    241         case 'A':
    242           incr = 7;
    243           break;
    244       }
    245       for (int k = 0; k < val.length; k += incr) {
    246         switch (cmd) {
    247           case 'm': // moveto - Start a new sub-path (relative)
    248             path.rMoveTo(val[k + 0], val[k + 1]);
    249             currentX += val[k + 0];
    250             currentY += val[k + 1];
    251             break;
    252           case 'M': // moveto - Start a new sub-path
    253             path.moveTo(val[k + 0], val[k + 1]);
    254             currentX = val[k + 0];
    255             currentY = val[k + 1];
    256             break;
    257           case 'l': // lineto - Draw a line from the current point (relative)
    258             path.rLineTo(val[k + 0], val[k + 1]);
    259             currentX += val[k + 0];
    260             currentY += val[k + 1];
    261             break;
    262           case 'L': // lineto - Draw a line from the current point
    263             path.lineTo(val[k + 0], val[k + 1]);
    264             currentX = val[k + 0];
    265             currentY = val[k + 1];
    266             break;
    267           case 'z': // closepath - Close the current subpath
    268           case 'Z': // closepath - Close the current subpath
    269             path.close();
    270             break;
    271           case 'h': // horizontal lineto - Draws a horizontal line (relative)
    272             path.rLineTo(val[k + 0], 0);
    273             currentX += val[k + 0];
    274             break;
    275           case 'H': // horizontal lineto - Draws a horizontal line
    276             path.lineTo(val[k + 0], currentY);
    277             currentX = val[k + 0];
    278             break;
    279           case 'v': // vertical lineto - Draws a vertical line from the current point (r)
    280             path.rLineTo(0, val[k + 0]);
    281             currentY += val[k + 0];
    282             break;
    283           case 'V': // vertical lineto - Draws a vertical line from the current point
    284             path.lineTo(currentX, val[k + 0]);
    285             currentY = val[k + 0];
    286             break;
    287           case 'c': // curveto - Draws a cubic Bzier curve (relative)
    288             path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
    289 
    290             ctrlPointX = currentX + val[k + 2];
    291             ctrlPointY = currentY + val[k + 3];
    292             currentX += val[k + 4];
    293             currentY += val[k + 5];
    294 
    295             break;
    296           case 'C': // curveto - Draws a cubic Bzier curve
    297             path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
    298             currentX = val[k + 4];
    299             currentY = val[k + 5];
    300             ctrlPointX = val[k + 2];
    301             ctrlPointY = val[k + 3];
    302             break;
    303           case 's': // smooth curveto - Draws a cubic Bzier curve (reflective cp)
    304             reflectiveCtrlPointX = 0;
    305             reflectiveCtrlPointY = 0;
    306             if (previousCmd == 'c'
    307                 || previousCmd == 's'
    308                 || previousCmd == 'C'
    309                 || previousCmd == 'S') {
    310               reflectiveCtrlPointX = currentX - ctrlPointX;
    311               reflectiveCtrlPointY = currentY - ctrlPointY;
    312             }
    313             path.rCubicTo(
    314                 reflectiveCtrlPointX,
    315                 reflectiveCtrlPointY,
    316                 val[k + 0],
    317                 val[k + 1],
    318                 val[k + 2],
    319                 val[k + 3]);
    320 
    321             ctrlPointX = currentX + val[k + 0];
    322             ctrlPointY = currentY + val[k + 1];
    323             currentX += val[k + 2];
    324             currentY += val[k + 3];
    325             break;
    326           case 'S': // shorthand/smooth curveto Draws a cubic Bzier curve(reflective cp)
    327             reflectiveCtrlPointX = currentX;
    328             reflectiveCtrlPointY = currentY;
    329             if (previousCmd == 'c'
    330                 || previousCmd == 's'
    331                 || previousCmd == 'C'
    332                 || previousCmd == 'S') {
    333               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    334               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    335             }
    336             path.cubicTo(
    337                 reflectiveCtrlPointX,
    338                 reflectiveCtrlPointY,
    339                 val[k + 0],
    340                 val[k + 1],
    341                 val[k + 2],
    342                 val[k + 3]);
    343             ctrlPointX = val[k + 0];
    344             ctrlPointY = val[k + 1];
    345             currentX = val[k + 2];
    346             currentY = val[k + 3];
    347             break;
    348           case 'q': // Draws a quadratic Bzier (relative)
    349             path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    350             ctrlPointX = currentX + val[k + 0];
    351             ctrlPointY = currentY + val[k + 1];
    352             currentX += val[k + 2];
    353             currentY += val[k + 3];
    354             break;
    355           case 'Q': // Draws a quadratic Bzier
    356             path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    357             ctrlPointX = val[k + 0];
    358             ctrlPointY = val[k + 1];
    359             currentX = val[k + 2];
    360             currentY = val[k + 3];
    361             break;
    362           case 't': // Draws a quadratic Bzier curve(reflective control point)(relative)
    363             reflectiveCtrlPointX = 0;
    364             reflectiveCtrlPointY = 0;
    365             if (previousCmd == 'q'
    366                 || previousCmd == 't'
    367                 || previousCmd == 'Q'
    368                 || previousCmd == 'T') {
    369               reflectiveCtrlPointX = currentX - ctrlPointX;
    370               reflectiveCtrlPointY = currentY - ctrlPointY;
    371             }
    372             path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
    373             ctrlPointX = currentX + reflectiveCtrlPointX;
    374             ctrlPointY = currentY + reflectiveCtrlPointY;
    375             currentX += val[k + 0];
    376             currentY += val[k + 1];
    377             break;
    378           case 'T': // Draws a quadratic Bzier curve (reflective control point)
    379             reflectiveCtrlPointX = currentX;
    380             reflectiveCtrlPointY = currentY;
    381             if (previousCmd == 'q'
    382                 || previousCmd == 't'
    383                 || previousCmd == 'Q'
    384                 || previousCmd == 'T') {
    385               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    386               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    387             }
    388             path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
    389             ctrlPointX = reflectiveCtrlPointX;
    390             ctrlPointY = reflectiveCtrlPointY;
    391             currentX = val[k + 0];
    392             currentY = val[k + 1];
    393             break;
    394           case 'a': // Draws an elliptical arc
    395             // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    396             drawArc(
    397                 path,
    398                 currentX,
    399                 currentY,
    400                 val[k + 5] + currentX,
    401                 val[k + 6] + currentY,
    402                 val[k + 0],
    403                 val[k + 1],
    404                 val[k + 2],
    405                 val[k + 3] != 0,
    406                 val[k + 4] != 0);
    407             currentX += val[k + 5];
    408             currentY += val[k + 6];
    409             ctrlPointX = currentX;
    410             ctrlPointY = currentY;
    411             break;
    412           case 'A': // Draws an elliptical arc
    413             drawArc(
    414                 path,
    415                 currentX,
    416                 currentY,
    417                 val[k + 5],
    418                 val[k + 6],
    419                 val[k + 0],
    420                 val[k + 1],
    421                 val[k + 2],
    422                 val[k + 3] != 0,
    423                 val[k + 4] != 0);
    424             currentX = val[k + 5];
    425             currentY = val[k + 6];
    426             ctrlPointX = currentX;
    427             ctrlPointY = currentY;
    428             break;
    429         }
    430         previousCmd = cmd;
    431       }
    432       current[0] = currentX;
    433       current[1] = currentY;
    434       current[2] = ctrlPointX;
    435       current[3] = ctrlPointY;
    436     }
    437 
    438     private static void drawArc(
    439         Path p,
    440         float x0,
    441         float y0,
    442         float x1,
    443         float y1,
    444         float a,
    445         float b,
    446         float theta,
    447         boolean isMoreThanHalf,
    448         boolean isPositiveArc) {
    449 
    450       /* Convert rotation angle from degrees to radians */
    451       double thetaD = Math.toRadians(theta);
    452       /* Pre-compute rotation matrix entries */
    453       double cosTheta = Math.cos(thetaD);
    454       double sinTheta = Math.sin(thetaD);
    455       /* Transform (x0, y0) and (x1, y1) into unit space */
    456       /* using (inverse) rotation, followed by (inverse) scale */
    457       double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
    458       double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
    459       double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
    460       double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
    461 
    462       /* Compute differences and averages */
    463       double dx = x0p - x1p;
    464       double dy = y0p - y1p;
    465       double xm = (x0p + x1p) / 2;
    466       double ym = (y0p + y1p) / 2;
    467       /* Solve for intersecting unit circles */
    468       double dsq = dx * dx + dy * dy;
    469       if (dsq == 0.0) {
    470         Log.w(LOGTAG, " Points are coincident");
    471         return; /* Points are coincident */
    472       }
    473       double disc = 1.0 / dsq - 1.0 / 4.0;
    474       if (disc < 0.0) {
    475         Log.w(LOGTAG, "Points are too far apart " + dsq);
    476         float adjust = (float) (Math.sqrt(dsq) / 1.99999);
    477         drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc);
    478         return; /* Points are too far apart */
    479       }
    480       double s = Math.sqrt(disc);
    481       double sdx = s * dx;
    482       double sdy = s * dy;
    483       double cx;
    484       double cy;
    485       if (isMoreThanHalf == isPositiveArc) {
    486         cx = xm - sdy;
    487         cy = ym + sdx;
    488       } else {
    489         cx = xm + sdy;
    490         cy = ym - sdx;
    491       }
    492 
    493       double eta0 = Math.atan2((y0p - cy), (x0p - cx));
    494 
    495       double eta1 = Math.atan2((y1p - cy), (x1p - cx));
    496 
    497       double sweep = (eta1 - eta0);
    498       if (isPositiveArc != (sweep >= 0)) {
    499         if (sweep > 0) {
    500           sweep -= 2 * Math.PI;
    501         } else {
    502           sweep += 2 * Math.PI;
    503         }
    504       }
    505 
    506       cx *= a;
    507       cy *= b;
    508       double tcx = cx;
    509       cx = cx * cosTheta - cy * sinTheta;
    510       cy = tcx * sinTheta + cy * cosTheta;
    511 
    512       arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
    513     }
    514 
    515     /**
    516      * Converts an arc to cubic Bezier segments and records them in p.
    517      *
    518      * @param p The target for the cubic Bezier segments
    519      * @param cx The x coordinate center of the ellipse
    520      * @param cy The y coordinate center of the ellipse
    521      * @param a The radius of the ellipse in the horizontal direction
    522      * @param b The radius of the ellipse in the vertical direction
    523      * @param e1x E(eta1) x coordinate of the starting point of the arc
    524      * @param e1y E(eta2) y coordinate of the starting point of the arc
    525      * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
    526      * @param start The start angle of the arc on the ellipse
    527      * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
    528      */
    529     private static void arcToBezier(
    530         Path p,
    531         double cx,
    532         double cy,
    533         double a,
    534         double b,
    535         double e1x,
    536         double e1y,
    537         double theta,
    538         double start,
    539         double sweep) {
    540       // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
    541       // and http://www.spaceroots.org/documents/ellipse/node22.html
    542 
    543       // Maximum of 45 degrees per cubic Bezier segment
    544       int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
    545 
    546       double eta1 = start;
    547       double cosTheta = Math.cos(theta);
    548       double sinTheta = Math.sin(theta);
    549       double cosEta1 = Math.cos(eta1);
    550       double sinEta1 = Math.sin(eta1);
    551       double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
    552       double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
    553 
    554       double anglePerSegment = sweep / numSegments;
    555       for (int i = 0; i < numSegments; i++) {
    556         double eta2 = eta1 + anglePerSegment;
    557         double sinEta2 = Math.sin(eta2);
    558         double cosEta2 = Math.cos(eta2);
    559         double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
    560         double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
    561         double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
    562         double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
    563         double tanDiff2 = Math.tan((eta2 - eta1) / 2);
    564         double alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
    565         double q1x = e1x + alpha * ep1x;
    566         double q1y = e1y + alpha * ep1y;
    567         double q2x = e2x - alpha * ep2x;
    568         double q2y = e2y - alpha * ep2y;
    569 
    570         p.cubicTo((float) q1x, (float) q1y, (float) q2x, (float) q2y, (float) e2x, (float) e2y);
    571         eta1 = eta2;
    572         e1x = e2x;
    573         e1y = e2y;
    574         ep1x = ep2x;
    575         ep1y = ep2y;
    576       }
    577     }
    578   }
    579 
    580   @Implements(value = PathParser.PathData.class, minSdk = N, isInAndroidSdk = false)
    581   public static class ShadowPathData {
    582     long mNativePathData = 0;
    583 
    584     @Implementation
    585     public void __constructor__() {
    586       // mNativePathData = nCreateEmptyPathData();
    587     }
    588 
    589     @Implementation
    590     public void __constructor__(PathParser.PathData data) {
    591       // mNativePathData = nCreatePathData(data.mNativePathData);
    592     }
    593 
    594     @Implementation
    595     public void __constructor__(String pathString) {
    596       // mNativePathData = nCreatePathDataFromString(pathString, pathString.length());
    597       // if (mNativePathData == 0) {
    598       //     throw new IllegalArgumentException("Invalid pathData: " + pathString);
    599       // }
    600     }
    601 
    602     @Implementation
    603     public long getNativePtr() {
    604       return mNativePathData;
    605     }
    606 
    607     /**
    608      * Update the path data to match the source. Before calling this, make sure canMorph(target,
    609      * source) is true.
    610      *
    611      * @param source The source path represented in PathData
    612      */
    613     @Implementation
    614     public void setPathData(PathParser.PathData source) {
    615       // nSetPathData(mNativePathData, source.mNativePathData);
    616     }
    617 
    618     @Override
    619     @Implementation
    620     protected void finalize() throws Throwable {
    621       if (mNativePathData != 0) {
    622         //   nFinalize(mNativePathData);
    623         mNativePathData = 0;
    624       }
    625       super.finalize();
    626     }
    627   }
    628 }
    629