complete elements percentage props support

Support Rect, Circle, Line, Ellipse percentage props
And support percentage props for gradients
This commit is contained in:
Horcrux
2016-04-27 16:09:38 +08:00
parent cd77525a7b
commit c2359616bf
11 changed files with 291 additions and 108 deletions
@@ -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;
}
}
}
@@ -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!");
}
}
}
@@ -27,6 +27,7 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
/* 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_SHAPE = "RNSVGShape";
private final String mClassName;
@@ -42,6 +43,10 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGRenderableViewManager(CLASS_TEXT);
}
public static RNSVGRenderableViewManager createRNSVGShapeViewManager() {
return new RNSVGRenderableViewManager(CLASS_SHAPE);
}
private RNSVGRenderableViewManager(String className) {
mClassName = className;
}
@@ -57,6 +62,8 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGGroupShadowNode();
} else if (mClassName == CLASS_SVG) {
return new RNSVGPathShadowNode();
} else if (mClassName == CLASS_SHAPE) {
return new RNSVGShapeShadowNode();
} else if (mClassName == CLASS_TEXT) {
return new RNSVGTextShadowNode();
} else {
@@ -70,9 +77,11 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return RNSVGGroupShadowNode.class;
} else if (mClassName == CLASS_SVG) {
return RNSVGPathShadowNode.class;
} else if (mClassName == CLASS_SHAPE) {
return RNSVGShapeShadowNode.class;
} else if (mClassName == CLASS_TEXT) {
return RNSVGTextShadowNode.class;
} else {
}else {
throw new IllegalStateException("Unexpected type " + mClassName);
}
}
@@ -0,0 +1,131 @@
/**
* 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 android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nullable;
/**
* Shadow node for virtual RNSVGPath view
*/
public class RNSVGShapeShadowNode extends RNSVGPathShadowNode {
protected ReadableMap mShape;
@ReactProp(name = "shape")
public void setShape(@Nullable ReadableMap shape) {
mShape = shape;
markUpdated();
}
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
if (mShape != null) {
int type = mShape.getInt("type");
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
mPath = new Path();
switch (type) {
case 0: {
// draw circle
// TODO:
float cx = getActualProp("cx", width);
float cy = getActualProp("cy", height);
float r = getActualProp("r", width);
mPath.addCircle(cx, cy, r, Path.Direction.CW);
break;
}
case 1: {
// draw ellipse
float cx = getActualProp("cx", width);
float cy = getActualProp("cy", height);
float rx = getActualProp("rx", width);
float ry = getActualProp("ry", height);
RectF oval = new RectF(cx - rx, cy - ry, cx + rx, cy + ry);
mPath.addOval(oval, Path.Direction.CW);
break;
}
case 2: {
// draw line
float x1 = getActualProp("x1", width);
float y1 = getActualProp("y1", height);
float x2 = getActualProp("x2", width);
float y2 = getActualProp("y2", height);
mPath.moveTo(x1, y1);
mPath.lineTo(x2, y2);
break;
}
case 3: {
// draw rect
float x = getActualProp("x", width);
float y = getActualProp("y", height);
float w = getActualProp("width", width);
float h = getActualProp("height", height);
float rx = getActualProp("rx", width);
float ry = getActualProp("ry", height);
if (rx != 0 || ry != 0) {
if (rx == 0) {
rx = ry;
} else if (ry == 0) {
ry = rx;
}
if (rx > 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;
}
}
}
@@ -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);
@@ -30,6 +30,7 @@ public class RNSvgPackage implements ReactPackage {
RNSVGRenderableViewManager.createRNSVGGroupViewManager(),
RNSVGRenderableViewManager.createRNSVGPathViewManager(),
RNSVGRenderableViewManager.createRNSVGTextViewManager(),
RNSVGRenderableViewManager.createRNSVGShapeViewManager(),
new RNSVGSvgViewManager());
}