From 870c0c37e7fee838d4efe8665b684d104f1f31b8 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Wed, 20 Apr 2016 19:02:26 +0800 Subject: [PATCH] add Gradients --- Example/examples/Gradients.js | 58 +- .../main/java/com/horcrux/svg/PropHelper.java | 1 + .../com/horcrux/svg/RNSVGGroupShadowNode.java | 37 +- .../com/horcrux/svg/RNSVGPathShadowNode.java | 508 +++++++++++------- .../svg/RNSVGRenderableViewManager.java | 111 ++-- .../java/com/horcrux/svg/RNSVGSvgView.java | 34 +- .../com/horcrux/svg/RNSVGSvgViewManager.java | 56 +- .../horcrux/svg/RNSVGSvgViewShadowNode.java | 62 +-- .../com/horcrux/svg/RNSVGTextShadowNode.java | 204 +++---- .../com/horcrux/svg/RNSVGVirtualNode.java | 149 ++--- 10 files changed, 660 insertions(+), 560 deletions(-) diff --git a/Example/examples/Gradients.js b/Example/examples/Gradients.js index fb723b2f..34913927 100644 --- a/Example/examples/Gradients.js +++ b/Example/examples/Gradients.js @@ -82,6 +82,11 @@ class RadialGradientExample extends Component{ stopColor="#ff0" stopOpacity="1" /> + - - - - - - - - ; - } -} - - class RadialGradientPart extends Component{ static title = 'Define another ellipse with a radial gradient from white to blue'; render() { @@ -174,6 +152,32 @@ class RadialGradientPart extends Component{ } +class FillGradientWithOpacity extends Component{ + static title = 'Fill a radial gradient with fillOpacity prop'; + render() { + return + + + + + + + + ; + } +} + const icon = MIN_OPACITY_FOR_DRAW) { - saveAndSetupCanvas(canvas); - // TODO(6352006): apply clipping (iOS doesn't do it yet, it seems to cause issues) - for (int i = 0; i < getChildCount(); i++) { - RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i); - child.draw(canvas, paint, opacity); - child.markUpdateSeen(); - } - - restoreCanvas(canvas); + @Override + public boolean isVirtual() { + return true; + } + + public void draw(Canvas canvas, Paint paint, float opacity) { + opacity *= mOpacity; + if (opacity > MIN_OPACITY_FOR_DRAW) { + saveAndSetupCanvas(canvas); + // TODO(6352006): apply clipping (iOS doesn't do it yet, it seems to cause issues) + for (int i = 0; i < getChildCount(); i++) { + RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i); + child.draw(canvas, paint, opacity); + child.markUpdateSeen(); + } + + restoreCanvas(canvas); + } } - } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java b/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java index 80cf6645..e3fdfe6e 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGPathShadowNode.java @@ -11,241 +11,333 @@ package com.horcrux.svg; import javax.annotation.Nullable; +import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Point; +import android.graphics.RadialGradient; import android.graphics.RectF; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Shader; +import android.graphics.Matrix; +import android.util.Log; + 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.annotations.ReactProp; +import java.util.Arrays; + /** * Shadow node for virtual RNSVGPath view */ public class RNSVGPathShadowNode extends RNSVGVirtualNode { - 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 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 JOIN_BEVEL = 2; + private static final int JOIN_MITER = 0; + private static final int JOIN_ROUND = 1; - private static final int PATH_TYPE_ARC = 4; - private static final int PATH_TYPE_CLOSE = 1; - private static final int PATH_TYPE_CURVETO = 3; - private static final int PATH_TYPE_LINETO = 2; - private static final int PATH_TYPE_MOVETO = 0; + private static final int PATH_TYPE_ARC = 4; + private static final int PATH_TYPE_CLOSE = 1; + private static final int PATH_TYPE_CURVETO = 3; + private static final int PATH_TYPE_LINETO = 2; + private static final int PATH_TYPE_MOVETO = 0; - protected @Nullable Path mPath; - private @Nullable float[] mStrokeColor; - private @Nullable float[] mFillColor; - private @Nullable float[] mStrokeDash; - private float mStrokeWidth = 1; - private int mStrokeCap = CAP_ROUND; - private int mStrokeJoin = JOIN_ROUND; + protected @Nullable Path mPath; + private @Nullable float[] mStrokeColor; + private @Nullable float[] mFillColor; + private @Nullable float[] mStrokeDash; + private float mStrokeWidth = 1; + private int mStrokeLinecap = CAP_ROUND; + private int mStrokeLinejoin = JOIN_ROUND; - @ReactProp(name = "d") - public void setPath(@Nullable ReadableArray shapePath) { - float[] pathData = PropHelper.toFloatArray(shapePath); - mPath = createPath(pathData); - markUpdated(); - } + private Point mPaint; - @ReactProp(name = "stroke") - public void setStroke(@Nullable ReadableArray strokeColors) { - mStrokeColor = PropHelper.toFloatArray(strokeColors); - markUpdated(); - } - - @ReactProp(name = "strokeDash") - public void setStrokeDash(@Nullable ReadableArray strokeDash) { - mStrokeDash = PropHelper.toFloatArray(strokeDash); - markUpdated(); - } - - @ReactProp(name = "fill") - public void setFill(@Nullable ReadableArray fillColors) { - mFillColor = PropHelper.toFloatArray(fillColors); - markUpdated(); - } - - @ReactProp(name = "strokeWidth", defaultFloat = 1f) - public void setStrokeWidth(float strokeWidth) { - mStrokeWidth = strokeWidth; - markUpdated(); - } - - @ReactProp(name = "strokeCap", defaultInt = CAP_ROUND) - public void setStrokeCap(int strokeCap) { - mStrokeCap = strokeCap; - markUpdated(); - } - - @ReactProp(name = "strokeJoin", defaultInt = JOIN_ROUND) - public void setStrokeJoin(int strokeJoin) { - mStrokeJoin = strokeJoin; - markUpdated(); - } - - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - opacity *= mOpacity; - if (opacity > MIN_OPACITY_FOR_DRAW) { - saveAndSetupCanvas(canvas); - if (mPath == null) { - throw new JSApplicationIllegalArgumentException( - "Paths should have a valid path (d) prop"); - } - if (setupStrokePaint(paint, opacity)) { - canvas.drawPath(mPath, paint); - } - if (setupFillPaint(paint, opacity)) { - canvas.drawPath(mPath, paint); - } - restoreCanvas(canvas); + @ReactProp(name = "d") + public void setPath(@Nullable ReadableArray shapePath) { + float[] pathData = PropHelper.toFloatArray(shapePath); + mPath = createPath(pathData); + markUpdated(); } - markUpdateSeen(); - } - /** - * 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) { - return false; + @ReactProp(name = "stroke") + public void setStroke(@Nullable ReadableArray strokeColors) { + mStrokeColor = PropHelper.toFloatArray(strokeColors); + markUpdated(); } - paint.reset(); - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - switch (mStrokeCap) { - case CAP_BUTT: - paint.setStrokeCap(Paint.Cap.BUTT); - break; - case CAP_SQUARE: - paint.setStrokeCap(Paint.Cap.SQUARE); - break; - case CAP_ROUND: - paint.setStrokeCap(Paint.Cap.ROUND); - break; - default: - throw new JSApplicationIllegalArgumentException( - "strokeCap " + mStrokeCap + " unrecognized"); - } - switch (mStrokeJoin) { - case JOIN_MITER: - paint.setStrokeJoin(Paint.Join.MITER); - break; - case JOIN_BEVEL: - paint.setStrokeJoin(Paint.Join.BEVEL); - break; - case JOIN_ROUND: - paint.setStrokeJoin(Paint.Join.ROUND); - break; - default: - throw new JSApplicationIllegalArgumentException( - "strokeJoin " + mStrokeJoin + " unrecognized"); - } - paint.setStrokeWidth(mStrokeWidth * mScale); - paint.setARGB( - (int) (mStrokeColor.length > 3 ? mStrokeColor[3] * opacity * 255 : opacity * 255), - (int) (mStrokeColor[0] * 255), - (int) (mStrokeColor[1] * 255), - (int) (mStrokeColor[2] * 255)); - if (mStrokeDash != null && mStrokeDash.length > 0) { - // TODO(6352067): Support dashes - FLog.w(ReactConstants.TAG, "RNSVG: Dashes are not supported yet!"); - } - return true; - } - /** - * 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) { - paint.reset(); - paint.setFlags(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.FILL); - int colorType = (int) mFillColor[0]; - switch (colorType) { - case 0: - paint.setARGB( - (int) (mFillColor.length > 4 ? mFillColor[4] * opacity * 255 : opacity * 255), - (int) (mFillColor[1] * 255), - (int) (mFillColor[2] * 255), - (int) (mFillColor[3] * 255)); - break; - default: - // TODO(6352048): Support gradients etc. - FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); - } - return true; + @ReactProp(name = "strokeDash") + public void setStrokeDash(@Nullable ReadableArray strokeDash) { + mStrokeDash = PropHelper.toFloatArray(strokeDash); + markUpdated(); } - return false; - } - /** - * Creates a {@link Path} from an array of instructions constructed by JS - * (see RNSVGSerializablePath.js). Each instruction starts with a type (see PATH_TYPE_*) followed - * by arguments for that instruction. For example, to create a line the instruction will be - * 2 (PATH_LINE_TO), x, y. This will draw a line from the last draw point (or 0,0) to x,y. - * - * @param data the array of instructions - * @return the {@link Path} that can be drawn to a canvas - */ - private Path createPath(float[] data) { - Path path = new Path(); - path.moveTo(0, 0); - int i = 0; - while (i < data.length) { - int type = (int) data[i++]; - switch (type) { - case PATH_TYPE_MOVETO: - path.moveTo(data[i++] * mScale, data[i++] * mScale); - break; - case PATH_TYPE_CLOSE: - path.close(); - break; - case PATH_TYPE_LINETO: - path.lineTo(data[i++] * mScale, data[i++] * mScale); - break; - case PATH_TYPE_CURVETO: - path.cubicTo( - data[i++] * mScale, - data[i++] * mScale, - data[i++] * mScale, - data[i++] * mScale, - data[i++] * mScale, - data[i++] * mScale); - break; - case PATH_TYPE_ARC: - { - float x = data[i++] * mScale; - float y = data[i++] * mScale; - float r = data[i++] * mScale; - float start = (float) Math.toDegrees(data[i++]); - float end = (float) Math.toDegrees(data[i++]); - boolean clockwise = data[i++] == 0f; - if (!clockwise) { - end = 360 - end; - } - float sweep = start - end; - RectF oval = new RectF(x - r, y - r, x + r, y + r); - path.addArc(oval, start, sweep); - break; + @ReactProp(name = "fill") + public void setFill(@Nullable ReadableArray fillColors) { + mFillColor = PropHelper.toFloatArray(fillColors); + markUpdated(); + } + + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + markUpdated(); + } + + @ReactProp(name = "strokeLinecap", defaultInt = CAP_ROUND) + public void setStrokeLinecap(int strokeLinecap) { + mStrokeLinecap = strokeLinecap; + markUpdated(); + } + + @ReactProp(name = "strokeLinejoin", defaultInt = JOIN_ROUND) + public void setStrokeLinejoin(int strokeLinejoin) { + mStrokeLinejoin = strokeLinejoin; + markUpdated(); + } + + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + opacity *= mOpacity; + if (opacity > MIN_OPACITY_FOR_DRAW) { + saveAndSetupCanvas(canvas); + if (mPath == null) { + throw new JSApplicationIllegalArgumentException( + "Paths should have a valid path (d) prop"); + } + if (setupStrokePaint(paint, opacity)) { + canvas.drawPath(mPath, paint); + } + if (setupFillPaint(paint, opacity)) { + canvas.drawPath(mPath, paint); + } + restoreCanvas(canvas); } - default: - throw new JSApplicationIllegalArgumentException( - "Unrecognized drawing instruction " + type); - } + markUpdateSeen(); + } + + /** + * 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) { + return false; + } + paint.reset(); + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.STROKE); + switch (mStrokeLinecap) { + case CAP_BUTT: + paint.setStrokeCap(Paint.Cap.BUTT); + break; + case CAP_SQUARE: + paint.setStrokeCap(Paint.Cap.SQUARE); + break; + case CAP_ROUND: + paint.setStrokeCap(Paint.Cap.ROUND); + break; + default: + throw new JSApplicationIllegalArgumentException( + "strokeLinecap " + mStrokeLinecap + " unrecognized"); + } + switch (mStrokeLinejoin) { + case JOIN_MITER: + paint.setStrokeJoin(Paint.Join.MITER); + break; + case JOIN_BEVEL: + paint.setStrokeJoin(Paint.Join.BEVEL); + break; + case JOIN_ROUND: + paint.setStrokeJoin(Paint.Join.ROUND); + break; + default: + throw new JSApplicationIllegalArgumentException( + "strokeLinejoin " + mStrokeLinejoin + " unrecognized"); + } + paint.setStrokeWidth(mStrokeWidth * mScale); + paint.setARGB( + (int) (mStrokeColor.length > 3 ? mStrokeColor[3] * opacity * 255 : opacity * 255), + (int) (mStrokeColor[0] * 255), + (int) (mStrokeColor[1] * 255), + (int) (mStrokeColor[2] * 255)); + if (mStrokeDash != null && mStrokeDash.length > 0) { + // TODO(6352067): Support dashes + FLog.w(ReactConstants.TAG, "RNSVG: Dashes are not supported yet!"); + } + return true; + } + + /* + * 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; + int offset = 0; + for (int i = 0; i < stopsCount; i++) { + int index; + + if (i % 2 == 0) { + index = i / 2; + } else { + index = stopsCount + offset - i; + offset++; + } + + stops[i] = value[startStops + index]; + stopsColors[i] = Color.argb( + (int) (value[startColorsPosition + 3 + index * 4] * 255), + (int) (value[startColorsPosition + index * 4] * 255), + (int) (value[startColorsPosition + 1 + index * 4] * 255), + (int) (value[startColorsPosition + 2 + index * 4] * 255)); + + } + } + + + /** + * 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) { + int stopsCount; + int [] stopsColors; + float [] stops; + if (mFillColor != null && mFillColor.length > 0) { + paint.reset(); + paint.setFlags(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + int colorType = (int) mFillColor[0]; + switch (colorType) { + case 0: + paint.setARGB( + (int) (mFillColor.length > 4 ? mFillColor[4] * opacity * 255 : opacity * 255), + (int) (mFillColor[1] * 255), + (int) (mFillColor[2] * 255), + (int) (mFillColor[3] * 255)); + break; + case 1: + stopsCount = (mFillColor.length - 5) / 5; + stopsColors = new int [stopsCount]; + stops = new float[stopsCount]; + + parseGradientStops(mFillColor, stopsCount, stops, stopsColors, 5); + paint.setShader( + new LinearGradient( + mFillColor[1] * mScale, + mFillColor[2] * mScale, + mFillColor[3] * mScale, + mFillColor[4] * mScale, + stopsColors, + stops, + Shader.TileMode.CLAMP)); + break; + case 2: + stopsCount = (mFillColor.length - 7) / 5; + stopsColors = new int [stopsCount]; + stops = new float[stopsCount]; + parseGradientStops(mFillColor, stopsCount, stops, stopsColors, 7); + + float radius = mFillColor[3]; + float radiusRatio = mFillColor[4] / radius; + Shader radialGradient = new RadialGradient( + mFillColor[5] * mScale, + mFillColor[6] * mScale / radiusRatio, + radius * mScale, + stopsColors, + stops, + Shader.TileMode.CLAMP + ); + + Matrix radialMatrix = new Matrix(); + float [] rawMatrix = new float[9]; + rawMatrix[0] = 1; + rawMatrix[1] = 0; + rawMatrix[2] = 0; + rawMatrix[3] = 0; + rawMatrix[4] = radiusRatio; + rawMatrix[5] = 0; + rawMatrix[6] = 0; + rawMatrix[7] = 0; + rawMatrix[8] = 1; + + radialMatrix.setValues(rawMatrix); + radialGradient.setLocalMatrix(radialMatrix); + paint.setShader(radialGradient); + break; + default: + // TODO: Support pattern. + FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); + } + return true; + } + return false; + } + + /** + * Creates a {@link Path} from an array of instructions constructed by JS + * (see RNSVGSerializablePath.js). Each instruction starts with a type (see PATH_TYPE_*) followed + * by arguments for that instruction. For example, to create a line the instruction will be + * 2 (PATH_LINE_TO), x, y. This will draw a line from the last draw point (or 0,0) to x,y. + * + * @param data the array of instructions + * @return the {@link Path} that can be drawn to a canvas + */ + private Path createPath(float[] data) { + Path path = new Path(); + path.moveTo(0, 0); + int i = 0; + while (i < data.length) { + int type = (int) data[i++]; + switch (type) { + case PATH_TYPE_MOVETO: + path.moveTo(data[i++] * mScale, data[i++] * mScale); + break; + case PATH_TYPE_CLOSE: + path.close(); + break; + case PATH_TYPE_LINETO: + path.lineTo(data[i++] * mScale, data[i++] * mScale); + break; + case PATH_TYPE_CURVETO: + path.cubicTo( + data[i++] * mScale, + data[i++] * mScale, + data[i++] * mScale, + data[i++] * mScale, + data[i++] * mScale, + data[i++] * mScale); + break; + case PATH_TYPE_ARC: + { + float x = data[i++] * mScale; + float y = data[i++] * mScale; + float r = data[i++] * mScale; + float start = (float) Math.toDegrees(data[i++]); + float end = (float) Math.toDegrees(data[i++]); + boolean clockwise = data[i++] == 0f; + if (!clockwise) { + end = 360 - end; + } + float sweep = start - end; + RectF oval = new RectF(x - r, y - r, x + r, y + r); + path.addArc(oval, start, sweep); + break; + } + default: + throw new JSApplicationIllegalArgumentException( + "Unrecognized drawing instruction " + type); + } + } + return path; } - return path; - } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java index 16e254e6..ff8fe5c2 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGRenderableViewManager.java @@ -9,6 +9,7 @@ package com.horcrux.svg; +import android.content.Context; import android.view.View; //import com.facebook.react.uimanager.ReactStylesDiffMap; @@ -23,66 +24,66 @@ import com.facebook.react.uimanager.ViewManager; */ public class RNSVGRenderableViewManager extends ViewManager { - /* package */ static final String CLASS_GROUP = "RNSVGGroup"; - /* package */ static final String CLASS_SVG = "RNSVGPath"; - /* package */ static final String CLASS_TEXT = "RNSVGText"; + /* package */ static final String CLASS_GROUP = "RNSVGGroup"; + /* package */ static final String CLASS_SVG = "RNSVGPath"; + /* package */ static final String CLASS_TEXT = "RNSVGText"; - private final String mClassName; + private final String mClassName; - public static RNSVGRenderableViewManager createRNSVGGroupViewManager() { - return new RNSVGRenderableViewManager(CLASS_GROUP); - } - - public static RNSVGRenderableViewManager createRNSVGPathViewManager() { - return new RNSVGRenderableViewManager(CLASS_SVG); - } - - public static RNSVGRenderableViewManager createRNSVGTextViewManager() { - return new RNSVGRenderableViewManager(CLASS_TEXT); - } - - private RNSVGRenderableViewManager(String className) { - mClassName = className; - } - - @Override - public String getName() { - return mClassName; - } - - @Override - public ReactShadowNode createShadowNodeInstance() { - if (mClassName == CLASS_GROUP) { - return new RNSVGGroupShadowNode(); - } else if (mClassName == CLASS_SVG) { - return new RNSVGPathShadowNode(); - } else if (mClassName == CLASS_TEXT) { - return new RNSVGTextShadowNode(); - } else { - throw new IllegalStateException("Unexpected type " + mClassName); + public static RNSVGRenderableViewManager createRNSVGGroupViewManager() { + return new RNSVGRenderableViewManager(CLASS_GROUP); } - } - @Override - public Class getShadowNodeClass() { - if (mClassName == CLASS_GROUP) { - return RNSVGGroupShadowNode.class; - } else if (mClassName == CLASS_SVG) { - return RNSVGPathShadowNode.class; - } else if (mClassName == CLASS_TEXT) { - return RNSVGTextShadowNode.class; - } else { - throw new IllegalStateException("Unexpected type " + mClassName); + public static RNSVGRenderableViewManager createRNSVGPathViewManager() { + return new RNSVGRenderableViewManager(CLASS_SVG); } - } - @Override - protected View createViewInstance(ThemedReactContext reactContext) { - throw new IllegalStateException("RNSVGPath does not map into a native view"); - } + public static RNSVGRenderableViewManager createRNSVGTextViewManager() { + return new RNSVGRenderableViewManager(CLASS_TEXT); + } - @Override - public void updateExtraData(View root, Object extraData) { - throw new IllegalStateException("RNSVGPath does not map into a native view"); - } + private RNSVGRenderableViewManager(String className) { + mClassName = className; + } + + @Override + public String getName() { + return mClassName; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + if (mClassName == CLASS_GROUP) { + return new RNSVGGroupShadowNode(); + } else if (mClassName == CLASS_SVG) { + return new RNSVGPathShadowNode(); + } else if (mClassName == CLASS_TEXT) { + return new RNSVGTextShadowNode(); + } else { + throw new IllegalStateException("Unexpected type " + mClassName); + } + } + + @Override + public Class getShadowNodeClass() { + if (mClassName == CLASS_GROUP) { + return RNSVGGroupShadowNode.class; + } else if (mClassName == CLASS_SVG) { + return RNSVGPathShadowNode.class; + } else if (mClassName == CLASS_TEXT) { + return RNSVGTextShadowNode.class; + } else { + throw new IllegalStateException("Unexpected type " + mClassName); + } + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + throw new IllegalStateException("RNSVGPath does not map into a native view"); + } + + @Override + public void updateExtraData(View root, Object extraData) { + throw new IllegalStateException("RNSVGPath does not map into a native view"); + } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGSvgView.java b/android/src/main/java/com/horcrux/svg/RNSVGSvgView.java index 383d6a13..1ef0133c 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGSvgView.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGSvgView.java @@ -21,25 +21,25 @@ import android.view.View; */ public class RNSVGSvgView extends View { - private @Nullable Bitmap mBitmap; + private @Nullable Bitmap mBitmap; - public RNSVGSvgView(Context context) { - super(context); - } - - public void setBitmap(Bitmap bitmap) { - if (mBitmap != null) { - mBitmap.recycle(); + public RNSVGSvgView(Context context) { + super(context); } - mBitmap = bitmap; - invalidate(); - } - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mBitmap != null) { - canvas.drawBitmap(mBitmap, 0, 0, null); + 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); + } } - } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGSvgViewManager.java b/android/src/main/java/com/horcrux/svg/RNSVGSvgViewManager.java index c6577d9f..28622806 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGSvgViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGSvgViewManager.java @@ -23,39 +23,39 @@ import com.facebook.react.uimanager.ThemedReactContext; public class RNSVGSvgViewManager extends BaseViewManager { - private static final String REACT_CLASS = "RNSVGSvgView"; + private static final String REACT_CLASS = "RNSVGSvgView"; + + private static final CSSNode.MeasureFunction MEASURE_FUNCTION = new CSSNode.MeasureFunction() { + @Override + public void measure(CSSNode node, float width, float height, MeasureOutput measureOutput) { + throw new IllegalStateException("SvgView should have explicit width and height set"); + } + }; - private static final CSSNode.MeasureFunction MEASURE_FUNCTION = new CSSNode.MeasureFunction() { @Override - public void measure(CSSNode node, float width, float height, MeasureOutput measureOutput) { - throw new IllegalStateException("SvgView should have explicit width and height set"); + public String getName() { + return REACT_CLASS; } - }; - @Override - public String getName() { - return REACT_CLASS; - } + @Override + public RNSVGSvgViewShadowNode createShadowNodeInstance() { + RNSVGSvgViewShadowNode node = new RNSVGSvgViewShadowNode(); + node.setMeasureFunction(MEASURE_FUNCTION); + return node; + } - @Override - public RNSVGSvgViewShadowNode createShadowNodeInstance() { - RNSVGSvgViewShadowNode node = new RNSVGSvgViewShadowNode(); - node.setMeasureFunction(MEASURE_FUNCTION); - return node; - } + @Override + public Class getShadowNodeClass() { + return RNSVGSvgViewShadowNode.class; + } - @Override - public Class getShadowNodeClass() { - return RNSVGSvgViewShadowNode.class; - } + @Override + protected RNSVGSvgView createViewInstance(ThemedReactContext reactContext) { + return new RNSVGSvgView(reactContext); + } - @Override - protected RNSVGSvgView createViewInstance(ThemedReactContext reactContext) { - return new RNSVGSvgView(reactContext); - } - - @Override - public void updateExtraData(RNSVGSvgView root, Object extraData) { - root.setBitmap((Bitmap) extraData); - } + @Override + public void updateExtraData(RNSVGSvgView root, Object extraData) { + root.setBitmap((Bitmap) extraData); + } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGSvgViewShadowNode.java b/android/src/main/java/com/horcrux/svg/RNSVGSvgViewShadowNode.java index e4171821..e7c55de2 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGSvgViewShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGSvgViewShadowNode.java @@ -21,36 +21,36 @@ import com.facebook.react.uimanager.UIViewOperationQueue; */ public class RNSVGSvgViewShadowNode extends LayoutShadowNode { - @Override - public boolean isVirtual() { - return false; - } - - @Override - public boolean isVirtualAnchor() { - return true; - } - - @Override - public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { - super.onCollectExtraUpdates(uiUpdater); - uiUpdater.enqueueUpdateExtraData(getReactTag(), drawOutput()); - } - - private Object drawOutput() { - // TODO(7255985): Use TextureView and pass Svg from the view to draw on it asynchronously - // instead of passing the bitmap (which is inefficient especially in terms of memory usage) - Bitmap bitmap = Bitmap.createBitmap( - (int) getLayoutWidth(), - (int) getLayoutHeight(), - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - for (int i = 0; i < getChildCount(); i++) { - RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i); - child.draw(canvas, paint, 1f); - child.markUpdateSeen(); + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isVirtualAnchor() { + return true; + } + + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { + super.onCollectExtraUpdates(uiUpdater); + uiUpdater.enqueueUpdateExtraData(getReactTag(), drawOutput()); + } + + private Object drawOutput() { + // TODO(7255985): Use TextureView and pass Svg from the view to draw on it asynchronously + // instead of passing the bitmap (which is inefficient especially in terms of memory usage) + Bitmap bitmap = Bitmap.createBitmap( + (int) getLayoutWidth(), + (int) getLayoutHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + for (int i = 0; i < getChildCount(); i++) { + RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i); + child.draw(canvas, paint, 1f); + child.markUpdateSeen(); + } + return bitmap; } - return bitmap; - } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java b/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java index 7de5c1e7..e0e96ca6 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGTextShadowNode.java @@ -25,117 +25,117 @@ import com.facebook.react.uimanager.annotations.ReactProp; */ public class RNSVGTextShadowNode extends RNSVGPathShadowNode { - private static final String PROP_LINES = "lines"; + private static final String PROP_LINES = "lines"; - private static final String PROP_FONT = "font"; - private static final String PROP_FONT_FAMILY = "fontFamily"; - private static final String PROP_FONT_SIZE = "fontSize"; - private static final String PROP_FONT_STYLE = "fontStyle"; - private static final String PROP_FONT_WEIGHT = "fontWeight"; + private static final String PROP_FONT = "font"; + private static final String PROP_FONT_FAMILY = "fontFamily"; + private static final String PROP_FONT_SIZE = "fontSize"; + private static final String PROP_FONT_STYLE = "fontStyle"; + private static final String PROP_FONT_WEIGHT = "fontWeight"; - private static final int DEFAULT_FONT_SIZE = 12; + private static final int DEFAULT_FONT_SIZE = 12; - private static final int TEXT_ALIGNMENT_CENTER = 2; - private static final int TEXT_ALIGNMENT_LEFT = 0; - private static final int TEXT_ALIGNMENT_RIGHT = 1; + private static final int TEXT_ALIGNMENT_CENTER = 2; + private static final int TEXT_ALIGNMENT_LEFT = 0; + private static final int TEXT_ALIGNMENT_RIGHT = 1; - private @Nullable ReadableMap mFrame; - private int mTextAlignment = TEXT_ALIGNMENT_LEFT; + private @Nullable ReadableMap mFrame; + private int mTextAlignment = TEXT_ALIGNMENT_LEFT; - @ReactProp(name = "frame") - public void setFrame(@Nullable ReadableMap frame) { - mFrame = frame; - } - - @ReactProp(name = "alignment", defaultInt = TEXT_ALIGNMENT_LEFT) - public void setAlignment(int alignment) { - mTextAlignment = alignment; - } - - @Override - public void draw(Canvas canvas, Paint paint, float opacity) { - if (mFrame == null) { - return; - } - opacity *= mOpacity; - if (opacity <= MIN_OPACITY_FOR_DRAW) { - return; - } - if (!mFrame.hasKey(PROP_LINES)) { - return; - } - ReadableArray linesProp = mFrame.getArray(PROP_LINES); - if (linesProp == null || linesProp.size() == 0) { - return; + @ReactProp(name = "frame") + public void setFrame(@Nullable ReadableMap frame) { + mFrame = frame; } - // only set up the canvas if we have something to draw - saveAndSetupCanvas(canvas); - String[] lines = new String[linesProp.size()]; - for (int i = 0; i < lines.length; i++) { - lines[i] = linesProp.getString(i); + @ReactProp(name = "alignment", defaultInt = TEXT_ALIGNMENT_LEFT) + public void setAlignment(int alignment) { + mTextAlignment = alignment; } - String text = TextUtils.join("\n", lines); - if (setupStrokePaint(paint, opacity)) { - applyTextPropertiesToPaint(paint); - if (mPath == null) { - canvas.drawText(text, 0, -paint.ascent(), paint); - } else { - canvas.drawTextOnPath(text, mPath, 0, 0, paint); - } - } - if (setupFillPaint(paint, opacity)) { - applyTextPropertiesToPaint(paint); - if (mPath == null) { - canvas.drawText(text, 0, -paint.ascent(), paint); - } else { - canvas.drawTextOnPath(text, mPath, 0, 0, paint); - } - } - restoreCanvas(canvas); - markUpdateSeen(); - } - private void applyTextPropertiesToPaint(Paint paint) { - int alignment = mTextAlignment; - switch (alignment) { - case TEXT_ALIGNMENT_LEFT: - paint.setTextAlign(Paint.Align.LEFT); - break; - case TEXT_ALIGNMENT_RIGHT: - paint.setTextAlign(Paint.Align.RIGHT); - break; - case TEXT_ALIGNMENT_CENTER: - paint.setTextAlign(Paint.Align.CENTER); - break; - } - if (mFrame != null) { - if (mFrame.hasKey(PROP_FONT)) { - ReadableMap font = mFrame.getMap(PROP_FONT); - if (font != null) { - float fontSize = DEFAULT_FONT_SIZE; - if (font.hasKey(PROP_FONT_SIZE)) { - fontSize = (float) font.getDouble(PROP_FONT_SIZE); - } - paint.setTextSize(fontSize * mScale); - boolean isBold = - font.hasKey(PROP_FONT_WEIGHT) && "bold".equals(font.getString(PROP_FONT_WEIGHT)); - boolean isItalic = - font.hasKey(PROP_FONT_STYLE) && "italic".equals(font.getString(PROP_FONT_STYLE)); - int fontStyle; - if (isBold && isItalic) { - fontStyle = Typeface.BOLD_ITALIC; - } else if (isBold) { - fontStyle = Typeface.BOLD; - } else if (isItalic) { - fontStyle = Typeface.ITALIC; - } else { - fontStyle = Typeface.NORMAL; - } - // NB: if the font family is null / unsupported, the default one will be used - paint.setTypeface(Typeface.create(font.getString(PROP_FONT_FAMILY), fontStyle)); + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + if (mFrame == null) { + return; + } + opacity *= mOpacity; + if (opacity <= MIN_OPACITY_FOR_DRAW) { + return; + } + if (!mFrame.hasKey(PROP_LINES)) { + return; + } + ReadableArray linesProp = mFrame.getArray(PROP_LINES); + if (linesProp == null || linesProp.size() == 0) { + return; + } + + // only set up the canvas if we have something to draw + saveAndSetupCanvas(canvas); + String[] lines = new String[linesProp.size()]; + for (int i = 0; i < lines.length; i++) { + lines[i] = linesProp.getString(i); + } + String text = TextUtils.join("\n", lines); + if (setupStrokePaint(paint, opacity)) { + applyTextPropertiesToPaint(paint); + if (mPath == null) { + canvas.drawText(text, 0, -paint.ascent(), paint); + } else { + canvas.drawTextOnPath(text, mPath, 0, 0, paint); + } + } + if (setupFillPaint(paint, opacity)) { + applyTextPropertiesToPaint(paint); + if (mPath == null) { + canvas.drawText(text, 0, -paint.ascent(), paint); + } else { + canvas.drawTextOnPath(text, mPath, 0, 0, paint); + } + } + restoreCanvas(canvas); + markUpdateSeen(); + } + + private void applyTextPropertiesToPaint(Paint paint) { + int alignment = mTextAlignment; + switch (alignment) { + case TEXT_ALIGNMENT_LEFT: + paint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGNMENT_RIGHT: + paint.setTextAlign(Paint.Align.RIGHT); + break; + case TEXT_ALIGNMENT_CENTER: + paint.setTextAlign(Paint.Align.CENTER); + break; + } + if (mFrame != null) { + if (mFrame.hasKey(PROP_FONT)) { + ReadableMap font = mFrame.getMap(PROP_FONT); + if (font != null) { + float fontSize = DEFAULT_FONT_SIZE; + if (font.hasKey(PROP_FONT_SIZE)) { + fontSize = (float) font.getDouble(PROP_FONT_SIZE); + } + paint.setTextSize(fontSize * mScale); + boolean isBold = + font.hasKey(PROP_FONT_WEIGHT) && "bold".equals(font.getString(PROP_FONT_WEIGHT)); + boolean isItalic = + font.hasKey(PROP_FONT_STYLE) && "italic".equals(font.getString(PROP_FONT_STYLE)); + int fontStyle; + if (isBold && isItalic) { + fontStyle = Typeface.BOLD_ITALIC; + } else if (isBold) { + fontStyle = Typeface.BOLD; + } else if (isItalic) { + fontStyle = Typeface.ITALIC; + } else { + fontStyle = Typeface.NORMAL; + } + // NB: if the font family is null / unsupported, the default one will be used + paint.setTypeface(Typeface.create(font.getString(PROP_FONT_FAMILY), fontStyle)); + } + } } - } } - } } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGVirtualNode.java b/android/src/main/java/com/horcrux/svg/RNSVGVirtualNode.java index 8233ec02..376fa1d0 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGVirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGVirtualNode.java @@ -11,6 +11,7 @@ package com.horcrux.svg; import javax.annotation.Nullable; +import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; @@ -27,87 +28,87 @@ import com.facebook.react.uimanager.ReactShadowNode; */ public abstract class RNSVGVirtualNode extends ReactShadowNode { - protected static final float MIN_OPACITY_FOR_DRAW = 0.01f; + protected static final float MIN_OPACITY_FOR_DRAW = 0.01f; - private static final float[] sMatrixData = new float[9]; - private static final float[] sRawMatrix = new float[9]; + private static final float[] sMatrixData = new float[9]; + private static final float[] sRawMatrix = new float[9]; - protected float mOpacity = 1f; - private @Nullable Matrix mMatrix = new Matrix(); + protected float mOpacity = 1f; + private @Nullable Matrix mMatrix = new Matrix(); - protected final float mScale; + protected final float mScale; - public RNSVGVirtualNode() { - mScale = DisplayMetricsHolder.getWindowDisplayMetrics().density; - } - - @Override - public boolean isVirtual() { - return true; - } - - public abstract void draw(Canvas canvas, Paint paint, float opacity); - - /** - * Sets up the transform matrix on the canvas before an element is drawn. - * - * NB: for perf reasons this does not apply opacity, as that would mean creating a new canvas - * layer (which allocates an offscreen bitmap) and having it composited afterwards. Instead, the - * drawing code should apply opacity recursively. - * - * @param canvas the canvas to set up - */ - protected final void saveAndSetupCanvas(Canvas canvas) { - canvas.save(); - if (mMatrix != null) { - canvas.concat(mMatrix); + public RNSVGVirtualNode() { + mScale = DisplayMetricsHolder.getWindowDisplayMetrics().density; } - } - /** - * Restore the canvas after an element was drawn. This is always called in mirror with - * {@link #saveAndSetupCanvas}. - * - * @param canvas the canvas to restore - */ - protected void restoreCanvas(Canvas canvas) { - canvas.restore(); - } - - @ReactProp(name = "opacity", defaultFloat = 1f) - public void setOpacity(float opacity) { - mOpacity = opacity; - markUpdated(); - } - - @ReactProp(name = "transform") - public void setTransform(@Nullable ReadableArray transformArray) { - if (transformArray != null) { - int matrixSize = PropHelper.toFloatArray(transformArray, sMatrixData); - if (matrixSize == 6) { - setupMatrix(); - } else if (matrixSize != -1) { - throw new JSApplicationIllegalArgumentException("Transform matrices must be of size 6"); - } - } else { - mMatrix = null; + @Override + public boolean isVirtual() { + return true; } - markUpdated(); - } - protected void setupMatrix() { - sRawMatrix[0] = sMatrixData[0]; - sRawMatrix[1] = sMatrixData[2]; - sRawMatrix[2] = sMatrixData[4] * mScale; - sRawMatrix[3] = sMatrixData[1]; - sRawMatrix[4] = sMatrixData[3]; - sRawMatrix[5] = sMatrixData[5] * mScale; - sRawMatrix[6] = 0; - sRawMatrix[7] = 0; - sRawMatrix[8] = 1; - if (mMatrix == null) { - mMatrix = new Matrix(); + public abstract void draw(Canvas canvas, Paint paint, float opacity); + + /** + * Sets up the transform matrix on the canvas before an element is drawn. + * + * NB: for perf reasons this does not apply opacity, as that would mean creating a new canvas + * layer (which allocates an offscreen bitmap) and having it composited afterwards. Instead, the + * drawing code should apply opacity recursively. + * + * @param canvas the canvas to set up + */ + protected final void saveAndSetupCanvas(Canvas canvas) { + canvas.save(); + if (mMatrix != null) { + canvas.concat(mMatrix); + } + } + + /** + * Restore the canvas after an element was drawn. This is always called in mirror with + * {@link #saveAndSetupCanvas}. + * + * @param canvas the canvas to restore + */ + protected void restoreCanvas(Canvas canvas) { + canvas.restore(); + } + + @ReactProp(name = "opacity", defaultFloat = 1f) + public void setOpacity(float opacity) { + mOpacity = opacity; + markUpdated(); + } + + @ReactProp(name = "transform") + public void setTransform(@Nullable ReadableArray transformArray) { + if (transformArray != null) { + int matrixSize = PropHelper.toFloatArray(transformArray, sMatrixData); + if (matrixSize == 6) { + setupMatrix(); + } else if (matrixSize != -1) { + throw new JSApplicationIllegalArgumentException("Transform matrices must be of size 6"); + } + } else { + mMatrix = null; + } + markUpdated(); + } + + protected void setupMatrix() { + sRawMatrix[0] = sMatrixData[0]; + sRawMatrix[1] = sMatrixData[2]; + sRawMatrix[2] = sMatrixData[4] * mScale; + sRawMatrix[3] = sMatrixData[1]; + sRawMatrix[4] = sMatrixData[3]; + sRawMatrix[5] = sMatrixData[5] * mScale; + sRawMatrix[6] = 0; + sRawMatrix[7] = 0; + sRawMatrix[8] = 1; + if (mMatrix == null) { + mMatrix = new Matrix(); + } + mMatrix.setValues(sRawMatrix); } - mMatrix.setValues(sRawMatrix); - } }