From 4112d60c52c1cd92321907fe35e15efe8208fc03 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Fri, 6 Jan 2017 14:39:44 +0800 Subject: [PATCH 01/10] Move path parser to native side (Android) --- .../com/horcrux/svg/CircleShadowNode.java | 8 +- .../com/horcrux/svg/EllipseShadowNode.java | 8 +- .../java/com/horcrux/svg/GroupShadowNode.java | 2 +- .../java/com/horcrux/svg/ImageShadowNode.java | 12 +- .../java/com/horcrux/svg/LineShadowNode.java | 8 +- .../java/com/horcrux/svg/PathShadowNode.java | 427 +--------------- .../main/java/com/horcrux/svg/PropHelper.java | 434 ++++++++++++++++- .../java/com/horcrux/svg/RectShadowNode.java | 8 +- .../com/horcrux/svg/RenderableShadowNode.java | 461 ++++++++++++++++++ .../horcrux/svg/RenderableViewManager.java | 4 + .../main/java/com/horcrux/svg/SvgView.java | 21 +- .../java/com/horcrux/svg/SvgViewManager.java | 2 +- .../com/horcrux/svg/SvgViewShadowNode.java | 75 ++- .../java/com/horcrux/svg/TextShadowNode.java | 2 +- .../java/com/horcrux/svg/UseShadowNode.java | 9 +- 15 files changed, 970 insertions(+), 511 deletions(-) create mode 100644 android/src/main/java/com/horcrux/svg/RenderableShadowNode.java diff --git a/android/src/main/java/com/horcrux/svg/CircleShadowNode.java b/android/src/main/java/com/horcrux/svg/CircleShadowNode.java index a7e536a9c..c3f3b9c7d 100644 --- a/android/src/main/java/com/horcrux/svg/CircleShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/CircleShadowNode.java @@ -19,7 +19,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class CircleShadowNode extends PathShadowNode { +public class CircleShadowNode extends RenderableShadowNode { private String mCx; private String mCy; @@ -43,12 +43,6 @@ public void setR(String r) { markUpdated(); } - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - mPath = getPath(canvas, paint); - super.draw(canvas, paint, opacity); - } - @Override protected Path getPath(Canvas canvas, Paint paint) { Path path = new Path(); diff --git a/android/src/main/java/com/horcrux/svg/EllipseShadowNode.java b/android/src/main/java/com/horcrux/svg/EllipseShadowNode.java index 43aecded9..120922e41 100644 --- a/android/src/main/java/com/horcrux/svg/EllipseShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/EllipseShadowNode.java @@ -19,7 +19,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class EllipseShadowNode extends PathShadowNode { +public class EllipseShadowNode extends RenderableShadowNode { private String mCx; private String mCy; @@ -50,12 +50,6 @@ public void setRy(String ry) { markUpdated(); } - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - mPath = getPath(canvas, paint); - super.draw(canvas, paint, opacity); - } - @Override protected Path getPath(Canvas canvas, Paint paint) { Path path = new Path(); diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index 0ef4ae114..fad5b6128 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -24,7 +24,7 @@ /** * Shadow node for virtual RNSVGGroup view */ -public class GroupShadowNode extends PathShadowNode { +public class GroupShadowNode extends RenderableShadowNode { public void draw(final Canvas canvas, final Paint paint, final float opacity) { final SvgViewShadowNode svg = getSvgShadowNode(); diff --git a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java index cf8415797..23eb7fda4 100644 --- a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java @@ -41,7 +41,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class ImageShadowNode extends PathShadowNode { +public class ImageShadowNode extends RenderableShadowNode { private String mX; private String mY; @@ -107,8 +107,7 @@ public void setMeetOrSlice(int meetOrSlice) { @Override public void draw(final Canvas canvas, final Paint paint, final float opacity) { - mPath = new Path(); - mPath.addRect(new RectF(getRect()), Path.Direction.CW); + mPath = getPath(canvas, paint); if (!mLoading.get()) { final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(mUri).build(); @@ -121,6 +120,13 @@ public void draw(final Canvas canvas, final Paint paint, final float opacity) { } } + @Override + protected Path getPath(Canvas canvas, Paint paint) { + Path path = new Path(); + path.addRect(new RectF(getRect()), Path.Direction.CW); + return path; + } + private void loadBitmap(ImageRequest request, final Canvas canvas, final Paint paint) { final DataSource> dataSource = Fresco.getImagePipeline().fetchDecodedImage(request, getThemedContext()); diff --git a/android/src/main/java/com/horcrux/svg/LineShadowNode.java b/android/src/main/java/com/horcrux/svg/LineShadowNode.java index b633692e5..03470dd07 100644 --- a/android/src/main/java/com/horcrux/svg/LineShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/LineShadowNode.java @@ -17,7 +17,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class LineShadowNode extends PathShadowNode { +public class LineShadowNode extends RenderableShadowNode { private String mX1; private String mY1; @@ -48,12 +48,6 @@ public void setY2(String y2) { markUpdated(); } - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - mPath = getPath(canvas, paint); - super.draw(canvas, paint, opacity); - } - @Override protected Path getPath(Canvas canvas, Paint paint) { Path path = new Path(); diff --git a/android/src/main/java/com/horcrux/svg/PathShadowNode.java b/android/src/main/java/com/horcrux/svg/PathShadowNode.java index 66cce64fe..ad0f48e06 100644 --- a/android/src/main/java/com/horcrux/svg/PathShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/PathShadowNode.java @@ -21,6 +21,7 @@ import android.graphics.RectF; import android.graphics.Color; +import android.util.Log; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -38,435 +39,19 @@ /** * Shadow node for virtual RNSVGPath view */ -public class PathShadowNode extends VirtualNode { +public class PathShadowNode extends RenderableShadowNode { - private static final int CAP_BUTT = 0; - private static final int CAP_ROUND = 1; - private static final int CAP_SQUARE = 2; - - private static final int JOIN_BEVEL = 2; - private static final int JOIN_MITER = 0; - private static final int JOIN_ROUND = 1; - - private static final int FILL_RULE_EVENODD = 0; - private static final int FILL_RULE_NONZERO = 1; - - public @Nullable ReadableArray mStroke; - public @Nullable float[] mStrokeDasharray; - public float mStrokeWidth = 1; - public float mStrokeOpacity = 1; - public float mStrokeMiterlimit = 4; - public float mStrokeDashoffset = 0; - public Paint.Cap mStrokeLinecap = Paint.Cap.ROUND; - public Paint.Join mStrokeLinejoin = Paint.Join.ROUND; - - public @Nullable ReadableArray mFill; - public float mFillOpacity = 1; - public Path.FillType mFillRule = Path.FillType.WINDING; - private boolean mFillRuleSet; - - protected Path mPath; - private float[] mD; - - private ArrayList mChangedList; - private ArrayList mOriginProperties; - protected ReadableArray mPropList = new JavaOnlyArray(); - protected WritableArray mOwnedPropList = new JavaOnlyArray(); + private Path mPath; @ReactProp(name = "d") - public void setPath(@Nullable ReadableArray shapePath) { - mD = PropHelper.toFloatArray(shapePath); - setupPath(); - markUpdated(); - } - - @ReactProp(name = "fill") - public void setFill(@Nullable ReadableArray fill) { - mFill = fill; - markUpdated(); - } - - @ReactProp(name = "fillOpacity", defaultFloat = 1f) - public void setFillOpacity(float fillOpacity) { - mFillOpacity = fillOpacity; - markUpdated(); - } - - @ReactProp(name = "fillRule", defaultInt = FILL_RULE_NONZERO) - public void setFillRule(int fillRule) { - switch (fillRule) { - case FILL_RULE_EVENODD: - mFillRule = Path.FillType.EVEN_ODD; - break; - case FILL_RULE_NONZERO: - break; - default: - throw new JSApplicationIllegalArgumentException( - "fillRule " + mFillRule + " unrecognized"); - } - - mFillRuleSet = true; - setupPath(); - markUpdated(); - } - - @ReactProp(name = "stroke") - public void setStroke(@Nullable ReadableArray strokeColors) { - mStroke = strokeColors; - markUpdated(); - } - - @ReactProp(name = "strokeOpacity", defaultFloat = 1f) - public void setStrokeOpacity(float strokeOpacity) { - mStrokeOpacity = strokeOpacity; - markUpdated(); - } - - @ReactProp(name = "strokeDasharray") - public void setStrokeDasharray(@Nullable ReadableArray strokeDasharray) { - - mStrokeDasharray = PropHelper.toFloatArray(strokeDasharray); - if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { - for (int i = 0; i < mStrokeDasharray.length; i++) { - mStrokeDasharray[i] = mStrokeDasharray[i] * mScale; - } - } - markUpdated(); - } - - @ReactProp(name = "strokeDashoffset", defaultFloat = 0f) - public void setStrokeDashoffset(float strokeWidth) { - mStrokeDashoffset = strokeWidth * mScale; - markUpdated(); - } - - @ReactProp(name = "strokeWidth", defaultFloat = 1f) - public void setStrokeWidth(float strokeWidth) { - mStrokeWidth = strokeWidth; - markUpdated(); - } - - @ReactProp(name = "strokeMiterlimit", defaultFloat = 4f) - public void setStrokeMiterlimit(float strokeMiterlimit) { - mStrokeMiterlimit = strokeMiterlimit; - markUpdated(); - } - - @ReactProp(name = "strokeLinecap", defaultInt = CAP_ROUND) - public void setStrokeLinecap(int strokeLinecap) { - switch (strokeLinecap) { - case CAP_BUTT: - mStrokeLinecap = Paint.Cap.BUTT; - break; - case CAP_SQUARE: - mStrokeLinecap = Paint.Cap.SQUARE; - break; - case CAP_ROUND: - mStrokeLinecap = Paint.Cap.ROUND; - break; - default: - throw new JSApplicationIllegalArgumentException( - "strokeLinecap " + mStrokeLinecap + " unrecognized"); - } - markUpdated(); - } - - @ReactProp(name = "strokeLinejoin", defaultInt = JOIN_ROUND) - public void setStrokeLinejoin(int strokeLinejoin) { - switch (strokeLinejoin) { - case JOIN_MITER: - mStrokeLinejoin = Paint.Join.MITER; - break; - case JOIN_BEVEL: - mStrokeLinejoin = Paint.Join.BEVEL; - break; - case JOIN_ROUND: - mStrokeLinejoin = Paint.Join.ROUND; - break; - default: - throw new JSApplicationIllegalArgumentException( - "strokeLinejoin " + mStrokeLinejoin + " unrecognized"); - } - markUpdated(); - } - - @ReactProp(name = "propList") - public void setPropList(@Nullable ReadableArray propList) { - WritableArray copy = new JavaOnlyArray(); - - if (propList != null) { - for (int i = 0; i < propList.size(); i++) { - String fieldName = propertyNameToFieldName(propList.getString(i)); - copy.pushString(fieldName); - mOwnedPropList.pushString(fieldName); - } - - } - - mPropList = copy; + public void setD(String d) { + PropHelper.PathParser parser = new PropHelper.PathParser(d, mScale); + mPath = parser.getPath(); markUpdated(); } - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - opacity *= mOpacity; - - if (opacity > MIN_OPACITY_FOR_DRAW) { - int count = saveAndSetupCanvas(canvas); - if (mPath == null) { - throw new JSApplicationIllegalArgumentException( - "Paths should have a valid path (d) prop"); - } - - clip(canvas, paint); - if (setupFillPaint(paint, opacity * mFillOpacity, null)) { - canvas.drawPath(mPath, paint); - } - if (setupStrokePaint(paint, opacity * mStrokeOpacity, null)) { - canvas.drawPath(mPath, paint); - } - - restoreCanvas(canvas, count); - markUpdateSeen(); - } - } - - private void setupPath() { - // init path after both fillRule and path have been set - if (mFillRuleSet && mD != null) { - mPath = new Path(); - mPath.setFillType(mFillRule); - super.createPath(mD, mPath); - } - } - - /** - * sorting stops and stopsColors from array - */ - private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) { - int startStops = value.size() - stopsCount; - for (int i = 0; i < stopsCount; i++) { - stops[i] = (float)value.getDouble(startStops + i); - stopsColors[i] = Color.argb( - (int) (value.getDouble(startColorsPosition + i * 4 + 3) * 255), - (int) (value.getDouble(startColorsPosition + i * 4) * 255), - (int) (value.getDouble(startColorsPosition + i * 4 + 1) * 255), - (int) (value.getDouble(startColorsPosition + i * 4 + 2) * 255)); - - } - } - - - /** - * Sets up paint according to the props set on a shadow view. Returns {@code true} - * if the fill should be drawn, {@code false} if not. - */ - protected boolean setupFillPaint(Paint paint, float opacity, @Nullable RectF box) { - if (mFill != null && mFill.size() > 0) { - paint.reset(); - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.FILL); - setupPaint(paint, opacity, mFill, box); - return true; - } - return false; - } - - /** - * Sets up paint according to the props set on a shadow view. Returns {@code true} - * if the stroke should be drawn, {@code false} if not. - */ - protected boolean setupStrokePaint(Paint paint, float opacity, @Nullable RectF box) { - paint.reset(); - if (mStrokeWidth == 0 || mStroke == null || mStroke.size() == 0) { - return false; - } - - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeCap(mStrokeLinecap); - paint.setStrokeJoin(mStrokeLinejoin); - paint.setStrokeMiter(mStrokeMiterlimit * mScale); - paint.setStrokeWidth(mStrokeWidth * mScale); - setupPaint(paint, opacity, mStroke, box); - - if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { - paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset)); - } - - return true; - } - - - private void setupPaint(Paint paint, float opacity, ReadableArray colors, @Nullable RectF box) { - int colorType = colors.getInt(0); - if (colorType == 0) { - // solid color - paint.setARGB( - (int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255), - (int) (colors.getDouble(1) * 255), - (int) (colors.getDouble(2) * 255), - (int) (colors.getDouble(3) * 255)); - } else if (colorType == 1) { - if (box == null) { - box = new RectF(); - mPath.computeBounds(box, true); - } - PropHelper.RNSVGBrush brush = getSvgShadowNode().getDefinedBrush(colors.getString(1)); - if (brush != null) { - brush.setupPaint(paint, box, mScale, opacity); - } - } else { - // TODO: Support pattern. - FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); - } - - } - @Override protected Path getPath(Canvas canvas, Paint paint) { return mPath; } - - @Override - public int hitTest(Point point, @Nullable Matrix matrix) { - Bitmap bitmap = Bitmap.createBitmap( - mCanvasWidth, - mCanvasHeight, - Bitmap.Config.ARGB_8888); - - Canvas canvas = new Canvas(bitmap); - - if (matrix != null) { - canvas.concat(matrix); - } - - canvas.concat(mMatrix); - - Paint paint = new Paint(); - clip(canvas, paint); - setHitTestFill(paint); - canvas.drawPath(mPath, paint); - - if (setHitTestStroke(paint)) { - canvas.drawPath(mPath, paint); - } - - canvas.setBitmap(bitmap); - try { - if (bitmap.getPixel(point.x, point.y) != 0) { - return getReactTag(); - } - } catch (Exception e) { - - return -1; - } finally { - bitmap.recycle(); - } - return -1; - } - - protected void setHitTestFill(Paint paint) { - paint.reset(); - paint.setARGB(255, 0, 0, 0); - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.FILL); - } - - protected boolean setHitTestStroke(Paint paint) { - if (mStrokeWidth == 0) { - return false; - } - - paint.reset(); - paint.setARGB(255, 0, 0, 0); - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(mStrokeWidth * mScale); - paint.setStrokeCap(mStrokeLinecap); - paint.setStrokeJoin(mStrokeLinejoin); - return true; - } - - @Override - public void mergeProperties(VirtualNode target, ReadableArray mergeList, boolean inherited) { - if (mergeList.size() == 0) { - return; - } - - if (!inherited) { - mOriginProperties = new ArrayList<>(); - mChangedList = new ArrayList<>(); - } - - WritableArray propList = new JavaOnlyArray(); - for (int i = 0; i < mPropList.size(); i++) { - propList.pushString(mPropList.getString(i)); - } - mOwnedPropList = propList; - - for (int i = 0, size = mergeList.size(); i < size; i++) { - try { - String fieldName = mergeList.getString(i); - Field field = getClass().getField(fieldName); - Object value = field.get(target); - - if (inherited) { - if (!hasOwnProperty(fieldName)) { - field.set(this, value); - propList.pushString(fieldName); - } - } else { - mOriginProperties.add(field.get(this)); - mChangedList.add(fieldName); - field.set(this, value); - } - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - } - - @Override - public void mergeProperties(VirtualNode target, ReadableArray mergeList) { - mergeProperties(target, mergeList, false); - } - - @Override - public void resetProperties() { - if (mChangedList != null) { - try { - for (int i = mChangedList.size() - 1; i >= 0; i--) { - Field field = getClass().getField(mChangedList.get(i)); - field.set(this, mOriginProperties.get(i)); - } - } catch (Exception e) { - throw new IllegalStateException(e); - } - - mChangedList = null; - mOriginProperties = null; - } - } - - // convert propertyName something like fillOpacity to fieldName like mFillOpacity - private String propertyNameToFieldName(String fieldName) { - Pattern pattern = Pattern.compile("^(\\w)"); - Matcher matched = pattern.matcher(fieldName); - StringBuffer sb = new StringBuffer("m"); - while (matched.find()) { - matched.appendReplacement(sb, matched.group(1).toUpperCase()); - } - matched.appendTail(sb); - return sb.toString(); - } - - private boolean hasOwnProperty(String propName) { - for (int i = mOwnedPropList.size() - 1; i >= 0; i--) { - if (mOwnedPropList.getString(i).equals(propName)) { - return true; - } - } - return false; - } } diff --git a/android/src/main/java/com/horcrux/svg/PropHelper.java b/android/src/main/java/com/horcrux/svg/PropHelper.java index 69965d1b7..691a12ddc 100644 --- a/android/src/main/java/com/horcrux/svg/PropHelper.java +++ b/android/src/main/java/com/horcrux/svg/PropHelper.java @@ -10,15 +10,18 @@ package com.horcrux.svg; import android.graphics.Color; +import android.graphics.Path; import android.graphics.RectF; import android.graphics.Paint; import android.graphics.RadialGradient; import android.graphics.LinearGradient; import android.graphics.Shader; import android.graphics.Matrix; +import android.util.Log; import javax.annotation.Nullable; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import java.util.regex.Matcher; @@ -35,7 +38,7 @@ * @return a {@code float[]} if converted successfully, or {@code null} if {@param value} was * {@code null}. */ - /*package*/ + static @Nullable float[] toFloatArray(@Nullable ReadableArray value) { @@ -57,7 +60,7 @@ float[] toFloatArray(@Nullable ReadableArray value) { * @param into output array * @return number of items copied from input to the output array */ - /*package*/ + static int toFloatArray(ReadableArray value, float[] into) { int length = value.size() > into.length ? into.length : value.size(); for (int i = 0; i < length; i++) { @@ -76,7 +79,7 @@ static int toFloatArray(ReadableArray value, float[] into) { * @param offset offset number * @return actual float based on relative number */ - /*package*/ + static float fromPercentageToFloat(String percentage, float relative, float offset, float scale) { Matcher matched = Pattern.compile("^(\\-?\\d+(?:\\.\\d+)?)%$").matcher(percentage); if (matched.matches()) { @@ -93,7 +96,7 @@ static float fromPercentageToFloat(String percentage, float relative, float offs * @return string is percentage-like or not. */ - /*package*/ + static boolean isPercentage(String string) { Pattern pattern = Pattern.compile("^(\\-?\\d+(?:\\.\\d+)?)%$"); return pattern.matcher(string).matches(); @@ -102,7 +105,7 @@ static boolean isPercentage(String string) { /** * */ - /*package*/ static class RNSVGBrush { + static class RNSVGBrush { private GradientType mType = GradientType.LINEAR_GRADIENT; private ReadableArray mPoints; @@ -130,10 +133,10 @@ private static void parseGradientStops(ReadableArray value, int stopsCount, floa for (int i = 0; i < stopsCount; i++) { stops[i] = (float) value.getDouble(startStops + i); stopsColors[i] = Color.argb( - (int) (value.getDouble(i * 4 + 3) * 255 * opacity), - (int) (value.getDouble(i * 4) * 255), - (int) (value.getDouble(i * 4 + 1) * 255), - (int) (value.getDouble(i * 4 + 2) * 255)); + (int) (value.getDouble(i * 4 + 3) * 255 * opacity), + (int) (value.getDouble(i * 4) * 255), + (int) (value.getDouble(i * 4 + 1) * 255), + (int) (value.getDouble(i * 4 + 2) * 255)); } } @@ -158,14 +161,14 @@ public void setupPaint(Paint paint, RectF box, float scale, float opacity) { float x2 = PropHelper.fromPercentageToFloat(mPoints.getString(2), width, offsetX, scale); float y2 = PropHelper.fromPercentageToFloat(mPoints.getString(3), height, offsetY, scale); paint.setShader( - new LinearGradient( - x1, - y1, - x2, - y2, - stopsColors, - stops, - Shader.TileMode.CLAMP)); + new LinearGradient( + x1, + y1, + x2, + y2, + stopsColors, + stops, + Shader.TileMode.CLAMP)); } else { float rx = PropHelper.fromPercentageToFloat(mPoints.getString(2), width, 0f, scale); float ry = PropHelper.fromPercentageToFloat(mPoints.getString(3), height, 0f, scale); @@ -175,12 +178,12 @@ public void setupPaint(Paint paint, RectF box, float scale, float opacity) { //float fx = PropHelper.fromPercentageToFloat(mPoints.getString(0), width, offsetX) * scale; //float fy = PropHelper.fromPercentageToFloat(mPoints.getString(1), height, offsetY) * scale / (ry / rx); Shader radialGradient = new RadialGradient( - cx, - cy, - rx, - stopsColors, - stops, - Shader.TileMode.CLAMP + cx, + cy, + rx, + stopsColors, + stops, + Shader.TileMode.CLAMP ); Matrix radialMatrix = new Matrix(); @@ -190,4 +193,389 @@ public void setupPaint(Paint paint, RectF box, float scale, float opacity) { } } } + + static class PathParser { + static private Pattern PATH_REG_EXP = Pattern.compile("[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+", Pattern.CASE_INSENSITIVE); + static private Pattern DECICAL_REG_EXP = Pattern.compile("(\\.\\d+)(?=\\-?\\.)"); + + private Matcher mMatcher; + private Path mPath; + private String mString; + private float mPenX = 0f; + private float mPenY = 0f; + private float mPenDownX; + private float mPenDownY; + private float mPivotX = 0f; + private float mPivotY = 0f; + private float mScale = 1f; + private boolean mValid = true; + private boolean mPendDownSet = false; + + private String mLastCommand; + private String mLastValue; + + public PathParser(String d, float scale) { + mScale = scale; + mString = d; + mPath = new Path(); + mMatcher = PATH_REG_EXP.matcher(DECICAL_REG_EXP.matcher(mString).replaceAll("$1,")); + + while (mMatcher.find() && mValid) { + executeCommand(mMatcher.group()); + } + } + + private void executeCommand(String command) { + switch (command) { + // moveTo command + case "m": + move(getNextFloat(), getNextFloat()); + break; + case "M": + moveTo(getNextFloat(), getNextFloat()); + break; + + // lineTo command + case "l": + line(getNextFloat(), getNextFloat()); + break; + case "L": + lineTo(getNextFloat(), getNextFloat()); + break; + + // horizontalTo command + case "h": + line(getNextFloat(), 0); + break; + case "H": + lineTo(getNextFloat(), mPenY); + break; + + // verticalTo command + case "v": + line(0, getNextFloat()); + break; + case "V": + lineTo(mPenX, getNextFloat()); + break; + + // curveTo command + case "c": + curve(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + case "C": + curveTo(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + + // smoothCurveTo command + case "s": + smoothCurve(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + case "S": + smoothCurveTo(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + + // quadraticBezierCurveTo command + case "q": + quadraticBezierCurve(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + case "Q": + quadraticBezierCurveTo(getNextFloat(), getNextFloat(), getNextFloat(), getNextFloat()); + break; + + // smoothQuadraticBezierCurveTo command + case "t": + smoothQuadraticBezierCurve(getNextFloat(), getNextFloat()); + break; + case "T": + smoothQuadraticBezierCurveTo(getNextFloat(), getNextFloat()); + break; + + // arcTo command + case "a": + arc(getNextFloat(), getNextFloat(), getNextFloat(), getNextBoolean(), getNextBoolean(), getNextFloat(), getNextFloat()); + break; + case "A": + arcTo(getNextFloat(), getNextFloat(), getNextFloat(), getNextBoolean(), getNextBoolean(), getNextFloat(), getNextFloat()); + break; + + // close command + case "Z": + case "z": + close(); + break; + default: + mLastValue = command; + executeCommand(mLastCommand); + return; + } + + mLastCommand = command; + + if (command.equals("m")) { + mLastCommand = "l"; + } else if (command.equals("M")) { + mLastCommand = "L"; + } + } + + public Path getPath() { + return mPath; + } + + private boolean getNextBoolean() { + if (mMatcher.find()) { + return mMatcher.group().equals("1"); + } else { + mValid = false; + mPath = new Path(); + return false; + } + } + + private float getNextFloat() { + if (mLastValue != null) { + String lastValue = mLastValue; + mLastValue = null; + return Float.parseFloat(lastValue); + } else if (mMatcher.find()) { + return Float.parseFloat(mMatcher.group()); + } else { + mValid = false; + mPath = new Path(); + return 0; + } + } + private void move(float x, float y) { + moveTo(x + mPenX, y + mPenY); + } + + private void moveTo(float x, float y) { + mPivotX = mPenX = x; + mPivotY = mPenY = y; + mPath.moveTo(x * mScale, y * mScale); + } + + private void line(float x, float y) { + lineTo(x + mPenX, y + mPenY); + } + + private void lineTo(float x, float y) { + setPendDown(); + mPivotX = mPenX = x; + mPivotY = mPenY = y; + mPath.lineTo(x * mScale, y * mScale); + } + + private void curve(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { + curveTo(c1x + mPenX, c1y + mPenY, c2x + mPenX, c2y + mPenY, ex + mPenX, ey + mPenY); + } + + private void curveTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { + mPivotX = c2x; + mPivotY = c2y; + cubicTo(c1x, c1y, c2x, c2y, ex, ey); + } + + private void cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { + setPendDown(); + mPenX = ex; + mPenY = ey; + mPath.cubicTo(c1x * mScale, c1y * mScale, c2x * mScale, c2y * mScale, ex * mScale, ey * mScale); + } + + private void smoothCurve(float c1x, float c1y, float ex, float ey) { + smoothCurveTo(c1x + mPenX, c1y + mPenY, ex + mPenX, ey + mPenY); + } + + private void smoothCurveTo(float c1x, float c1y, float ex, float ey) { + float c2x = c1x; + float c2y = c1y; + c1x = (mPenX * 2) - mPivotX; + c1y = (mPenY * 2) - mPivotY; + mPivotX = c2x; + mPivotY = c2y; + cubicTo(c1x, c1y, c2x, c2y, ex, ey); + } + + private void quadraticBezierCurve(float c1x, float c1y, float c2x, float c2y) { + quadraticBezierCurveTo(c1x + mPenX, c1y + mPenY, c2x + mPenX, c2y + mPenY); + } + + private void quadraticBezierCurveTo(float c1x, float c1y, float c2x, float c2y) { + mPivotX = c1x; + mPivotY = c1y; + float ex = c2x; + float ey = c2y; + c2x = (ex + c1x * 2) / 3; + c2y = (ey + c1y * 2) / 3; + c1x = (mPenX + c1x * 2) / 3; + c1y = (mPenY + c1y * 2) / 3; + cubicTo(c1x, c1y, c2x, c2y, ex, ey); + } + + private void smoothQuadraticBezierCurve(float c1x, float c1y) { + smoothQuadraticBezierCurveTo(c1x + mPenX, c1y + mPenY); + } + + private void smoothQuadraticBezierCurveTo(float c1x, float c1y) { + float c2x = c1x; + float c2y = c1y; + c1x = (mPenX * 2) - mPivotX; + c1y = (mPenY * 2) - mPivotY; + quadraticBezierCurveTo(c1x, c1y, c2x, c2y); + } + + private void arc(float rx, float ry, float rotation, boolean outer, boolean clockwise, float x, float y) { + arcTo(rx, ry, rotation, outer, clockwise, x + mPenX, y + mPenY); + } + + private void arcTo(float rx, float ry, float rotation, boolean outer, boolean clockwise, float x, float y) { + float tX = mPenX; + float tY = mPenY; + + ry = Math.abs(ry == 0 ? (rx == 0 ? (y - tY) : rx) : ry); + rx = Math.abs(rx == 0 ? (x - tX) : rx); + + if (rx == 0 || ry == 0 || (x == tX && y == tY)) { + lineTo(x, y); + return; + } + + float rad = (float) Math.toRadians(rotation); + float cos = (float) Math.cos(rad); + float sin = (float) Math.sin(rad); + x -= tX; + y -= tY; + + // Ellipse Center + float cx = cos * x / 2 + sin * y / 2; + float cy = -sin * x / 2 + cos * y / 2; + float rxry = rx * rx * ry * ry; + float rycx = ry * ry * cx * cx; + float rxcy = rx * rx * cy * cy; + float a = rxry - rxcy - rycx; + + if (a < 0){ + a = (float)Math.sqrt(1 - a / rxry); + rx *= a; + ry *= a; + cx = x / 2; + cy = y / 2; + } else { + a = (float)Math.sqrt(a / (rxcy + rycx)); + + if (outer == clockwise) { + a = -a; + } + float cxd = -a * cy * rx / ry; + float cyd = a * cx * ry / rx; + cx = cos * cxd - sin * cyd + x / 2; + cy = sin * cxd + cos * cyd + y / 2; + } + + // Rotation + Scale Transform + float xx = cos / rx; + float yx = sin / rx; + float xy = -sin / ry; + float yy = cos / ry; + + // Start and End Angle + float sa = (float) Math.atan2(xy * -cx + yy * -cy, xx * -cx + yx * -cy); + float ea = (float) Math.atan2(xy * (x - cx) + yy * (y - cy), xx * (x - cx) + yx * (y - cy)); + + cx += tX; + cy += tY; + x += tX; + y += tY; + + setPendDown(); + + mPenX = mPivotX = x; + mPenY = mPivotY = y; + + //arcToBezier(cx, cy, rx, ry, sa, ea, clockwise, rad); + if (rx != ry || rad != 0f) { + arcToBezier(cx, cy, rx, ry, sa, ea, clockwise, rad); + } else { + float start = (float) Math.toDegrees(sa); + float end = (float) Math.toDegrees(ea); + + if (!clockwise) { + end = 360 - end; + } + float sweep = start - end; + RectF oval = new RectF( + (cx - rx) * mScale, + (cy - rx) * mScale, + (cx + rx) * mScale, + (cy + rx) * mScale); + mPath.arcTo(oval, start, sweep); + } + } + + private void close() { + if (mPendDownSet) { + mPenX = mPenDownX; + mPenY = mPenDownY; + mPendDownSet = false; + mPath.close(); + } + } + + private void arcToBezier(float cx, float cy, float rx, float ry, float sa, float ea, boolean clockwise, float rad) { + // Inverse Rotation + Scale Transform + float cos = (float) Math.cos(rad); + float sin = (float) Math.sin(rad); + float xx = cos * rx; + float yx = -sin * ry; + float xy = sin * rx; + float yy = cos * ry; + + // Bezier Curve Approximation + float arc = ea - sa; + if (arc < 0 && clockwise) { + arc += Math.PI * 2; + } else if (arc > 0 && !clockwise) { + arc -= Math.PI * 2; + } + + int n = (int) Math.ceil(Math.abs(arc / (Math.PI / 2))); + + float step = arc / n; + float k = (4 / 3) * (float) Math.tan(step / 4); + + float x = (float) Math.cos(sa); + float y = (float) Math.sin(sa); + + for (int i = 0; i < n; i++){ + float cp1x = x - k * y; + float cp1y = y + k * x; + + sa += step; + x = (float) Math.cos(sa); + y = (float) Math.sin(sa); + + float cp2x = x + k * y; + float cp2y = y - k * x; + + mPath.cubicTo( + (cx + xx * cp1x + yx * cp1y) * mScale, + (cy + xy * cp1x + yy * cp1y) * mScale, + (cx + xx * cp2x + yx * cp2y) * mScale, + (cy + xy * cp2x + yy * cp2y) * mScale, + (cx + xx * x + yx * y) * mScale, + (cy + xy * x + yy * y) * mScale + ); + } + } + + private void setPendDown() { + if (!mPendDownSet) { + mPenDownX = mPenX; + mPenDownY = mPenY; + mPendDownSet = true; + } + } + } } diff --git a/android/src/main/java/com/horcrux/svg/RectShadowNode.java b/android/src/main/java/com/horcrux/svg/RectShadowNode.java index 16da0543c..961e0d6d9 100644 --- a/android/src/main/java/com/horcrux/svg/RectShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RectShadowNode.java @@ -18,7 +18,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class RectShadowNode extends PathShadowNode { +public class RectShadowNode extends RenderableShadowNode { private String mX; private String mY; @@ -66,12 +66,6 @@ public void setRy(String ry) { markUpdated(); } - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - mPath = getPath(canvas, paint); - super.draw(canvas, paint, opacity); - } - @Override protected Path getPath(Canvas canvas, Paint paint) { Path path = new Path(); diff --git a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java new file mode 100644 index 000000000..2283e1cdc --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java @@ -0,0 +1,461 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +package com.horcrux.svg; + +import javax.annotation.Nullable; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.RectF; + +import android.graphics.Color; +import android.util.Log; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.uimanager.annotations.ReactProp; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Shadow node for virtual RNSVGPath view + */ +abstract public class RenderableShadowNode extends VirtualNode { + + // strokeLinecap + private static final int CAP_BUTT = 0; + private static final int CAP_ROUND = 1; + private static final int CAP_SQUARE = 2; + + // strokeLinejoin + private static final int JOIN_BEVEL = 2; + private static final int JOIN_MITER = 0; + private static final int JOIN_ROUND = 1; + + // fillRule + private static final int FILL_RULE_EVENODD = 0; + private static final int FILL_RULE_NONZERO = 1; + + public @Nullable ReadableArray mStroke; + public @Nullable float[] mStrokeDasharray; + + public float mStrokeWidth = 1; + public float mStrokeOpacity = 1; + public float mStrokeMiterlimit = 4; + public float mStrokeDashoffset = 0; + + public Paint.Cap mStrokeLinecap = Paint.Cap.ROUND; + public Paint.Join mStrokeLinejoin = Paint.Join.ROUND; + + public @Nullable ReadableArray mFill; + public float mFillOpacity = 1; + public Path.FillType mFillRule = Path.FillType.WINDING; + + protected Path mPath; + + private ArrayList mChangedList; + private ArrayList mOriginProperties; + protected ReadableArray mPropList = new JavaOnlyArray(); + protected WritableArray mOwnedPropList = new JavaOnlyArray(); + + @ReactProp(name = "fill") + public void setFill(@Nullable ReadableArray fill) { + mFill = fill; + markUpdated(); + } + + @ReactProp(name = "fillOpacity", defaultFloat = 1f) + public void setFillOpacity(float fillOpacity) { + mFillOpacity = fillOpacity; + markUpdated(); + } + + @ReactProp(name = "fillRule", defaultInt = FILL_RULE_NONZERO) + public void setFillRule(int fillRule) { + switch (fillRule) { + case FILL_RULE_EVENODD: + mFillRule = Path.FillType.EVEN_ODD; + break; + case FILL_RULE_NONZERO: + break; + default: + throw new JSApplicationIllegalArgumentException( + "fillRule " + mFillRule + " unrecognized"); + } + + mPath = null; + markUpdated(); + } + + @ReactProp(name = "stroke") + public void setStroke(@Nullable ReadableArray strokeColors) { + mStroke = strokeColors; + markUpdated(); + } + + @ReactProp(name = "strokeOpacity", defaultFloat = 1f) + public void setStrokeOpacity(float strokeOpacity) { + mStrokeOpacity = strokeOpacity; + markUpdated(); + } + + @ReactProp(name = "strokeDasharray") + public void setStrokeDasharray(@Nullable ReadableArray strokeDasharray) { + + mStrokeDasharray = PropHelper.toFloatArray(strokeDasharray); + if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { + for (int i = 0; i < mStrokeDasharray.length; i++) { + mStrokeDasharray[i] = mStrokeDasharray[i] * mScale; + } + } + markUpdated(); + } + + @ReactProp(name = "strokeDashoffset", defaultFloat = 0f) + public void setStrokeDashoffset(float strokeWidth) { + mStrokeDashoffset = strokeWidth * mScale; + markUpdated(); + } + + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + markUpdated(); + } + + @ReactProp(name = "strokeMiterlimit", defaultFloat = 4f) + public void setStrokeMiterlimit(float strokeMiterlimit) { + mStrokeMiterlimit = strokeMiterlimit; + markUpdated(); + } + + @ReactProp(name = "strokeLinecap", defaultInt = CAP_ROUND) + public void setStrokeLinecap(int strokeLinecap) { + switch (strokeLinecap) { + case CAP_BUTT: + mStrokeLinecap = Paint.Cap.BUTT; + break; + case CAP_SQUARE: + mStrokeLinecap = Paint.Cap.SQUARE; + break; + case CAP_ROUND: + mStrokeLinecap = Paint.Cap.ROUND; + break; + default: + throw new JSApplicationIllegalArgumentException( + "strokeLinecap " + mStrokeLinecap + " unrecognized"); + } + markUpdated(); + } + + @ReactProp(name = "strokeLinejoin", defaultInt = JOIN_ROUND) + public void setStrokeLinejoin(int strokeLinejoin) { + switch (strokeLinejoin) { + case JOIN_MITER: + mStrokeLinejoin = Paint.Join.MITER; + break; + case JOIN_BEVEL: + mStrokeLinejoin = Paint.Join.BEVEL; + break; + case JOIN_ROUND: + mStrokeLinejoin = Paint.Join.ROUND; + break; + default: + throw new JSApplicationIllegalArgumentException( + "strokeLinejoin " + mStrokeLinejoin + " unrecognized"); + } + markUpdated(); + } + + @ReactProp(name = "propList") + public void setPropList(@Nullable ReadableArray propList) { + WritableArray copy = new JavaOnlyArray(); + + if (propList != null) { + for (int i = 0; i < propList.size(); i++) { + String fieldName = propertyNameToFieldName(propList.getString(i)); + copy.pushString(fieldName); + mOwnedPropList.pushString(fieldName); + } + + } + + mPropList = copy; + markUpdated(); + } + + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + if (mPath == null) { + mPath = getPath(canvas, paint); + mPath.setFillType(mFillRule); + } + + opacity *= mOpacity; + + if (opacity > MIN_OPACITY_FOR_DRAW) { + int count = saveAndSetupCanvas(canvas); + if (mPath == null) { + throw new JSApplicationIllegalArgumentException( + "Paths should have a valid path (d) prop"); + } + + clip(canvas, paint); + if (setupFillPaint(paint, opacity * mFillOpacity, null)) { + canvas.drawPath(mPath, paint); + } + if (setupStrokePaint(paint, opacity * mStrokeOpacity, null)) { + canvas.drawPath(mPath, paint); + } + + restoreCanvas(canvas, count); + markUpdateSeen(); + } + } + /** + * sorting stops and stopsColors from array + */ + private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) { + int startStops = value.size() - stopsCount; + for (int i = 0; i < stopsCount; i++) { + stops[i] = (float)value.getDouble(startStops + i); + stopsColors[i] = Color.argb( + (int) (value.getDouble(startColorsPosition + i * 4 + 3) * 255), + (int) (value.getDouble(startColorsPosition + i * 4) * 255), + (int) (value.getDouble(startColorsPosition + i * 4 + 1) * 255), + (int) (value.getDouble(startColorsPosition + i * 4 + 2) * 255)); + + } + } + + + /** + * Sets up paint according to the props set on a shadow view. Returns {@code true} + * if the fill should be drawn, {@code false} if not. + */ + protected boolean setupFillPaint(Paint paint, float opacity, @Nullable RectF box) { + if (mFill != null && mFill.size() > 0) { + paint.reset(); + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + setupPaint(paint, opacity, mFill, box); + return true; + } + return false; + } + + /** + * Sets up paint according to the props set on a shadow view. Returns {@code true} + * if the stroke should be drawn, {@code false} if not. + */ + protected boolean setupStrokePaint(Paint paint, float opacity, @Nullable RectF box) { + paint.reset(); + if (mStrokeWidth == 0 || mStroke == null || mStroke.size() == 0) { + return false; + } + + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(mStrokeLinecap); + paint.setStrokeJoin(mStrokeLinejoin); + paint.setStrokeMiter(mStrokeMiterlimit * mScale); + paint.setStrokeWidth(mStrokeWidth * mScale); + setupPaint(paint, opacity, mStroke, box); + + if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { + paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset)); + } + + return true; + } + + + private void setupPaint(Paint paint, float opacity, ReadableArray colors, @Nullable RectF box) { + int colorType = colors.getInt(0); + if (colorType == 0) { + // solid color + paint.setARGB( + (int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255), + (int) (colors.getDouble(1) * 255), + (int) (colors.getDouble(2) * 255), + (int) (colors.getDouble(3) * 255)); + } else if (colorType == 1) { + if (box == null) { + box = new RectF(); + mPath.computeBounds(box, true); + } + PropHelper.RNSVGBrush brush = getSvgShadowNode().getDefinedBrush(colors.getString(1)); + if (brush != null) { + brush.setupPaint(paint, box, mScale, opacity); + } + } else { + // TODO: Support pattern. + FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); + } + + } + + + abstract protected Path getPath(Canvas canvas, Paint paint); + + @Override + public int hitTest(Point point, @Nullable Matrix matrix) { + Bitmap bitmap = Bitmap.createBitmap( + mCanvasWidth, + mCanvasHeight, + Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + + if (matrix != null) { + canvas.concat(matrix); + } + + canvas.concat(mMatrix); + + Paint paint = new Paint(); + clip(canvas, paint); + setHitTestFill(paint); + canvas.drawPath(mPath, paint); + + if (setHitTestStroke(paint)) { + canvas.drawPath(mPath, paint); + } + + canvas.setBitmap(bitmap); + try { + if (bitmap.getPixel(point.x, point.y) != 0) { + return getReactTag(); + } + } catch (Exception e) { + + return -1; + } finally { + bitmap.recycle(); + } + return -1; + } + + protected void setHitTestFill(Paint paint) { + paint.reset(); + paint.setARGB(255, 0, 0, 0); + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + } + + protected boolean setHitTestStroke(Paint paint) { + if (mStrokeWidth == 0) { + return false; + } + + paint.reset(); + paint.setARGB(255, 0, 0, 0); + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(mStrokeWidth * mScale); + paint.setStrokeCap(mStrokeLinecap); + paint.setStrokeJoin(mStrokeLinejoin); + return true; + } + + @Override + public void mergeProperties(VirtualNode target, ReadableArray mergeList, boolean inherited) { + if (mergeList.size() == 0) { + return; + } + + if (!inherited) { + mOriginProperties = new ArrayList<>(); + mChangedList = new ArrayList<>(); + } + + WritableArray propList = new JavaOnlyArray(); + for (int i = 0; i < mPropList.size(); i++) { + propList.pushString(mPropList.getString(i)); + } + mOwnedPropList = propList; + + for (int i = 0, size = mergeList.size(); i < size; i++) { + try { + String fieldName = mergeList.getString(i); + Field field = getClass().getField(fieldName); + Object value = field.get(target); + + if (inherited) { + if (!hasOwnProperty(fieldName)) { + field.set(this, value); + propList.pushString(fieldName); + } + } else { + mOriginProperties.add(field.get(this)); + mChangedList.add(fieldName); + field.set(this, value); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + } + + @Override + public void mergeProperties(VirtualNode target, ReadableArray mergeList) { + mergeProperties(target, mergeList, false); + } + + @Override + public void resetProperties() { + if (mChangedList != null) { + try { + for (int i = mChangedList.size() - 1; i >= 0; i--) { + Field field = getClass().getField(mChangedList.get(i)); + field.set(this, mOriginProperties.get(i)); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + + mChangedList = null; + mOriginProperties = null; + } + } + + // convert propertyName something like fillOpacity to fieldName like mFillOpacity + private String propertyNameToFieldName(String fieldName) { + Pattern pattern = Pattern.compile("^(\\w)"); + Matcher matched = pattern.matcher(fieldName); + StringBuffer sb = new StringBuffer("m"); + while (matched.find()) { + matched.appendReplacement(sb, matched.group(1).toUpperCase()); + } + matched.appendTail(sb); + return sb.toString(); + } + + private boolean hasOwnProperty(String propName) { + for (int i = mOwnedPropList.size() - 1; i >= 0; i--) { + if (mOwnedPropList.getString(i).equals(propName)) { + return true; + } + } + return false; + } +} diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index dd6994cef..1074e2d72 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -186,4 +186,8 @@ protected View createViewInstance(ThemedReactContext reactContext) { public void updateExtraData(View root, Object extraData) { throw new IllegalStateException("SVG elements does not map into a native view"); } + + @Override + public void onDropViewInstance(View view) { + } } diff --git a/android/src/main/java/com/horcrux/svg/SvgView.java b/android/src/main/java/com/horcrux/svg/SvgView.java index 4423d6828..bbc674347 100644 --- a/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/android/src/main/java/com/horcrux/svg/SvgView.java @@ -14,6 +14,7 @@ import android.graphics.Point; import android.util.Log; import android.view.MotionEvent; +import android.view.TextureView; import android.view.View; import com.facebook.react.ReactRootView; @@ -32,7 +33,7 @@ /** * Custom {@link View} implementation that draws an RNSVGSvg React view and its \children. */ -public class SvgView extends View { +public class SvgView extends TextureView { public enum Events { EVENT_DATA_URL("onDataURL"); @@ -48,7 +49,6 @@ public String toString() { } } - private @Nullable Bitmap mBitmap; private RCTEventEmitter mEventEmitter; private EventDispatcher mEventDispatcher; private int mTargetTag; @@ -58,6 +58,7 @@ public String toString() { public SvgView(ReactContext reactContext) { super(reactContext); + setOpaque(false); mEventEmitter = reactContext.getJSModule(RCTEventEmitter.class); mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); } @@ -66,22 +67,6 @@ private SvgViewShadowNode getShadowNode() { return SvgViewShadowNode.getShadowNodeByTag(getId()); } - public void setBitmap(Bitmap bitmap) { - if (mBitmap != null) { - mBitmap.recycle(); - } - mBitmap = bitmap; - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mBitmap != null) { - canvas.drawBitmap(mBitmap, 0, 0, null); - } - } - @Override public boolean dispatchTouchEvent(MotionEvent ev) { SvgViewShadowNode svg = getShadowNode(); diff --git a/android/src/main/java/com/horcrux/svg/SvgViewManager.java b/android/src/main/java/com/horcrux/svg/SvgViewManager.java index 14241d3cf..e8f15ad99 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewManager.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewManager.java @@ -68,7 +68,7 @@ protected SvgView createViewInstance(ThemedReactContext reactContext) { @Override public void updateExtraData(SvgView root, Object extraData) { - root.setBitmap((Bitmap) extraData); + root.setSurfaceTextureListener((SvgViewShadowNode) extraData); } @Override diff --git a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java index f44a8bc87..2e347c31f 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java @@ -13,11 +13,20 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.support.annotation.Nullable; import android.util.Base64; +import android.util.Log; import android.util.SparseArray; import android.graphics.Color; import android.graphics.PorterDuff; +import android.view.Surface; +import android.view.TextureView; + +import com.facebook.common.logging.FLog; +import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.UIViewOperationQueue; import java.io.ByteArrayOutputStream; @@ -27,15 +36,18 @@ /** * Shadow node for RNSVG virtual tree root - RNSVGSvgView */ -public class SvgViewShadowNode extends LayoutShadowNode { +public class SvgViewShadowNode extends LayoutShadowNode implements TextureView.SurfaceTextureListener { private static final SparseArray mTagToShadowNode = new SparseArray<>(); + private @Nullable Surface mSurface; public static SvgViewShadowNode getShadowNodeByTag(int tag) { return mTagToShadowNode.get(tag); } + private boolean mHasPendingUpdates; private boolean mResponsible = false; + private static final Map mDefinedClipPaths = new HashMap<>(); private static final Map mDefinedTemplates = new HashMap<>(); private static final Map mDefinedBrushes = new HashMap<>(); @@ -53,8 +65,8 @@ public boolean isVirtualAnchor() { @Override public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { super.onCollectExtraUpdates(uiUpdater); - uiUpdater.enqueueUpdateExtraData(getReactTag(), drawOutput()); - + drawOutput(); + uiUpdater.enqueueUpdateExtraData(getReactTag(), this); } @Override @@ -63,17 +75,52 @@ public void setReactTag(int reactTag) { mTagToShadowNode.put(getReactTag(), this); } - public Object drawOutput() { - Bitmap bitmap = Bitmap.createBitmap( - (int) getLayoutWidth(), - (int) getLayoutHeight(), - Bitmap.Config.ARGB_8888); + public void drawOutput() { + if (mSurface == null || !mSurface.isValid()) { + markChildrenUpdatesSeen(this); + return; + } + + try { + Canvas canvas = mSurface.lockCanvas(null); + drawChildren(canvas); + + if (mSurface != null) { + mSurface.unlockCanvasAndPost(canvas); + } + + } catch (IllegalArgumentException | IllegalStateException e) { + FLog.e(ReactConstants.TAG, e.getClass().getSimpleName() + " in Svg.unlockCanvasAndPost"); + } + } + + private void markChildrenUpdatesSeen(ReactShadowNode shadowNode) { + for (int i = 0; i < shadowNode.getChildCount(); i++) { + ReactShadowNode child = shadowNode.getChildAt(i); + child.markUpdateSeen(); + markChildrenUpdatesSeen(child); + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mSurface = new Surface(surface); + drawOutput(); + } - Canvas canvas = new Canvas(bitmap); - drawChildren(canvas); - return bitmap; + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + surface.release(); + mSurface = null; + return true; } + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) {} + private void drawChildren(Canvas canvas) { canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); Paint paint = new Paint(); @@ -97,9 +144,9 @@ private void drawChildren(Canvas canvas) { public String getBase64() { Bitmap bitmap = Bitmap.createBitmap( - (int) getLayoutWidth(), - (int) getLayoutHeight(), - Bitmap.Config.ARGB_8888); + (int) getLayoutWidth(), + (int) getLayoutHeight(), + Bitmap.Config.ARGB_8888); drawChildren(new Canvas(bitmap)); ByteArrayOutputStream stream = new ByteArrayOutputStream(); diff --git a/android/src/main/java/com/horcrux/svg/TextShadowNode.java b/android/src/main/java/com/horcrux/svg/TextShadowNode.java index c1bdb1937..1ae85c8b2 100644 --- a/android/src/main/java/com/horcrux/svg/TextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TextShadowNode.java @@ -29,7 +29,7 @@ /** * Shadow node for virtual RNSVGText view */ -public class TextShadowNode extends PathShadowNode { +public class TextShadowNode extends RenderableShadowNode { private static final String PROP_LINES = "lines"; diff --git a/android/src/main/java/com/horcrux/svg/UseShadowNode.java b/android/src/main/java/com/horcrux/svg/UseShadowNode.java index a0ab7a7c3..b23137f39 100644 --- a/android/src/main/java/com/horcrux/svg/UseShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/UseShadowNode.java @@ -11,6 +11,7 @@ import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Path; import com.facebook.common.logging.FLog; import com.facebook.react.common.ReactConstants; @@ -19,7 +20,7 @@ /** * Shadow node for virtual RNSVGPath view */ -public class UseShadowNode extends PathShadowNode { +public class UseShadowNode extends RenderableShadowNode { private String mHref; private String mWidth; @@ -70,4 +71,10 @@ public void draw(Canvas canvas, Paint paint, float opacity) { "template named: " + mHref + " is not defined."); } } + + @Override + protected Path getPath(Canvas canvas, Paint paint) { + // todo: + return new Path(); + } } From 0a6dc56b68eb9a19be4a671f2f5a47076003de0a Mon Sep 17 00:00:00 2001 From: Horcrux Date: Fri, 6 Jan 2017 14:57:06 +0800 Subject: [PATCH 02/10] Move serialize path to native side --- elements/Path.js | 4 +--- lib/attributes.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/elements/Path.js b/elements/Path.js index 85f72361a..d8ff16784 100644 --- a/elements/Path.js +++ b/elements/Path.js @@ -1,5 +1,4 @@ import React, {PropTypes} from 'react'; -import SerializablePath from '../lib/SerializablePath'; import createReactNativeComponentClass from 'react-native/Libraries/Renderer/src/renderers/native/createReactNativeComponentClass'; import {PathAttributes} from '../lib/attributes'; import Shape from './Shape'; @@ -20,12 +19,11 @@ class Path extends Shape { render() { let props = this.props; - let d = new SerializablePath(props.d).toJSON(); return ( {this.root = ele;}} {...this.extractProps(props)} - d={d} + d={props.d} /> ); } diff --git a/lib/attributes.js b/lib/attributes.js index a74fd36f2..90f5b0394 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -99,9 +99,7 @@ const UseAttributes = merge({ }, RenderableAttributes); const PathAttributes = merge({ - d: { - diff: arrayDiffer - } + d: true }, RenderableAttributes); const TextAttributes = merge({ From 293263d6ef2fa93c5e6a860a2ecef72c02845bac Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sat, 7 Jan 2017 09:22:52 +0800 Subject: [PATCH 03/10] Move `d` parsing to native side(iOS) --- .../main/java/com/horcrux/svg/PropHelper.java | 12 +- ios/RNSVG.xcodeproj/project.pbxproj | 10 +- ios/Utils/RCTConvert+RNSVG.h | 3 +- ios/Utils/RCTConvert+RNSVG.m | 47 +-- ios/Utils/RNSVGPathParser.h | 17 + ios/Utils/RNSVGPathParser.m | 379 ++++++++++++++++++ ios/Utils/RNSVGPercentageConverter.m | 12 +- lib/extract/extractClipping.js | 3 +- 8 files changed, 423 insertions(+), 60 deletions(-) create mode 100644 ios/Utils/RNSVGPathParser.h create mode 100644 ios/Utils/RNSVGPathParser.m diff --git a/android/src/main/java/com/horcrux/svg/PropHelper.java b/android/src/main/java/com/horcrux/svg/PropHelper.java index 691a12ddc..2f02a675d 100644 --- a/android/src/main/java/com/horcrux/svg/PropHelper.java +++ b/android/src/main/java/com/horcrux/svg/PropHelper.java @@ -196,7 +196,7 @@ public void setupPaint(Paint paint, RectF box, float scale, float opacity) { static class PathParser { static private Pattern PATH_REG_EXP = Pattern.compile("[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+", Pattern.CASE_INSENSITIVE); - static private Pattern DECICAL_REG_EXP = Pattern.compile("(\\.\\d+)(?=\\-?\\.)"); + static private Pattern DECIMAL_REG_EXP = Pattern.compile("(\\.\\d+)(?=\\-?\\.)"); private Matcher mMatcher; private Path mPath; @@ -218,7 +218,7 @@ public PathParser(String d, float scale) { mScale = scale; mString = d; mPath = new Path(); - mMatcher = PATH_REG_EXP.matcher(DECICAL_REG_EXP.matcher(mString).replaceAll("$1,")); + mMatcher = PATH_REG_EXP.matcher(DECIMAL_REG_EXP.matcher(mString).replaceAll("$1,")); while (mMatcher.find() && mValid) { executeCommand(mMatcher.group()); @@ -361,7 +361,7 @@ private void line(float x, float y) { } private void lineTo(float x, float y) { - setPendDown(); + setPenDown(); mPivotX = mPenX = x; mPivotY = mPenY = y; mPath.lineTo(x * mScale, y * mScale); @@ -378,7 +378,7 @@ private void curveTo(float c1x, float c1y, float c2x, float c2y, float ex, float } private void cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { - setPendDown(); + setPenDown(); mPenX = ex; mPenY = ey; mPath.cubicTo(c1x * mScale, c1y * mScale, c2x * mScale, c2y * mScale, ex * mScale, ey * mScale); @@ -489,7 +489,7 @@ private void arcTo(float rx, float ry, float rotation, boolean outer, boolean cl x += tX; y += tY; - setPendDown(); + setPenDown(); mPenX = mPivotX = x; mPenY = mPivotY = y; @@ -570,7 +570,7 @@ private void arcToBezier(float cx, float cy, float rx, float ry, float sa, float } } - private void setPendDown() { + private void setPenDown() { if (!mPendDownSet) { mPenDownX = mPenX; mPenDownY = mPenY; diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index 380036801..3033b8ab3 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 10ED4A9E1CF0656A0078BC02 /* RNSVGClipPathManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10ED4A9D1CF0656A0078BC02 /* RNSVGClipPathManager.m */; }; 10ED4AA21CF078830078BC02 /* RNSVGNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 10ED4AA11CF078830078BC02 /* RNSVGNode.m */; }; 10FDEEB21D3FB60500A5C46C /* RNSVGBaseBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = 10FDEEB11D3FB60500A5C46C /* RNSVGBaseBrush.m */; }; + 7F9CDAFA1E1F809C00E0C805 /* RNSVGPathParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -155,6 +156,8 @@ 10FDEEB01D3FB60500A5C46C /* RNSVGBaseBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGBaseBrush.h; sourceTree = ""; }; 10FDEEB11D3FB60500A5C46C /* RNSVGBaseBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGBaseBrush.m; sourceTree = ""; }; 10FDEEB31D3FBED400A5C46C /* RNSVGBrushType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGBrushType.h; sourceTree = ""; }; + 7F9CDAF81E1F809C00E0C805 /* RNSVGPathParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGPathParser.h; path = Utils/RNSVGPathParser.h; sourceTree = ""; }; + 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGPathParser.m; path = Utils/RNSVGPathParser.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -310,13 +313,15 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 1039D29E1CE72177001E90A8 /* RNSVGCGFloatArray.h */, 10ABC7381D43982B006CCF6E /* RNSVGVBMOS.h */, 10ABC7371D439779006CCF6E /* RNSVGCGFCRule.h */, 1039D2AE1CE72F27001E90A8 /* RNSVGPercentageConverter.h */, 1039D2AF1CE72F27001E90A8 /* RNSVGPercentageConverter.m */, + 7F9CDAF81E1F809C00E0C805 /* RNSVGPathParser.h */, + 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */, 1039D29B1CE72177001E90A8 /* RCTConvert+RNSVG.h */, 1039D29C1CE72177001E90A8 /* RCTConvert+RNSVG.m */, - 1039D29E1CE72177001E90A8 /* RNSVGCGFloatArray.h */, ); name = Utils; sourceTree = ""; @@ -412,6 +417,7 @@ 10BA0D351CE74E3100887C2B /* RNSVGEllipseManager.m in Sources */, 1039D2A01CE72177001E90A8 /* RCTConvert+RNSVG.m in Sources */, 0CF68B0B1AF0549300FF9E5C /* RNSVGBrush.m in Sources */, + 7F9CDAFA1E1F809C00E0C805 /* RNSVGPathParser.m in Sources */, 10BA0D361CE74E3100887C2B /* RNSVGGroupManager.m in Sources */, 10BA0D4A1CE74E3D00887C2B /* RNSVGLine.m in Sources */, 10FDEEB21D3FB60500A5C46C /* RNSVGBaseBrush.m in Sources */, @@ -518,6 +524,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RNSVG; + PUBLIC_HEADERS_FOLDER_PATH = /usr/local/include/; SKIP_INSTALL = YES; }; name = Debug; @@ -532,6 +539,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RNSVG; + PUBLIC_HEADERS_FOLDER_PATH = /usr/local/include/; SKIP_INSTALL = YES; }; name = Release; diff --git a/ios/Utils/RCTConvert+RNSVG.h b/ios/Utils/RCTConvert+RNSVG.h index a5dc77dc5..2c29fd1c4 100644 --- a/ios/Utils/RCTConvert+RNSVG.h +++ b/ios/Utils/RCTConvert+RNSVG.h @@ -17,13 +17,14 @@ @interface RCTConvert (RNSVG) -+ (CGPathRef)CGPath:(id)json; ++ (CGPathRef)CGPath:(NSString *)d; + (CTTextAlignment)CTTextAlignment:(id)json; + (RNSVGCGFCRule)RNSVGCGFCRule:(id)json; + (RNSVGTextFrame)RNSVGTextFrame:(id)json; + (RNSVGCGFloatArray)RNSVGCGFloatArray:(id)json; + (RNSVGBrush *)RNSVGBrush:(id)json; + + (NSArray *)RNSVGBezier:(id)json; + (CGRect)CGRect:(id)json offset:(NSUInteger)offset; + (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset; diff --git a/ios/Utils/RCTConvert+RNSVG.m b/ios/Utils/RCTConvert+RNSVG.m index b44923a1d..1be458ae4 100644 --- a/ios/Utils/RCTConvert+RNSVG.m +++ b/ios/Utils/RCTConvert+RNSVG.m @@ -15,54 +15,13 @@ #import "RNSVGCGFCRule.h" #import "RNSVGVBMOS.h" #import +#import "RNSVGPathParser.h" @implementation RCTConvert (RNSVG) -+ (CGPathRef)CGPath:(id)json ++ (CGPathRef)CGPath:(NSString *)d { - NSArray *arr = [self NSNumberArray:json]; - - NSUInteger count = [arr count]; - -#define NEXT_VALUE [self double:arr[i++]] - - CGMutablePathRef path = CGPathCreateMutable(); - CGPathMoveToPoint(path, nil, 0, 0); - - @try { - NSUInteger i = 0; - while (i < count) { - NSUInteger type = [arr[i++] unsignedIntegerValue]; - switch (type) { - case 0: - CGPathMoveToPoint(path, nil, NEXT_VALUE, NEXT_VALUE); - break; - case 1: - CGPathCloseSubpath(path); - break; - case 2: - CGPathAddLineToPoint(path, nil, NEXT_VALUE, NEXT_VALUE); - break; - case 3: - CGPathAddCurveToPoint(path, nil, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE); - break; - case 4: - CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0); - break; - default: - RCTLogError(@"Invalid CGPath type %zd at element %zd of %@", type, i, arr); - CGPathRelease(path); - return nil; - } - } - } - @catch (NSException *exception) { - RCTLogError(@"Invalid CGPath format: %@", arr); - CGPathRelease(path); - return nil; - } - - return (CGPathRef)CFAutorelease(path); + return [[[RNSVGPathParser alloc] initWithPathString: d] getPath]; } RCT_ENUM_CONVERTER(CTTextAlignment, (@{ diff --git a/ios/Utils/RNSVGPathParser.h b/ios/Utils/RNSVGPathParser.h new file mode 100644 index 000000000..1815390cd --- /dev/null +++ b/ios/Utils/RNSVGPathParser.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@interface RNSVGPathParser : NSObject + +- (instancetype) initWithPathString:(NSString *)d; +- (CGPathRef)getPath; + +@end diff --git a/ios/Utils/RNSVGPathParser.m b/ios/Utils/RNSVGPathParser.m new file mode 100644 index 000000000..3c07a9ca7 --- /dev/null +++ b/ios/Utils/RNSVGPathParser.m @@ -0,0 +1,379 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNSVGPathParser.h" +#import + +@implementation RNSVGPathParser : NSObject +{ + NSString* _d; + NSString* _originD; + NSRegularExpression* _pathRegularExpression; + double _penX; + double _penY; + double _penDownX; + double _penDownY; + double _pivotX; + double _pivotY; + BOOL _valid; + BOOL _penDownSet; +} + +- (instancetype) initWithPathString:(NSString *)d +{ + if (self = [super init]) { + NSRegularExpression* decimalRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"(\\.\\d+)(?=\\-?\\.)" options:0 error:nil]; + _originD = d; + _d = [decimalRegularExpression stringByReplacingMatchesInString:d options:0 range:NSMakeRange(0, [d length]) withTemplate:@"$1\,"]; + _pathRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+" options:NSRegularExpressionCaseInsensitive error:nil]; + } + return self; +} + +- (CGPathRef)getPath +{ + CGMutablePathRef path = CGPathCreateMutable(); + NSArray* results = [_pathRegularExpression matchesInString:_d options:0 range:NSMakeRange(0, [_d length])]; + + int count = [results count]; + if (count) { + NSUInteger i = 0; + #define NEXT_VALUE [self getNextValue:results[i++]] + #define NEXT_DOUBLE [self double:NEXT_VALUE] + #define NEXT_BOOL [self bool:NEXT_VALUE] + NSString* lastCommand; + NSString* command = NEXT_VALUE; + + @try { + while (command) { + if ([command isEqualToString:@"m"]) { // moveTo command + [self move:path x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"M"]) { + [self moveTo:path x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"l"]) { // lineTo command + [self line:path x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"L"]) { + [self lineTo:path x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"h"]) { // horizontalTo command + [self line:path x:NEXT_DOUBLE y:0]; + } else if ([command isEqualToString:@"H"]) { + [self lineTo:path x:NEXT_DOUBLE y:_penY]; + } else if ([command isEqualToString:@"v"]) { // verticalTo command + [self line:path x:0 y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"V"]) { + [self lineTo:path x:_penX y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"c"]) { // curveTo command + [self curve:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE c2x:NEXT_DOUBLE c2y:NEXT_DOUBLE ex:NEXT_DOUBLE ey:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"C"]) { + [self curveTo:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE c2x:NEXT_DOUBLE c2y:NEXT_DOUBLE ex:NEXT_DOUBLE ey:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"s"]) { // smoothCurveTo command + [self smoothCurve:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE ex:NEXT_DOUBLE ey:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"S"]) { + [self smoothCurveTo:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE ex:NEXT_DOUBLE ey:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"q"]) { // quadraticBezierCurveTo command + [self quadraticBezierCurve:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE c2x:NEXT_DOUBLE c2y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"Q"]) { + [self quadraticBezierCurveTo:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE c2x:NEXT_DOUBLE c2y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"t"]) {// smoothQuadraticBezierCurveTo command + [self smoothQuadraticBezierCurve:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"T"]) { + [self smoothQuadraticBezierCurveTo:path c1x:NEXT_DOUBLE c1y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"a"]) { // arcTo command + [self arc:path rx:NEXT_DOUBLE ry:NEXT_DOUBLE rotation:NEXT_DOUBLE outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"A"]) { + [self arcTo:path rx:NEXT_DOUBLE ry:NEXT_DOUBLE rotation:NEXT_DOUBLE outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_DOUBLE y:NEXT_DOUBLE]; + } else if ([command isEqualToString:@"z"]) { // close command + [self close:path]; + } else if ([command isEqualToString:@"Z"]) { + [self close:path]; + } else { + command = lastCommand; + i--; + continue; + } + + lastCommand = command; + if ([lastCommand isEqualToString:@"m"]) { + lastCommand = @"l"; + } else if ([lastCommand isEqualToString:@"M"]) { + lastCommand = @"L"; + } + + command = i < count ? NEXT_VALUE : nil; + } + } @catch (NSException *exception) { + RCTLogWarn(@"Invalid CGPath format: %@", _originD); + CGPathRelease(path); + return nil; + } + + } + + return (CGPathRef)CFAutorelease(path); +} + +- (NSString *)getNextValue:(NSTextCheckingResult *)result +{ + if (!result) { + return nil; + } + return [_d substringWithRange:NSMakeRange(result.range.location, result.range.length)]; +} + +- (double)double:(NSString *)value +{ + return [value doubleValue]; +} + +- (BOOL)bool:(NSString *)value +{ + return ![value isEqualToString:@"0"]; +} + +- (void)move:(CGPathRef)path x:(double)x y:(double)y +{ + [self moveTo:path x:x + _penX y:y + _penY]; +} + +- (void)moveTo:(CGPathRef)path x:(double)x y:(double)y +{ + _pivotX = _penX = x; + _pivotY = _penY = y; + CGPathMoveToPoint(path, nil, x, y); +} + +- (void)line:(CGPathRef)path x:(double)x y:(double)y +{ + [self lineTo:path x:x + _penX y:y + _penY]; +} + +- (void)lineTo:(CGPathRef)path x:(double)x y:(double)y{ + [self setPenDown]; + _pivotX = _penX = x; + _pivotY = _penY = y; + CGPathAddLineToPoint(path, nil, x, y); +} + +- (void)curve:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey +{ + [self curveTo:path c1x:c1x + _penX + c1y:c1y + _penY + c2x:c2x + _penX + c2y:c2y + _penY + ex:ex + _penX + ey:ey + _penY]; +} + +- (void)curveTo:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey +{ + _pivotX = ex; + _pivotY = ey; + [self curveToPoint:path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey]; +} + +- (void)curveToPoint:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey +{ + [self setPenDown]; + _penX = ex; + _penY = ey; + CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); +} + +- (void)smoothCurve:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y ex:(double)ex ey:(double)ey +{ + [self smoothCurveTo:path c1x:c1x + _penX c1y:c1y + _penY ex:ex + _penX ey:ey + _penY]; +} + +- (void)smoothCurveTo:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y ex:(double)ex ey:(double)ey +{ + double c2x = c1x; + double c2y = c1y; + c1x = (_penX * 2) - _pivotX; + c1y = (_penY * 2) - _pivotY; + _pivotX = c2x; + _pivotY = c2y; + [self curveToPoint:path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey]; +} + +- (void)quadraticBezierCurve:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y +{ + [self quadraticBezierCurveTo:path c1x:(double)c1x + _penX c1y:(double)c1y + _penY c2x:(double)c2x + _penX c2y:(double)c2y + _penY]; +} + +- (void)quadraticBezierCurveTo:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y +{ + _pivotX = c1x; + _pivotY = c1y; + double ex = c2x; + double ey = c2y; + c2x = (ex + c1x * 2) / 3; + c2y = (ey + c1y * 2) / 3; + c1x = (_penX + c1x * 2) / 3; + c1y = (_penY + c1y * 2) / 3; + [self curveToPoint:path c1x:(double)c1x c1y:(double)c1y c2x:(double)c2x c2y:(double)c2y ex:(double)ex ey:(double)ey]; +} + +- (void)smoothQuadraticBezierCurve:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y +{ + [self smoothQuadraticBezierCurveTo:path c1x:c1x + _penX c1y:c1y + _penY]; +} + +- (void)smoothQuadraticBezierCurveTo:(CGPathRef)path c1x:(double)c1x c1y:(double)c1y +{ + double c2x = c1x; + double c2y = c1y; + c1x = (_penX * 2) - _pivotX; + c1y = (_penY * 2) - _pivotY; + [self quadraticBezierCurveTo:path c1x:c1x c1y:c1y c2x:c2x c2y:c2y]; +} + +- (void)arc:(CGPathRef)path rx:(double)rx ry:(double)ry rotation:(double)rotation outer:(BOOL)outer clockwise:(BOOL)clockwise x:(double)x y:(double)y +{ + [self arcTo:path rx:rx ry:ry rotation:rotation outer:outer clockwise:clockwise x:x + _penX y:y + _penY]; +} + +- (void)arcTo:(CGPathRef)path rx:(double)rx ry:(double)ry rotation:(double)rotation outer:(BOOL)outer clockwise:(BOOL)clockwise x:(double)x y:(double)y +{ + double tX = _penX; + double tY = _penY; + + ry = abs(ry == 0 ? (rx == 0 ? (y - tY) : rx) : ry); + rx = abs(rx == 0 ? (x - tX) : rx); + + if (rx == 0 || ry == 0 || (x == tX && y == tY)) { + [self lineTo:path x:x y:y]; + return; + } + + + double rad = rotation * M_PI / 180; + double cosed = cos(rad); + double sined = sin(rad); + x -= tX; + y -= tY; + // Ellipse Center + float cx = cosed * x / 2 + sined * y / 2; + float cy = -sined * x / 2 + cosed * y / 2; + float rxry = rx * rx * ry * ry; + float rycx = ry * ry * cx * cx; + float rxcy = rx * rx * cy * cy; + float a = rxry - rxcy - rycx; + + if (a < 0){ + a = sqrt(1 - a / rxry); + rx *= a; + ry *= a; + cx = x / 2; + cy = y / 2; + } else { + a = sqrt(a / (rxcy + rycx)); + + if (outer == clockwise) { + a = -a; + } + float cxd = -a * cy * rx / ry; + float cyd = a * cx * ry / rx; + cx = cosed * cxd - sined * cyd + x / 2; + cy = sined * cxd + cosed * cyd + y / 2; + } + + // Rotation + Scale Transform + float xx = cosed / rx; + float yx = sined / rx; + float xy = -sined / ry; + float yy = cosed / ry; + + // Start and End Angle + float sa = atan2(xy * -cx + yy * -cy, xx * -cx + yx * -cy); + float ea = atan2(xy * (x - cx) + yy * (y - cy), xx * (x - cx) + yx * (y - cy)); + + cx += tX; + cy += tY; + x += tX; + y += tY; + + [self setPenDown]; + + _penX = _pivotX = x; + _penY = _pivotY = y; + + if (rx != ry || rad != 0) { + [self arcToBezier:path cx:cx cy:cy rx:rx ry:ry sa:sa ea:ea clockwise:clockwise rad:rad]; + } else { + CGPathAddArc(path, nil, cx, cy, rx, sa, ea, !clockwise); + } +} + +- (void)arcToBezier:(CGPathRef)path cx:(double)cx cy:(double)cy rx:(double)rx ry:(double)ry sa:(double)sa ea:(double)ea clockwise:(BOOL)clockwise rad:(double)rad +{ + // Inverse Rotation + Scale Transform + double cosed = cos(rad); + double sined = sin(rad); + double xx = cosed * rx; + double yx = -sined * ry; + double xy = sined * rx; + double yy = cosed * ry; + + // Bezier Curve Approximation + double arc = ea - sa; + if (arc < 0 && clockwise) { + arc += M_PI * 2; + } else if (arc > 0 && !clockwise) { + arc -= M_PI * 2; + } + + int n = ceil(abs(arc / (M_PI / 2))); + + double step = arc / n; + double k = (4 / 3) * tan(step / 4); + + double x = cos(sa); + double y = sin(sa); + + for (int i = 0; i < n; i++){ + double cp1x = x - k * y; + double cp1y = y + k * x; + + sa += step; + x = cos(sa); + y = sin(sa); + + double cp2x = x + k * y; + double cp2y = y - k * x; + + CGPathAddCurveToPoint(path, + nil, + cx + xx * cp1x + yx * cp1y, + cy + xy * cp1x + yy * cp1y, + cx + xx * cp2x + yx * cp2y, + cy + xy * cp2x + yy * cp2y, + cx + xx * x + yx * y, + cy + xy * x + yy * y); + } +} + +- (void)close:(CGPathRef)path +{ + if (_penDownSet) { + _penX = _penDownX; + _penY = _penDownY; + _penDownSet = NO; + CGPathCloseSubpath(path); + } +} + +- (void)setPenDown +{ + if (!_penDownSet) { + _penDownX = _penX; + _penDownY = _penY; + _penDownSet = YES; + } +} + +@end diff --git a/ios/Utils/RNSVGPercentageConverter.m b/ios/Utils/RNSVGPercentageConverter.m index 6b2b0a604..3ba5986fc 100644 --- a/ios/Utils/RNSVGPercentageConverter.m +++ b/ios/Utils/RNSVGPercentageConverter.m @@ -12,7 +12,7 @@ @implementation RNSVGPercentageConverter { CGFloat _relative; CGFloat _offset; - NSRegularExpression *percentageRegularExpression; + NSRegularExpression *_percentageRegularExpression; } - (instancetype) initWithRelativeAndOffset:(CGFloat)relative offset:(CGFloat)offset @@ -20,7 +20,7 @@ - (instancetype) initWithRelativeAndOffset:(CGFloat)relative offset:(CGFloat)off if (self = [super init]) { _relative = relative; _offset = offset; - percentageRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"^(\\-?\\d+(?:\\.\\d+)?)%$" options:0 error:nil]; + _percentageRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"^(\\-?\\d+(?:\\.\\d+)?)%$" options:0 error:nil]; } return self; } @@ -28,14 +28,14 @@ - (instancetype) initWithRelativeAndOffset:(CGFloat)relative offset:(CGFloat)off - (id)init { if (self = [super init]) { - percentageRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"^(\\-?\\d+(?:\\.\\d+)?)%$" options:0 error:nil]; + _percentageRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"^(\\-?\\d+(?:\\.\\d+)?)%$" options:0 error:nil]; } return self; } - (NSRegularExpression *) getPercentageRegularExpression { - return percentageRegularExpression; + return _percentageRegularExpression; } - (CGFloat) stringToFloat:(NSString *)string @@ -61,7 +61,7 @@ - (CGFloat) percentageToFloat:(NSString *)percentage relative:(CGFloat)relative { __block CGFloat matched; - [percentageRegularExpression enumerateMatchesInString:percentage + [_percentageRegularExpression enumerateMatchesInString:percentage options:0 range:NSMakeRange(0, percentage.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) @@ -76,7 +76,7 @@ - (CGFloat) percentageToFloat:(NSString *)percentage relative:(CGFloat)relative - (BOOL) isPercentage:(NSString *) string { - return [percentageRegularExpression firstMatchInString:string options:0 range:NSMakeRange(0, [string length])] != nil; + return [_percentageRegularExpression firstMatchInString:string options:0 range:NSMakeRange(0, [string length])] != nil; } @end diff --git a/lib/extract/extractClipping.js b/lib/extract/extractClipping.js index 6b03a60df..fea0bff11 100644 --- a/lib/extract/extractClipping.js +++ b/lib/extract/extractClipping.js @@ -1,4 +1,3 @@ -import SerializablePath from '../SerializablePath'; import clipReg from './patternReg'; const clipRules = { @@ -18,7 +17,7 @@ export default function (props) { if (matched) { clippingProps.clipPathRef = matched[1]; } else { - clippingProps.clipPath = new SerializablePath(clipPath).toJSON(); + clippingProps.clipPath = clipPath; } } From 609226d732bcf4cbdbacafca362eedc69c29e754 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sat, 7 Jan 2017 21:46:08 +0800 Subject: [PATCH 04/10] Fix arcTo circle bug --- .../main/java/com/horcrux/svg/PropHelper.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/PropHelper.java b/android/src/main/java/com/horcrux/svg/PropHelper.java index 2f02a675d..52472629f 100644 --- a/android/src/main/java/com/horcrux/svg/PropHelper.java +++ b/android/src/main/java/com/horcrux/svg/PropHelper.java @@ -494,22 +494,34 @@ private void arcTo(float rx, float ry, float rotation, boolean outer, boolean cl mPenX = mPivotX = x; mPenY = mPivotY = y; - //arcToBezier(cx, cy, rx, ry, sa, ea, clockwise, rad); if (rx != ry || rad != 0f) { arcToBezier(cx, cy, rx, ry, sa, ea, clockwise, rad); } else { + float start = (float) Math.toDegrees(sa); float end = (float) Math.toDegrees(ea); + float sweep = Math.abs((start - end) % 360); + + if (outer) { + if (sweep < 180) { + sweep = 360 - sweep; + } + } else { + if (sweep > 180) { + sweep = 360 - sweep; + } + } if (!clockwise) { - end = 360 - end; + sweep = -sweep; } - float sweep = start - end; + RectF oval = new RectF( (cx - rx) * mScale, (cy - rx) * mScale, (cx + rx) * mScale, (cy + rx) * mScale); + mPath.arcTo(oval, start, sweep); } } From 152e839126e66a708a9492d203ef7fb4302e1030 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sat, 7 Jan 2017 23:17:10 +0800 Subject: [PATCH 05/10] Remove path data usage in clipPath Remove path data usage in clipPath. Use clipPath in SVG way --- .../com/horcrux/svg/SvgViewShadowNode.java | 7 +- .../java/com/horcrux/svg/VirtualNode.java | 71 ++++++++----------- ios/Elements/RNSVGPath.m | 3 +- ios/RNSVGNode.h | 7 +- ios/RNSVGNode.m | 35 +++++---- ios/RNSVGRenderable.m | 16 +++-- ios/ViewManagers/RNSVGNodeManager.m | 3 +- lib/attributes.js | 5 +- lib/extract/extractClipping.js | 4 +- 9 files changed, 67 insertions(+), 84 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java index 2e347c31f..6f92aed87 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java @@ -45,12 +45,11 @@ public static SvgViewShadowNode getShadowNodeByTag(int tag) { return mTagToShadowNode.get(tag); } - private boolean mHasPendingUpdates; private boolean mResponsible = false; - private static final Map mDefinedClipPaths = new HashMap<>(); - private static final Map mDefinedTemplates = new HashMap<>(); - private static final Map mDefinedBrushes = new HashMap<>(); + private final Map mDefinedClipPaths = new HashMap<>(); + private final Map mDefinedTemplates = new HashMap<>(); + private final Map mDefinedBrushes = new HashMap<>(); @Override public boolean isVirtual() { diff --git a/android/src/main/java/com/horcrux/svg/VirtualNode.java b/android/src/main/java/com/horcrux/svg/VirtualNode.java index 8de108f73..ea64da335 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/VirtualNode.java @@ -17,8 +17,10 @@ import android.graphics.Rect; import android.graphics.Region; +import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.ReactShadowNode; @@ -35,8 +37,7 @@ public abstract class VirtualNode extends LayoutShadowNode { protected float mOpacity = 1f; protected Matrix mMatrix = new Matrix(); - protected @Nullable Path mClipPath; - protected @Nullable String mClipPathRef; + protected @Nullable String mClipPath; private static final int PATH_TYPE_CLOSE = 1; private static final int PATH_TYPE_CURVETO = 3; @@ -47,10 +48,7 @@ public abstract class VirtualNode extends LayoutShadowNode { private static final int CLIP_RULE_NONZERO = 1; protected final float mScale; - private float[] mClipData; private int mClipRule; - private boolean mClipRuleSet; - private boolean mClipDataSet; protected boolean mResponsible; protected int mCanvasX; protected int mCanvasY; @@ -96,14 +94,6 @@ protected void restoreCanvas(Canvas canvas, int count) { canvas.restoreToCount(count); } - @ReactProp(name = "clipPath") - public void setClipPath(@Nullable ReadableArray clipPath) { - mClipData = PropHelper.toFloatArray(clipPath); - mClipDataSet = true; - setupClip(); - markUpdated(); - } - @ReactProp(name = "name") public void setName(String name) { mName = name; @@ -111,17 +101,15 @@ public void setName(String name) { } - @ReactProp(name = "clipPathRef") - public void setClipPathRef(String clipPathRef) { - mClipPathRef = clipPathRef; + @ReactProp(name = "clipPath") + public void setClipPath(String clipPath) { + mClipPath = clipPath; markUpdated(); } @ReactProp(name = "clipRule", defaultInt = CLIP_RULE_NONZERO) - public void setClipRule(int clipRule) { + public void clipRule(int clipRule) { mClipRule = clipRule; - mClipRuleSet = true; - setupClip(); markUpdated(); } @@ -138,7 +126,7 @@ public void setMatrix(@Nullable ReadableArray matrixArray) { if (matrixSize == 6) { setupMatrix(); } else if (matrixSize != -1) { - throw new JSApplicationIllegalArgumentException("Transform matrices must be of size 6"); + FLog.w(ReactConstants.TAG, "RNSVG: Transform matrices must be of size 6"); } } else { mMatrix = null; @@ -153,24 +141,6 @@ public void setResponsible(boolean responsible) { markUpdated(); } - private void setupClip() { - if (mClipDataSet && mClipRuleSet) { - mClipPath = new Path(); - - switch (mClipRule) { - case CLIP_RULE_EVENODD: - mClipPath.setFillType(Path.FillType.EVEN_ODD); - break; - case CLIP_RULE_NONZERO: - break; - default: - throw new JSApplicationIllegalArgumentException( - "clipRule " + mClipRule + " unrecognized"); - } - createPath(mClipData, mClipPath); - } - } - protected void setupMatrix() { sRawMatrix[0] = sMatrixData[0]; sRawMatrix[1] = sMatrixData[2]; @@ -226,13 +196,28 @@ protected void createPath(float[] data, Path path) { } protected @Nullable Path getClipPath(Canvas canvas, Paint paint) { - Path clip = mClipPath; - if (clip == null && mClipPathRef != null) { - VirtualNode node = getSvgShadowNode().getDefinedClipPath(mClipPathRef); - clip = node.getPath(canvas, paint); + if (mClipPath != null) { + VirtualNode node = getSvgShadowNode().getDefinedClipPath(mClipPath); + + if (node != null) { + Path clipPath = node.getPath(canvas, paint); + switch (mClipRule) { + case CLIP_RULE_EVENODD: + clipPath.setFillType(Path.FillType.EVEN_ODD); + break; + case CLIP_RULE_NONZERO: + break; + default: + FLog.w(ReactConstants.TAG, "RNSVG: clipRule: " + mClipRule + " unrecognized"); + } + + return clipPath; + } else { + FLog.w(ReactConstants.TAG, "RNSVG: Undefined clipPath: " + mClipPath); + } } - return clip; + return null; } protected void clip(Canvas canvas, Paint paint) { diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index c71a40819..6556886a9 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -45,8 +45,7 @@ - (void)renderLayerTo:(CGContextRef)context CGPathRelease(strokePath); } - CGAffineTransform transform = self.matrix; - self.hitArea = CFAutorelease(CGPathCreateCopyByTransformingPath(hitArea, &transform)); + self.hitArea = CGPathCreateCopy(hitArea); CGPathRelease(hitArea); } diff --git a/ios/RNSVGNode.h b/ios/RNSVGNode.h index e8adf1c85..710cd3aa1 100644 --- a/ios/RNSVGNode.h +++ b/ios/RNSVGNode.h @@ -20,8 +20,7 @@ @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) CGFloat opacity; @property (nonatomic, assign) RNSVGCGFCRule clipRule; -@property (nonatomic, assign) CGPathRef clipPath; // convert clipPath="M0,0 L0,10 L10,10z" into path -@property (nonatomic, strong) NSString *clipPathRef; // use clipPath="url(#clip)" as ClipPath +@property (nonatomic, strong) NSString *clipPath; @property (nonatomic, assign) BOOL responsible; @property (nonatomic, assign) CGAffineTransform matrix; @property (nonatomic, assign) BOOL active; @@ -37,7 +36,9 @@ */ - (void)renderLayerTo:(CGContextRef)context; -- (void)renderClip:(CGContextRef)context; +- (CGPathRef)getClipPath; + +- (CGPathRef)getClipPath:(CGContextRef)context; /** * clip node by clipPath or clipPathRef. diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index a719123c9..6d397e832 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -13,6 +13,7 @@ @implementation RNSVGNode { BOOL _transparent; + CGPathRef _cachedClipPath; } - (instancetype)init @@ -78,24 +79,15 @@ - (void)setMatrix:(CGAffineTransform)matrix _matrix = matrix; } -- (void)setClipPath:(CGPathRef)clipPath +- (void)setClipPath:(NSString *)clipPath { if (_clipPath == clipPath) { return; } + CGPathRelease(_cachedClipPath); + _cachedClipPath = nil; + _clipPath = clipPath; [self invalidate]; - CGPathRelease(_clipPath); - _clipPath = CGPathRetain(clipPath); -} - -- (void)setClipPathRef:(NSString *)clipPathRef -{ - if (_clipPathRef == clipPathRef) { - return; - } - [self invalidate]; - self.clipPath = nil; - _clipPathRef = clipPathRef; } - (void)beginTransparencyLayer:(CGContextRef)context @@ -117,16 +109,23 @@ - (void)renderTo:(CGContextRef)context // abstract } -- (void)renderClip:(CGContextRef)context +- (CGPathRef)getClipPath { - if (self.clipPathRef) { - self.clipPath = [[[self getSvgView] getDefinedClipPath:self.clipPathRef] getPath:context]; + return _cachedClipPath; +} + +- (CGPathRef)getClipPath:(CGContextRef)context +{ + if (self.clipPath) { + _cachedClipPath = CGPathRetain([[[self getSvgView] getDefinedClipPath:self.clipPath] getPath:context]); } + + return [self getClipPath]; } - (void)clip:(CGContextRef)context { - CGPathRef clipPath = self.clipPath; + CGPathRef clipPath = [self getClipPath:context]; if (clipPath) { CGContextAddPath(context, clipPath); @@ -209,7 +208,7 @@ - (void)resetProperties - (void)dealloc { - CGPathRelease(_clipPath); + CGPathRelease(_cachedClipPath); } @end diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 8e6ecbd77..27b109986 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -169,7 +169,6 @@ - (void)renderTo:(CGContextRef)context CGContextSetAlpha(context, self.opacity); [self beginTransparencyLayer:context]; - [self renderClip:context]; [self renderLayerTo:context]; [self endTransparencyLayer:context]; @@ -182,7 +181,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event return [self hitTest:point withEvent:event withTransform:CGAffineTransformMakeRotation(0)]; } -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event withTransform:(CGAffineTransform)transfrom +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event withTransform:(CGAffineTransform)transform { if (self.active) { if (!event) { @@ -190,16 +189,21 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event withTransform:(CGA } return self; } - - CGPathRef hitArea = CGPathCreateCopyByTransformingPath(self.hitArea, &transfrom); - CGPathRef clipPath = self.clipPath; + + CGAffineTransform matrix = CGAffineTransformConcat(self.matrix, transform); + CGPathRef hitArea = CGPathCreateCopyByTransformingPath(self.hitArea, &matrix); + CGPathRef clipPath = [self getClipPath]; BOOL contains = CGPathContainsPoint(hitArea, nil, point, NO); CGPathRelease(hitArea); + if (contains) { if (!clipPath) { return self; } else { - return CGPathContainsPoint(clipPath, nil, point, NO) ? self : nil; + clipPath = CGPathCreateCopyByTransformingPath(clipPath, &matrix); + BOOL result = CGPathContainsPoint(clipPath, nil, point, self.clipRule == kRNSVGCGFCRuleEvenodd); + CGPathRelease(clipPath); + return result ? self : nil; } } else { return nil; diff --git a/ios/ViewManagers/RNSVGNodeManager.m b/ios/ViewManagers/RNSVGNodeManager.m index 8af8568fa..5cda8ca4c 100644 --- a/ios/ViewManagers/RNSVGNodeManager.m +++ b/ios/ViewManagers/RNSVGNodeManager.m @@ -32,8 +32,7 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(name, NSString) RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(matrix, CGAffineTransform) -RCT_EXPORT_VIEW_PROPERTY(clipPathRef, NSString) -RCT_EXPORT_VIEW_PROPERTY(clipPath, CGPath) +RCT_EXPORT_VIEW_PROPERTY(clipPath, NSString) RCT_EXPORT_VIEW_PROPERTY(clipRule, RNSVGCGFCRule) RCT_EXPORT_VIEW_PROPERTY(responsible, BOOL) diff --git a/lib/attributes.js b/lib/attributes.js index 90f5b0394..6cda80314 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -58,10 +58,7 @@ const NodeAttributes = { }, opacity: true, clipRule: true, - clipPathRef: true, - clipPath: { - diff: arrayDiffer - }, + clipPath: true, propList: { diff: arrayDiffer }, diff --git a/lib/extract/extractClipping.js b/lib/extract/extractClipping.js index fea0bff11..605c42e51 100644 --- a/lib/extract/extractClipping.js +++ b/lib/extract/extractClipping.js @@ -15,9 +15,9 @@ export default function (props) { let matched = clipPath.match(clipReg); if (matched) { - clippingProps.clipPathRef = matched[1]; + clippingProps.clipPath = matched[1]; } else { - clippingProps.clipPath = clipPath; + console.warn('Invalid `clipPath` prop, expected a clipPath like `"#id"`, but got: "' + clipPath + '"'); } } From 431b61ce7653a409d0a108dd21b4f01293641b56 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sun, 8 Jan 2017 17:08:37 +0800 Subject: [PATCH 06/10] Fix memory leaks in iOS Fix memory leaks in iOS and do not render stroke if strokeWidth is undefined --- ios/Elements/RNSVGGroup.m | 2 +- ios/Elements/RNSVGPath.m | 9 ++++---- ios/Elements/RNSVGUse.m | 2 +- ios/RNSVGNode.m | 5 ++-- ios/RNSVGRenderable.h | 2 +- ios/RNSVGRenderable.m | 48 ++++++++++++++++++--------------------- ios/RNSVGViewBox.m | 8 ++++--- 7 files changed, 37 insertions(+), 39 deletions(-) diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index b6ff38721..b8caee79c 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -24,7 +24,7 @@ - (void)renderLayerTo:(CGContextRef)context }]; [self traverseSubviews:^(RNSVGNode *node) { - [node mergeProperties:self mergeList:self.ownedPropList inherited:YES]; + [node mergeProperties:self mergeList:self.attributeList inherited:YES]; [node renderTo:context]; return YES; }]; diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index 6556886a9..97772e020 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -29,8 +29,7 @@ - (void)dealloc - (void)renderLayerTo:(CGContextRef)context { // todo: add detection if path has changed since last update. - self.d = [self getPath:context]; - CGPathRef path = self.d; + CGPathRef path = [self getPath:context]; if ((!self.fill && !self.stroke) || !path) { return; } @@ -38,14 +37,14 @@ - (void)renderLayerTo:(CGContextRef)context if ([self getSvgView].responsible) { // Add path to hitArea CGMutablePathRef hitArea = CGPathCreateMutableCopy(path); - if (self.stroke) { + if (self.stroke && self.strokeWidth) { // Add stroke to hitArea CGPathRef strokePath = CGPathCreateCopyByStrokingPath(hitArea, nil, self.strokeWidth, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit); CGPathAddPath(hitArea, nil, strokePath); CGPathRelease(strokePath); } - self.hitArea = CGPathCreateCopy(hitArea); + self.hitArea = CFAutorelease(CGPathCreateCopy(hitArea)); CGPathRelease(hitArea); } @@ -75,7 +74,7 @@ - (void)renderLayerTo:(CGContextRef)context } } - if (self.stroke) { + if (self.stroke && self.strokeWidth) { CGContextSetLineWidth(context, self.strokeWidth); CGContextSetLineCap(context, self.strokeLinecap); CGContextSetLineJoin(context, self.strokeLinejoin); diff --git a/ios/Elements/RNSVGUse.m b/ios/Elements/RNSVGUse.m index fea9e7a75..e56382b67 100644 --- a/ios/Elements/RNSVGUse.m +++ b/ios/Elements/RNSVGUse.m @@ -27,7 +27,7 @@ - (void)renderLayerTo:(CGContextRef)context if (template) { [self beginTransparencyLayer:context]; [self clip:context]; - [template mergeProperties:self mergeList:self.ownedPropList]; + [template mergeProperties:self mergeList:self.attributeList inherited:YES]; [template renderTo:context]; [template resetProperties]; [self endTransparencyLayer:context]; diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index 6d397e832..d56cfd9d5 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -116,7 +116,8 @@ - (CGPathRef)getClipPath - (CGPathRef)getClipPath:(CGContextRef)context { - if (self.clipPath) { + if (self.clipPath && !_cachedClipPath) { + CGPathRelease(_cachedClipPath); _cachedClipPath = CGPathRetain([[[self getSvgView] getDefinedClipPath:self.clipPath] getPath:context]); } @@ -125,7 +126,7 @@ - (CGPathRef)getClipPath:(CGContextRef)context - (void)clip:(CGContextRef)context { - CGPathRef clipPath = [self getClipPath:context]; + CGPathRef clipPath = [self getClipPath:context]; if (clipPath) { CGContextAddPath(context, clipPath); diff --git a/ios/RNSVGRenderable.h b/ios/RNSVGRenderable.h index 6866e511f..29b36c662 100644 --- a/ios/RNSVGRenderable.h +++ b/ios/RNSVGRenderable.h @@ -28,7 +28,7 @@ @property (nonatomic, assign) CGFloat strokeDashoffset; @property (nonatomic, assign) CGPathRef hitArea; @property (nonatomic, copy) NSArray *propList; -@property (nonatomic, strong) NSMutableArray *ownedPropList; +@property (nonatomic, strong) NSArray *attributeList; - (void)setBoundingBox:(CGRect)boundingBox; - (CGFloat)getWidthRelatedValue:(NSString *)string; diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 27b109986..126cef3db 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -12,7 +12,7 @@ @implementation RNSVGRenderable { NSMutableDictionary *_originProperties; - NSArray *_changedList; + NSArray *_lastMergedList; RNSVGPercentageConverter *_widthConverter; RNSVGPercentageConverter *_heightConverter; CGFloat _contextWidth; @@ -148,8 +148,8 @@ - (void)setPropList:(NSArray *)propList if (propList == _propList) { return; } + _attributeList = [propList copy]; _propList = propList; - self.ownedPropList = [propList mutableCopy]; [self invalidate]; } @@ -191,18 +191,20 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event withTransform:(CGA } CGAffineTransform matrix = CGAffineTransformConcat(self.matrix, transform); + CGPathRef hitArea = CGPathCreateCopyByTransformingPath(self.hitArea, &matrix); - CGPathRef clipPath = [self getClipPath]; BOOL contains = CGPathContainsPoint(hitArea, nil, point, NO); CGPathRelease(hitArea); if (contains) { + CGPathRef clipPath = [self getClipPath]; + if (!clipPath) { return self; } else { - clipPath = CGPathCreateCopyByTransformingPath(clipPath, &matrix); - BOOL result = CGPathContainsPoint(clipPath, nil, point, self.clipRule == kRNSVGCGFCRuleEvenodd); - CGPathRelease(clipPath); + CGPathRef transformedClipPath = CGPathCreateCopyByTransformingPath(clipPath, &matrix); + BOOL result = CGPathContainsPoint(transformedClipPath, nil, point, self.clipRule == kRNSVGCGFCRuleEvenodd); + CGPathRelease(transformedClipPath); return result ? self : nil; } } else { @@ -255,44 +257,38 @@ - (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray *)mergeList inherited:(BOOL)inherited { + _lastMergedList = mergeList; + if (mergeList.count == 0) { return; } - self.ownedPropList = [self.propList mutableCopy]; + NSMutableArray* attributeList = [self.propList mutableCopy]; - if (!inherited) { - _originProperties = [[NSMutableDictionary alloc] init]; - _changedList = mergeList; - } + _originProperties = [[NSMutableDictionary alloc] init]; for (NSString *key in mergeList) { if (inherited) { - [self inheritProperty:target propName:key]; + if (![attributeList containsObject:key]) { + [attributeList addObject:key]; + [_originProperties setValue:[self valueForKey:key] forKey:key]; + [self setValue:[target valueForKey:key] forKey:key]; + } } else { [_originProperties setValue:[self valueForKey:key] forKey:key]; [self setValue:[target valueForKey:key] forKey:key]; } } + + _attributeList = [attributeList copy]; } - (void)resetProperties { - if (_changedList) { - for (NSString *key in _changedList) { - [self setValue:[_originProperties valueForKey:key] forKey:key]; - } - } - _changedList = nil; -} - -- (void)inheritProperty:(__kindof RNSVGNode *)parent propName:(NSString *)propName -{ - if (![self.ownedPropList containsObject:propName]) { - // add prop to props - [self.ownedPropList addObject:propName]; - [self setValue:[parent valueForKey:propName] forKey:propName]; + for (NSString *key in _lastMergedList) { + [self setValue:[_originProperties valueForKey:key] forKey:key]; } + _attributeList = [_propList copy]; } - (void)renderLayerTo:(CGContextRef)context diff --git a/ios/RNSVGViewBox.m b/ios/RNSVGViewBox.m index fa7903616..80772557a 100644 --- a/ios/RNSVGViewBox.m +++ b/ios/RNSVGViewBox.m @@ -160,7 +160,7 @@ - (CGAffineTransform)getTransform return CGAffineTransformTranslate(transform, -translateX * (_fromSymbol ? scaleX : 1), -translateY * (_fromSymbol ? scaleY : 1)); } -- (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray *)mergeList +- (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray *)mergeList inherited:(BOOL)inherited { if ([target isKindOfClass:[RNSVGUse class]]) { RNSVGUse *use = target; @@ -172,8 +172,10 @@ - (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray Date: Sun, 8 Jan 2017 17:43:03 +0800 Subject: [PATCH 07/10] move renderLayerTo to Renderable Move renderLayerTo to Renderable. --- ios/Elements/RNSVGPath.m | 99 ++---------------------------------- ios/RNSVGRenderable.m | 99 +++++++++++++++++++++++++++++++++--- ios/Shapes/RNSVGCircle.h | 2 +- ios/Shapes/RNSVGEllipse.h | 2 +- ios/Shapes/RNSVGLine.h | 2 +- ios/Shapes/RNSVGRect.h | 2 +- lib/extract/extractStroke.js | 6 +-- 7 files changed, 102 insertions(+), 110 deletions(-) diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index 97772e020..c4b56eef1 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -21,105 +21,14 @@ - (void)setD:(CGPathRef)d _d = CGPathRetain(d); } -- (void)dealloc -{ - CGPathRelease(_d); -} - -- (void)renderLayerTo:(CGContextRef)context +- (CGPathRef)getPath:(CGContextRef)context { - // todo: add detection if path has changed since last update. - CGPathRef path = [self getPath:context]; - if ((!self.fill && !self.stroke) || !path) { - return; - } - - if ([self getSvgView].responsible) { - // Add path to hitArea - CGMutablePathRef hitArea = CGPathCreateMutableCopy(path); - if (self.stroke && self.strokeWidth) { - // Add stroke to hitArea - CGPathRef strokePath = CGPathCreateCopyByStrokingPath(hitArea, nil, self.strokeWidth, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit); - CGPathAddPath(hitArea, nil, strokePath); - CGPathRelease(strokePath); - } - - self.hitArea = CFAutorelease(CGPathCreateCopy(hitArea)); - CGPathRelease(hitArea); - } - - if (self.opacity == 0) { - return; - } - - CGPathDrawingMode mode = kCGPathStroke; - BOOL fillColor = YES; - - if (self.fill) { - mode = self.fillRule == kRNSVGCGFCRuleEvenodd ? kCGPathEOFill : kCGPathFill; - fillColor = [self.fill applyFillColor:context opacity:self.fillOpacity]; - - if (!fillColor) { - [self clip:context]; - - CGContextSaveGState(context); - CGContextAddPath(context, path); - CGContextClip(context); - RNSVGBrushConverter *brushConverter = [[self getSvgView] getDefinedBrushConverter:[self.fill brushRef]]; - [self.fill paint:context opacity:self.fillOpacity brushConverter:brushConverter]; - CGContextRestoreGState(context); - if (!self.stroke) { - return; - } - } - } - - if (self.stroke && self.strokeWidth) { - CGContextSetLineWidth(context, self.strokeWidth); - CGContextSetLineCap(context, self.strokeLinecap); - CGContextSetLineJoin(context, self.strokeLinejoin); - RNSVGCGFloatArray dash = self.strokeDasharray; - - if (dash.count) { - CGContextSetLineDash(context, self.strokeDashoffset, dash.array, dash.count); - } - - if (!fillColor) { - CGContextAddPath(context, path); - CGContextReplacePathWithStrokedPath(context); - CGContextClip(context); - } - - if ([self.stroke applyStrokeColor:context opacity:self.strokeOpacity]) { - if (mode == kCGPathFill) { - mode = kCGPathFillStroke; - } else if (mode == kCGPathEOFill) { - mode = kCGPathEOFillStroke; - } - } else { - // draw fill - [self clip:context]; - CGContextAddPath(context, path); - CGContextDrawPath(context, mode); - - // draw stroke - CGContextAddPath(context, path); - CGContextReplacePathWithStrokedPath(context); - CGContextClip(context); - RNSVGBrushConverter *brushConverter = [[self getSvgView] getDefinedBrushConverter:[self.stroke brushRef]]; - [self.stroke paint:context opacity:self.strokeOpacity brushConverter:brushConverter]; - return; - } - } - - [self clip:context]; - CGContextAddPath(context, path); - CGContextDrawPath(context, mode); + return self.d; } -- (CGPathRef)getPath:(CGContextRef)context +- (void)dealloc { - return self.d; + CGPathRelease(_d); } @end diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 126cef3db..96b7dfc7c 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -25,7 +25,7 @@ - (id)init if (self = [super init]) { _fillOpacity = 1; _strokeOpacity = 1; - _strokeWidth = 1; + _strokeWidth = 0; } return self; } @@ -175,6 +175,98 @@ - (void)renderTo:(CGContextRef)context CGContextRestoreGState(context); } +- (void)renderLayerTo:(CGContextRef)context +{ + // todo: add detection if path has changed since last update. + CGPathRef path = [self getPath:context]; + if ((!self.fill && !self.stroke) || !path) { + return; + } + + if ([self getSvgView].responsible) { + // Add path to hitArea + CGMutablePathRef hitArea = CGPathCreateMutableCopy(path); + if (self.stroke && self.strokeWidth) { + // Add stroke to hitArea + CGPathRef strokePath = CGPathCreateCopyByStrokingPath(hitArea, nil, self.strokeWidth, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit); + CGPathAddPath(hitArea, nil, strokePath); + CGPathRelease(strokePath); + } + + self.hitArea = CFAutorelease(CGPathCreateCopy(hitArea)); + CGPathRelease(hitArea); + } + + if (self.opacity == 0) { + return; + } + + CGPathDrawingMode mode = kCGPathStroke; + BOOL fillColor = YES; + + if (self.fill) { + mode = self.fillRule == kRNSVGCGFCRuleEvenodd ? kCGPathEOFill : kCGPathFill; + fillColor = [self.fill applyFillColor:context opacity:self.fillOpacity]; + + if (!fillColor) { + [self clip:context]; + + CGContextSaveGState(context); + CGContextAddPath(context, path); + CGContextClip(context); + RNSVGBrushConverter *brushConverter = [[self getSvgView] getDefinedBrushConverter:[self.fill brushRef]]; + [self.fill paint:context opacity:self.fillOpacity brushConverter:brushConverter]; + CGContextRestoreGState(context); + if (!self.stroke) { + return; + } + } + } + + if (self.stroke && self.strokeWidth) { + CGContextSetLineWidth(context, self.strokeWidth); + CGContextSetLineCap(context, self.strokeLinecap); + CGContextSetLineJoin(context, self.strokeLinejoin); + RNSVGCGFloatArray dash = self.strokeDasharray; + + if (dash.count) { + CGContextSetLineDash(context, self.strokeDashoffset, dash.array, dash.count); + } + + if (!fillColor) { + CGContextAddPath(context, path); + CGContextReplacePathWithStrokedPath(context); + CGContextClip(context); + } + + if ([self.stroke applyStrokeColor:context opacity:self.strokeOpacity]) { + if (mode == kCGPathFill) { + mode = kCGPathFillStroke; + } else if (mode == kCGPathEOFill) { + mode = kCGPathEOFillStroke; + } + } else { + // draw fill + [self clip:context]; + CGContextAddPath(context, path); + CGContextDrawPath(context, mode); + + // draw stroke + CGContextAddPath(context, path); + CGContextReplacePathWithStrokedPath(context); + CGContextClip(context); + RNSVGBrushConverter *brushConverter = [[self getSvgView] getDefinedBrushConverter:[self.stroke brushRef]]; + [self.stroke paint:context opacity:self.strokeOpacity brushConverter:brushConverter]; + return; + } + } + + [self clip:context]; + CGContextAddPath(context, path); + CGContextDrawPath(context, mode); +} + + // hitTest delagate - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { @@ -291,9 +383,4 @@ - (void)resetProperties _attributeList = [_propList copy]; } -- (void)renderLayerTo:(CGContextRef)context -{ - // abstract -} - @end diff --git a/ios/Shapes/RNSVGCircle.h b/ios/Shapes/RNSVGCircle.h index b1f086b3b..8d195803b 100644 --- a/ios/Shapes/RNSVGCircle.h +++ b/ios/Shapes/RNSVGCircle.h @@ -10,7 +10,7 @@ #import "RNSVGPath.h" -@interface RNSVGCircle : RNSVGPath +@interface RNSVGCircle : RNSVGRenderable @property (nonatomic, strong) NSString* cx; @property (nonatomic, strong) NSString* cy; diff --git a/ios/Shapes/RNSVGEllipse.h b/ios/Shapes/RNSVGEllipse.h index 17d279be2..67c246a3e 100644 --- a/ios/Shapes/RNSVGEllipse.h +++ b/ios/Shapes/RNSVGEllipse.h @@ -10,7 +10,7 @@ #import "RNSVGPath.h" -@interface RNSVGEllipse : RNSVGPath +@interface RNSVGEllipse : RNSVGRenderable @property (nonatomic, strong) NSString* cx; @property (nonatomic, strong) NSString* cy; @property (nonatomic, strong) NSString* rx; diff --git a/ios/Shapes/RNSVGLine.h b/ios/Shapes/RNSVGLine.h index 0263e3a1d..e1df2ccb6 100644 --- a/ios/Shapes/RNSVGLine.h +++ b/ios/Shapes/RNSVGLine.h @@ -10,7 +10,7 @@ #import "RNSVGPath.h" -@interface RNSVGLine : RNSVGPath +@interface RNSVGLine : RNSVGRenderable @property (nonatomic, strong) NSString* x1; @property (nonatomic, strong) NSString* y1; @property (nonatomic, strong) NSString* x2; diff --git a/ios/Shapes/RNSVGRect.h b/ios/Shapes/RNSVGRect.h index c63c1b829..224aeef79 100644 --- a/ios/Shapes/RNSVGRect.h +++ b/ios/Shapes/RNSVGRect.h @@ -10,7 +10,7 @@ #import "RNSVGPath.h" -@interface RNSVGRect : RNSVGPath +@interface RNSVGRect : RNSVGRenderable @property (nonatomic, strong) NSString* x; @property (nonatomic, strong) NSString* y; diff --git a/lib/extract/extractStroke.js b/lib/extract/extractStroke.js index 94b08568b..ad978867d 100644 --- a/lib/extract/extractStroke.js +++ b/lib/extract/extractStroke.js @@ -20,10 +20,6 @@ export default function(props) { let strokeWidth = +props.strokeWidth; - if (_.isNil(props.strokeWidth)) { - strokeWidth = null; - } - let strokeDasharray = props.strokeDasharray; if (typeof strokeDasharray === 'string') { @@ -41,7 +37,7 @@ export default function(props) { strokeLinecap: caps[props.strokeLinecap] || 0, strokeLinejoin: joins[props.strokeLinejoin] || 0, strokeDasharray: strokeDasharray || null, - strokeWidth: strokeWidth, + strokeWidth: strokeWidth || null, strokeDashoffset: strokeDasharray ? (+props.strokeDashoffset || 0) : null, strokeMiterlimit: props.strokeMiterlimit || 4 }; From 074166b111cdeb5a3b2a4ec04b23fe7d0d5afa0e Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sun, 8 Jan 2017 19:05:37 +0800 Subject: [PATCH 08/10] rename mergeProperties related variables --- .../java/com/horcrux/svg/GroupShadowNode.java | 6 +- .../com/horcrux/svg/RenderableShadowNode.java | 69 ++++++++----------- .../java/com/horcrux/svg/UseShadowNode.java | 2 +- .../com/horcrux/svg/ViewBoxShadowNode.java | 2 +- ios/Elements/RNSVGGroup.m | 4 +- 5 files changed, 34 insertions(+), 49 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index fad5b6128..60473c455 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -38,7 +38,7 @@ public void draw(final Canvas canvas, final Paint paint, final float opacity) { public boolean run(VirtualNode node) { node.setupDimensions(canvas); - node.mergeProperties(self, mOwnedPropList, true); + node.mergeProperties(self, mAttributeList, true); node.draw(canvas, paint, opacity * mOpacity); node.markUpdateSeen(); @@ -109,10 +109,10 @@ public boolean run(VirtualNode node) { } @Override - public void mergeProperties(final VirtualNode target, final ReadableArray mergeList) { + public void mergeProperties(final VirtualNode target, final ReadableArray mergeList, final boolean inherited) { traverseChildren(new NodeRunnable() { public boolean run(VirtualNode node) { - node.mergeProperties(target, mergeList); + node.mergeProperties(target, mergeList, inherited); return true; } }); diff --git a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java index 2283e1cdc..7d9e7fa39 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java @@ -24,6 +24,7 @@ import android.util.Log; import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.ReadableArray; @@ -72,10 +73,10 @@ abstract public class RenderableShadowNode extends VirtualNode { protected Path mPath; - private ArrayList mChangedList; + private ReadableArray mLastMergedList; private ArrayList mOriginProperties; protected ReadableArray mPropList = new JavaOnlyArray(); - protected WritableArray mOwnedPropList = new JavaOnlyArray(); + protected WritableArray mAttributeList = new JavaOnlyArray(); @ReactProp(name = "fill") public void setFill(@Nullable ReadableArray fill) { @@ -136,7 +137,7 @@ public void setStrokeDashoffset(float strokeWidth) { markUpdated(); } - @ReactProp(name = "strokeWidth", defaultFloat = 1f) + @ReactProp(name = "strokeWidth", defaultFloat = 0f) public void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; markUpdated(); @@ -188,18 +189,16 @@ public void setStrokeLinejoin(int strokeLinejoin) { @ReactProp(name = "propList") public void setPropList(@Nullable ReadableArray propList) { - WritableArray copy = new JavaOnlyArray(); - if (propList != null) { + WritableArray copy = Arguments.createArray(); for (int i = 0; i < propList.size(); i++) { String fieldName = propertyNameToFieldName(propList.getString(i)); copy.pushString(fieldName); - mOwnedPropList.pushString(fieldName); + mAttributeList.pushString(fieldName); } - + mPropList = copy; } - mPropList = copy; markUpdated(); } @@ -231,22 +230,6 @@ public void draw(Canvas canvas, Paint paint, float opacity) { markUpdateSeen(); } } - /** - * sorting stops and stopsColors from array - */ - private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) { - int startStops = value.size() - stopsCount; - for (int i = 0; i < stopsCount; i++) { - stops[i] = (float)value.getDouble(startStops + i); - stopsColors[i] = Color.argb( - (int) (value.getDouble(startColorsPosition + i * 4 + 3) * 255), - (int) (value.getDouble(startColorsPosition + i * 4) * 255), - (int) (value.getDouble(startColorsPosition + i * 4 + 1) * 255), - (int) (value.getDouble(startColorsPosition + i * 4 + 2) * 255)); - - } - } - /** * Sets up paint according to the props set on a shadow view. Returns {@code true} @@ -379,20 +362,14 @@ protected boolean setHitTestStroke(Paint paint) { @Override public void mergeProperties(VirtualNode target, ReadableArray mergeList, boolean inherited) { + mLastMergedList = mergeList; + if (mergeList.size() == 0) { return; } - if (!inherited) { - mOriginProperties = new ArrayList<>(); - mChangedList = new ArrayList<>(); - } - - WritableArray propList = new JavaOnlyArray(); - for (int i = 0; i < mPropList.size(); i++) { - propList.pushString(mPropList.getString(i)); - } - mOwnedPropList = propList; + mOriginProperties = new ArrayList<>(); + resetAttributeList(); for (int i = 0, size = mergeList.size(); i < size; i++) { try { @@ -402,12 +379,12 @@ public void mergeProperties(VirtualNode target, ReadableArray mergeList, boolean if (inherited) { if (!hasOwnProperty(fieldName)) { + mAttributeList.pushString(fieldName); + mOriginProperties.add(field.get(this)); field.set(this, value); - propList.pushString(fieldName); } } else { mOriginProperties.add(field.get(this)); - mChangedList.add(fieldName); field.set(this, value); } } catch (Exception e) { @@ -423,19 +400,27 @@ public void mergeProperties(VirtualNode target, ReadableArray mergeList) { @Override public void resetProperties() { - if (mChangedList != null) { + if (mLastMergedList != null) { try { - for (int i = mChangedList.size() - 1; i >= 0; i--) { - Field field = getClass().getField(mChangedList.get(i)); + for (int i = mLastMergedList.size() - 1; i >= 0; i--) { + Field field = getClass().getField(mLastMergedList.getString(i)); field.set(this, mOriginProperties.get(i)); } } catch (Exception e) { throw new IllegalStateException(e); } - mChangedList = null; + mLastMergedList = null; mOriginProperties = null; } + resetAttributeList(); + } + + private void resetAttributeList() { + mAttributeList = Arguments.createArray(); + for (int i = 0; i < mPropList.size(); i++) { + mAttributeList.pushString(mPropList.getString(i)); + } } // convert propertyName something like fillOpacity to fieldName like mFillOpacity @@ -451,8 +436,8 @@ private String propertyNameToFieldName(String fieldName) { } private boolean hasOwnProperty(String propName) { - for (int i = mOwnedPropList.size() - 1; i >= 0; i--) { - if (mOwnedPropList.getString(i).equals(propName)) { + for (int i = mAttributeList.size() - 1; i >= 0; i--) { + if (mAttributeList.getString(i).equals(propName)) { return true; } } diff --git a/android/src/main/java/com/horcrux/svg/UseShadowNode.java b/android/src/main/java/com/horcrux/svg/UseShadowNode.java index b23137f39..40931d006 100644 --- a/android/src/main/java/com/horcrux/svg/UseShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/UseShadowNode.java @@ -60,7 +60,7 @@ public void draw(Canvas canvas, Paint paint, float opacity) { int count = saveAndSetupCanvas(canvas); clip(canvas, paint); - template.mergeProperties(this, mOwnedPropList); + template.mergeProperties(this, mAttributeList, true); template.draw(canvas, paint, opacity * mOpacity); template.resetProperties(); diff --git a/android/src/main/java/com/horcrux/svg/ViewBoxShadowNode.java b/android/src/main/java/com/horcrux/svg/ViewBoxShadowNode.java index aa0762d2c..deadb22b1 100644 --- a/android/src/main/java/com/horcrux/svg/ViewBoxShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/ViewBoxShadowNode.java @@ -174,7 +174,7 @@ public Matrix getTransform() { } @Override - public void mergeProperties(VirtualNode target, ReadableArray mergeList) { + public void mergeProperties(VirtualNode target, ReadableArray mergeList, boolean inherited) { if (target instanceof UseShadowNode) { mFromSymbol = true; mBoxWidth = ((UseShadowNode)target).getWidth(); diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index b8caee79c..392278611 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -83,10 +83,10 @@ - (void)saveDefinition } -- (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray *)mergeList +- (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray *)mergeList inherited:(BOOL)inherited { [self traverseSubviews:^(RNSVGNode *node) { - [node mergeProperties:target mergeList:mergeList]; + [node mergeProperties:target mergeList:mergeList inherited:inherited]; return YES; }]; } From 16231226d78ab7f1db1d6213a4cea758ae9313a2 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sun, 8 Jan 2017 21:19:25 +0800 Subject: [PATCH 09/10] Fallback to bitmap again! --- .../main/java/com/horcrux/svg/SvgView.java | 22 +++++++- .../java/com/horcrux/svg/SvgViewManager.java | 2 +- .../com/horcrux/svg/SvgViewShadowNode.java | 56 ++++--------------- 3 files changed, 30 insertions(+), 50 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/SvgView.java b/android/src/main/java/com/horcrux/svg/SvgView.java index bbc674347..77308be9d 100644 --- a/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/android/src/main/java/com/horcrux/svg/SvgView.java @@ -31,9 +31,9 @@ import javax.annotation.Nullable; /** - * Custom {@link View} implementation that draws an RNSVGSvg React view and its \children. + * Custom {@link View} implementation that draws an RNSVGSvg React view and its children. */ -public class SvgView extends TextureView { +public class SvgView extends View { public enum Events { EVENT_DATA_URL("onDataURL"); @@ -49,6 +49,7 @@ public String toString() { } } + private @Nullable Bitmap mBitmap; private RCTEventEmitter mEventEmitter; private EventDispatcher mEventDispatcher; private int mTargetTag; @@ -58,11 +59,26 @@ public String toString() { public SvgView(ReactContext reactContext) { super(reactContext); - setOpaque(false); mEventEmitter = reactContext.getJSModule(RCTEventEmitter.class); mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); } + public void setBitmap(Bitmap bitmap) { + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = bitmap; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mBitmap != null) { + canvas.drawBitmap(mBitmap, 0, 0, null); + } + } + private SvgViewShadowNode getShadowNode() { return SvgViewShadowNode.getShadowNodeByTag(getId()); } diff --git a/android/src/main/java/com/horcrux/svg/SvgViewManager.java b/android/src/main/java/com/horcrux/svg/SvgViewManager.java index e8f15ad99..14241d3cf 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewManager.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewManager.java @@ -68,7 +68,7 @@ protected SvgView createViewInstance(ThemedReactContext reactContext) { @Override public void updateExtraData(SvgView root, Object extraData) { - root.setSurfaceTextureListener((SvgViewShadowNode) extraData); + root.setBitmap((Bitmap) extraData); } @Override diff --git a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java index 6f92aed87..3ec062cf0 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java @@ -36,7 +36,7 @@ /** * Shadow node for RNSVG virtual tree root - RNSVGSvgView */ -public class SvgViewShadowNode extends LayoutShadowNode implements TextureView.SurfaceTextureListener { +public class SvgViewShadowNode extends LayoutShadowNode { private static final SparseArray mTagToShadowNode = new SparseArray<>(); private @Nullable Surface mSurface; @@ -64,8 +64,7 @@ public boolean isVirtualAnchor() { @Override public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { super.onCollectExtraUpdates(uiUpdater); - drawOutput(); - uiUpdater.enqueueUpdateExtraData(getReactTag(), this); + uiUpdater.enqueueUpdateExtraData(getReactTag(), drawOutput()); } @Override @@ -74,52 +73,17 @@ public void setReactTag(int reactTag) { mTagToShadowNode.put(getReactTag(), this); } - public void drawOutput() { - if (mSurface == null || !mSurface.isValid()) { - markChildrenUpdatesSeen(this); - return; - } - - try { - Canvas canvas = mSurface.lockCanvas(null); - drawChildren(canvas); - - if (mSurface != null) { - mSurface.unlockCanvasAndPost(canvas); - } - - } catch (IllegalArgumentException | IllegalStateException e) { - FLog.e(ReactConstants.TAG, e.getClass().getSimpleName() + " in Svg.unlockCanvasAndPost"); - } - } - - private void markChildrenUpdatesSeen(ReactShadowNode shadowNode) { - for (int i = 0; i < shadowNode.getChildCount(); i++) { - ReactShadowNode child = shadowNode.getChildAt(i); - child.markUpdateSeen(); - markChildrenUpdatesSeen(child); - } - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - mSurface = new Surface(surface); - drawOutput(); - } + public Object drawOutput() { + Bitmap bitmap = Bitmap.createBitmap( + (int) getLayoutWidth(), + (int) getLayoutHeight(), + Bitmap.Config.ARGB_8888); - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - surface.release(); - mSurface = null; - return true; + Canvas canvas = new Canvas(bitmap); + drawChildren(canvas); + return bitmap; } - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) {} - private void drawChildren(Canvas canvas) { canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); Paint paint = new Paint(); From a325ac66ed6bb9b5149d5a68630ca6c9a64776b7 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Sun, 8 Jan 2017 21:20:42 +0800 Subject: [PATCH 10/10] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42cca0db6..6e1bef3b6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "4.5.0", + "version": "4.6.0", "name": "react-native-svg", "description": "SVG library for react-native", "repository": {