From c2359616bfd3c5afe1c0fb3f4abd72c5543a9967 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Wed, 27 Apr 2016 16:09:38 +0800 Subject: [PATCH] complete elements percentage props support Support Rect, Circle, Line, Ellipse percentage props And support percentage props for gradients --- Example/examples/Text.js | 68 +++++---- README.md | 5 +- .../main/java/com/horcrux/svg/PropHelper.java | 21 +++ .../com/horcrux/svg/RNSVGPathShadowNode.java | 144 ++++++++++-------- .../svg/RNSVGRenderableViewManager.java | 11 +- .../com/horcrux/svg/RNSVGShapeShadowNode.java | 131 ++++++++++++++++ .../com/horcrux/svg/RNSVGTextShadowNode.java | 11 +- .../java/com/horcrux/svg/RNSvgPackage.java | 1 + ios/Brushes/RNSVGRadialGradient.m | 4 +- ios/RNSVGShape.m | 1 - lib/stopsOpacity.js | 2 +- 11 files changed, 291 insertions(+), 108 deletions(-) create mode 100644 android/src/main/java/com/horcrux/svg/RNSVGShapeShadowNode.java diff --git a/Example/examples/Text.js b/Example/examples/Text.js index 485e60ba..cb17ee13 100644 --- a/Example/examples/Text.js +++ b/Example/examples/Text.js @@ -60,6 +60,34 @@ class TextRotate extends Component{ } } +// TODO: iOS not support text stroke with pattern +class TextStroke extends Component{ + static title = 'Stroke the text'; + render() { + return + + + + + + + STROKE TEXT + ; + } +} + class TextFill extends Component{ static title = 'Fill the text with LinearGradient'; render() { @@ -68,8 +96,8 @@ class TextFill extends Component{ width="200" > - - + + @@ -87,34 +115,6 @@ class TextFill extends Component{ } } -// TODO: iOS not support text stroke with pattern -class TextStroke extends Component{ - static title = 'Stroke the text'; - render() { - return - - - - - - - STROKE TEXT - ; - } -} - class TextPath extends Component{ static title = 'Transform the text'; @@ -154,7 +154,13 @@ const icon = ; -const samples = [TextExample, TextRotate, TextStroke, TextFill, TextPath]; +const samples = [ + TextExample, + TextRotate, + TextStroke, + TextFill, + TextPath +]; export { icon, diff --git a/README.md b/README.md index bcd47915..a4b2ae27 100644 --- a/README.md +++ b/README.md @@ -562,9 +562,8 @@ npm install 4. [morph animations](https://github.com/gorangajic/react-svg-morph) 5. fix propTypes 6. more Text features support -7. support percent props -8. Pattern element -9. Image element +7. Pattern element +8. Image element #### Thanks: diff --git a/android/src/main/java/com/horcrux/svg/PropHelper.java b/android/src/main/java/com/horcrux/svg/PropHelper.java index df8b745e..d3405101 100644 --- a/android/src/main/java/com/horcrux/svg/PropHelper.java +++ b/android/src/main/java/com/horcrux/svg/PropHelper.java @@ -9,10 +9,15 @@ package com.horcrux.svg; +import android.util.Log; + import javax.annotation.Nullable; import com.facebook.react.bridge.ReadableArray; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Contains static helper methods for accessing props. */ @@ -51,4 +56,20 @@ import com.facebook.react.bridge.ReadableArray; return value.size(); } + /** + * Converts percentage string into actual based on a relative number + * + * @param percentage percentage string + * @param relative relative number + * @return actual float based on relative number + */ + /*package*/ static float fromPercentageToFloat(String percentage, float relative, float offset, float scale) { + Pattern pattern = Pattern.compile("^(\\-?\\d+(?:\\.\\d+)?)%$"); + Matcher matched = pattern.matcher(percentage); + if (matched.matches()) { + return Float.valueOf(matched.group(1)) / 100 * relative + offset; + } else { + return Float.valueOf(percentage) * scale; + } + } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java b/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java index 697865a9..0bcdfc2b 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java @@ -17,6 +17,7 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.RadialGradient; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Color; @@ -48,9 +49,9 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { private static final int FILL_RULE_EVENODD = 0; private static final int FILL_RULE_NONZERO = 1; - protected @Nullable Path mPath; - private @Nullable float[] mStrokeColor; - private @Nullable float[] mFillColor; + protected Path mPath; + private @Nullable ReadableArray mStrokeColor; + private @Nullable ReadableArray mFillColor; private @Nullable float[] mStrokeDasharray; private float mStrokeWidth = 1; private float mStrokeDashoffset = 0; @@ -60,6 +61,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { private boolean mFillRuleSet; private boolean mPathSet; private float[] mShapePath; + protected RectF mContentBoundingBox; private Point mPaint; @ReactProp(name = "d") @@ -72,7 +74,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { @ReactProp(name = "fill") public void setFill(@Nullable ReadableArray fillColors) { - mFillColor = PropHelper.toFloatArray(fillColors); + mFillColor = fillColors; markUpdated(); } @@ -87,7 +89,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { @ReactProp(name = "stroke") public void setStroke(@Nullable ReadableArray strokeColors) { - mStrokeColor = PropHelper.toFloatArray(strokeColors); + mStrokeColor = strokeColors; markUpdated(); } @@ -139,10 +141,10 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { clip(canvas, paint); - if (setupFillPaint(paint, opacity)) { + if (setupFillPaint(paint, opacity, null)) { canvas.drawPath(mPath, paint); } - if (setupStrokePaint(paint, opacity)) { + if (setupStrokePaint(paint, opacity, null)) { canvas.drawPath(mPath, paint); } @@ -168,21 +170,24 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { } mPath = super.createPath(mShapePath, path); + RectF box = new RectF(); + mPath.computeBounds(box, true); + mContentBoundingBox = box; } } - /* + /** * sorting stops and stopsColors from array */ - private static void parseGradientStops(float[] value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) { - int startStops = value.length - stopsCount; + 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] = value[startStops + i]; + stops[i] = (float)value.getDouble(startStops + i); stopsColors[i] = Color.argb( - (int) (value[startColorsPosition + i * 4 + 3] * 255), - (int) (value[startColorsPosition + i * 4] * 255), - (int) (value[startColorsPosition + i * 4 + 1] * 255), - (int) (value[startColorsPosition + i * 4 + 2] * 255)); + (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)); } } @@ -192,12 +197,12 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { * Sets up {@link #mPaint} 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) { - if (mFillColor != null && mFillColor.length > 0) { + protected boolean setupFillPaint(Paint paint, float opacity, @Nullable RectF box) { + if (mFillColor != null && mFillColor.size() > 0) { paint.reset(); paint.setFlags(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.FILL); - setupPaint(paint, opacity, mFillColor); + setupPaint(paint, opacity, mFillColor, box); return true; } return false; @@ -207,8 +212,8 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { * Sets up {@link #mPaint} 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) { - if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.length == 0) { + protected boolean setupStrokePaint(Paint paint, float opacity, @Nullable RectF box) { + if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.size() == 0) { return false; } paint.reset(); @@ -244,8 +249,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { } paint.setStrokeWidth(mStrokeWidth * mScale); - - setupPaint(paint, opacity, mStrokeColor); + setupPaint(paint, opacity, mStrokeColor, box); if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset)); @@ -255,67 +259,73 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode { } - private void setupPaint(Paint paint, float opacity, float[] colors) { - int stopsCount; - int [] stopsColors; - float [] stops; + 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 || colorType == 2) { + if (box == null) { + box = mContentBoundingBox; + } - int colorType = (int) colors[0]; - switch (colorType) { - case 0: - paint.setARGB( - (int) (colors.length > 4 ? colors[4] * opacity * 255 : opacity * 255), - (int) (colors[1] * 255), - (int) (colors[2] * 255), - (int) (colors[3] * 255)); - break; - case 1: - stopsCount = (colors.length - 5) / 5; - stopsColors = new int [stopsCount]; - stops = new float[stopsCount]; + int startColorsPosition = colorType == 1 ? 5 : 7; - parseGradientStops(colors, stopsCount, stops, stopsColors, 5); + int stopsCount = (colors.size() - startColorsPosition) / 5; + int [] stopsColors = new int [stopsCount]; + float [] stops = new float[stopsCount]; + float height = box.height(); + float width = box.width(); + float midX = box.centerX(); + float midY = box.centerY(); + float offsetX = (midX - width / 2); + float offsetY = (midY - height / 2); + + parseGradientStops(colors, stopsCount, stops, stopsColors, startColorsPosition); + + if (colorType == 1) { + float x1 = PropHelper.fromPercentageToFloat(colors.getString(1), width, offsetX, mScale); + float y1 = PropHelper.fromPercentageToFloat(colors.getString(2), height, offsetY, mScale); + float x2 = PropHelper.fromPercentageToFloat(colors.getString(3), width, offsetX, mScale); + float y2 = PropHelper.fromPercentageToFloat(colors.getString(4), height, offsetY, mScale); paint.setShader( new LinearGradient( - colors[1] * mScale, - colors[2] * mScale, - colors[3] * mScale, - colors[4] * mScale, + x1, + y1, + x2, + y2, stopsColors, stops, Shader.TileMode.CLAMP)); - break; - case 2: - stopsCount = (colors.length - 7) / 5; - stopsColors = new int [stopsCount]; - stops = new float[stopsCount]; - parseGradientStops(colors, stopsCount, stops, stopsColors, 7); - - // TODO: support focus - float focusX = colors[1]; - float focusY = colors[2]; - - float radius = colors[3]; - float radiusRatio = colors[4] / radius; + } else { + float rx = PropHelper.fromPercentageToFloat(colors.getString(3), width, 0f, mScale); + float ry = PropHelper.fromPercentageToFloat(colors.getString(4), height, 0f, mScale); + float cx = PropHelper.fromPercentageToFloat(colors.getString(5), width, offsetX, mScale); + float cy = PropHelper.fromPercentageToFloat(colors.getString(6), height, offsetY, mScale) / (ry / rx); + // TODO: do not support focus point. + float fx = PropHelper.fromPercentageToFloat(colors.getString(1), width, offsetX, mScale); + float fy = PropHelper.fromPercentageToFloat(colors.getString(2), height, offsetY, mScale) / (ry / rx); Shader radialGradient = new RadialGradient( - colors[5] * mScale, - colors[6] * mScale / radiusRatio, - radius * mScale, + cx, + cy, + rx, stopsColors, stops, Shader.TileMode.CLAMP ); Matrix radialMatrix = new Matrix(); - - // seems like a bug here? - radialMatrix.preScale(1f, radiusRatio); + radialMatrix.preScale(1f, ry / rx); radialGradient.setLocalMatrix(radialMatrix); paint.setShader(radialGradient); - break; - default: - // TODO: Support pattern. - FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); + } + } else { + // TODO: Support pattern. + FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); } } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java index cbcdb1ab..3b2f5672 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java @@ -27,6 +27,7 @@ public class RNSVGRenderableViewManager extends ViewManager w / 2) { + rx = w / 2; + } + + if (ry > h / 2) { + ry = h / 2; + } + mPath.addRoundRect(new RectF(x, y, x + w, y + h), rx, ry, Path.Direction.CW); + } else { + mPath.addRect(x, y, x + w, y + h, Path.Direction.CW); + } + break; + } + default: + FLog.e(ReactConstants.TAG, "RNSVG: Invalid Shape type " + type + " at " + mShape); + } + + RectF shapeBox = new RectF(); + mPath.computeBounds(shapeBox, true); + mContentBoundingBox = shapeBox; + super.draw(canvas, paint, opacity); + } + } + + private float getActualProp(String name, float relative) { + if (mShape.hasKey(name)) { + ReadableMap value = mShape.getMap(name); + + if (value.getBoolean("percentage")) { + return (float)value.getDouble("value") * relative * mScale; + } else { + return (float)value.getDouble("value") * mScale; + } + } else { + return 0f; + } + } +} diff --git a/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java b/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java index d7f16218..c7127914 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java @@ -14,8 +14,11 @@ import javax.annotation.Nullable; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Typeface; import android.text.TextUtils; +import android.util.Log; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; @@ -89,7 +92,11 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode { lines[i] = linesProp.getString(i); } String text = TextUtils.join("\n", lines); - if (setupStrokePaint(paint, opacity)) { + + Rect bound = new Rect(); + paint.getTextBounds(text, 0, text.length(), bound); + RectF box = new RectF(bound); + if (setupStrokePaint(paint, opacity, box)) { applyTextPropertiesToPaint(paint); if (mPath == null) { canvas.drawText(text, 0, -paint.ascent(), paint); @@ -97,7 +104,7 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode { canvas.drawTextOnPath(text, mPath, 0, 0, paint); } } - if (setupFillPaint(paint, opacity)) { + if (setupFillPaint(paint, opacity, box)) { applyTextPropertiesToPaint(paint); if (mPath == null) { canvas.drawText(text, 0, -paint.ascent(), paint); diff --git a/android/src/main/java/com/horcrux/svg/RNSvgPackage.java b/android/src/main/java/com/horcrux/svg/RNSvgPackage.java index 340b30e2..0763b9d8 100644 --- a/android/src/main/java/com/horcrux/svg/RNSvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/RNSvgPackage.java @@ -30,6 +30,7 @@ public class RNSvgPackage implements ReactPackage { RNSVGRenderableViewManager.createRNSVGGroupViewManager(), RNSVGRenderableViewManager.createRNSVGPathViewManager(), RNSVGRenderableViewManager.createRNSVGTextViewManager(), + RNSVGRenderableViewManager.createRNSVGShapeViewManager(), new RNSVGSvgViewManager()); } diff --git a/ios/Brushes/RNSVGRadialGradient.m b/ios/Brushes/RNSVGRadialGradient.m index 6ae6f505..bda357a7 100644 --- a/ios/Brushes/RNSVGRadialGradient.m +++ b/ios/Brushes/RNSVGRadialGradient.m @@ -63,9 +63,9 @@ CGFloat rx = [self getActualProp:2 relative:width offset:0]; CGFloat ry = [self getActualProp:3 relative:height offset:0]; CGFloat fx = [self getActualProp:0 relative:width offset:offsetX]; - CGFloat fy = [self getActualProp:1 relative:height offset:offsetY] / (ry / rx); // fx == fy + CGFloat fy = [self getActualProp:1 relative:height offset:offsetY] / (ry / rx); CGFloat cx = [self getActualProp:4 relative:width offset:offsetX]; - CGFloat cy = [self getActualProp:5 relative:height offset:offsetY] / (ry / rx); // rx == ry + CGFloat cy = [self getActualProp:5 relative:height offset:offsetY] / (ry / rx); CGAffineTransform transform = CGAffineTransformMakeScale(1, ry / rx); CGContextConcatCTM(context, transform); diff --git a/ios/RNSVGShape.m b/ios/RNSVGShape.m index acd595a8..2b30bff5 100644 --- a/ios/RNSVGShape.m +++ b/ios/RNSVGShape.m @@ -58,7 +58,6 @@ case 3: { // draw rect - CGPathMoveToPoint(path, NULL, 0, 0); CGFloat x = [self getActualProp:@"x" relative:width]; CGFloat y = [self getActualProp:@"y" relative:height]; CGFloat w = [self getActualProp:@"width" relative:width]; diff --git a/lib/stopsOpacity.js b/lib/stopsOpacity.js index ebf9bcd0..11d9a862 100644 --- a/lib/stopsOpacity.js +++ b/lib/stopsOpacity.js @@ -1,7 +1,7 @@ import _ from 'lodash'; export default function (stops, opacity) { return _.reduce(stops, (ret, color, key) => { - ret[key] = color.alpha(opacity).rgbaString(); + ret[key] = color.alpha(color.alpha() * opacity).rgbaString(); return ret; }, {}); }