From e0c3e152943d5df84b47a32ea931e0a055cf8a0f Mon Sep 17 00:00:00 2001 From: Horcrux Date: Thu, 12 Jan 2017 20:36:20 +0800 Subject: [PATCH] Finish TextPath and TSpan in Android --- .../com/horcrux/svg/BezierTransformer.java | 142 +++++++++ .../com/horcrux/svg/CircleShadowNode.java | 3 +- .../java/com/horcrux/svg/GlyphContext.java | 187 ++++++++++++ .../java/com/horcrux/svg/GroupShadowNode.java | 42 +-- .../java/com/horcrux/svg/PathShadowNode.java | 9 +- .../main/java/com/horcrux/svg/PropHelper.java | 64 +++- .../com/horcrux/svg/RenderableShadowNode.java | 5 +- .../horcrux/svg/RenderableViewManager.java | 27 +- .../java/com/horcrux/svg/SpanShadowNode.java | 155 ---------- .../main/java/com/horcrux/svg/SvgPackage.java | 33 +- .../java/com/horcrux/svg/TSpanShadowNode.java | 192 ++++++++++++ .../com/horcrux/svg/TextPathShadowNode.java | 73 +++++ .../java/com/horcrux/svg/TextShadowNode.java | 282 +++++++++++------- .../java/com/horcrux/svg/VirtualNode.java | 41 --- ios/Text/RNSVGBezierTransformer.h | 5 +- ios/Text/RNSVGBezierTransformer.m | 38 +-- 16 files changed, 918 insertions(+), 380 deletions(-) create mode 100644 android/src/main/java/com/horcrux/svg/BezierTransformer.java create mode 100644 android/src/main/java/com/horcrux/svg/GlyphContext.java delete mode 100644 android/src/main/java/com/horcrux/svg/SpanShadowNode.java create mode 100644 android/src/main/java/com/horcrux/svg/TSpanShadowNode.java create mode 100644 android/src/main/java/com/horcrux/svg/TextPathShadowNode.java diff --git a/android/src/main/java/com/horcrux/svg/BezierTransformer.java b/android/src/main/java/com/horcrux/svg/BezierTransformer.java new file mode 100644 index 00000000..a31d7352 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/BezierTransformer.java @@ -0,0 +1,142 @@ +/** + * 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.Matrix; +import android.graphics.PointF; +import android.util.Log; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +public class BezierTransformer { + private ReadableArray mBezierCurves; + private int mCurrentBezierIndex = 0; + private float mStartOffset = 0f; + private float mLastOffset = 0f; + private float mLastRecord = 0f; + private float mLastDistance = 0f; + private PointF mLastPoint = new PointF(); + private PointF mP0 = new PointF(); + private PointF mP1 = new PointF(); + private PointF mP2 = new PointF(); + private PointF mP3 = new PointF(); + private boolean mReachedStart; + private boolean mReachedEnd; + + BezierTransformer(ReadableArray bezierCurves, float startOffset) { + mBezierCurves = bezierCurves; + mStartOffset = startOffset; + } + + private float calculateBezier(float t, float P0, float P1, float P2, float P3) { + return (1-t)*(1-t)*(1-t)*P0+3*(1-t)*(1-t)*t*P1+3*(1-t)*t*t*P2+t*t*t*P3; + } + + private PointF pointAtOffset(float t) { + float x = calculateBezier(t, mP0.x, mP1.x, mP2.x, mP3.x); + float y = calculateBezier(t, mP0.y, mP1.y, mP2.y, mP3.y); + return new PointF(x, y); + } + + private float calculateBezierPrime(float t, float P0, float P1, float P2, float P3) { + return -3*(1-t)*(1-t)*P0+(3*(1-t)*(1-t)*P1)-(6*t*(1-t)*P1)-(3*t*t*P2)+(6*t*(1-t)*P2)+3*t*t*P3; + } + + private float angleAtOffset(float t) { + float dx = calculateBezierPrime(t, mP0.x, mP1.x, mP2.x, mP3.x); + float dy = calculateBezierPrime(t, mP0.y, mP1.y, mP2.y, mP3.y); + return (float)Math.atan2(dy, dx); + } + + private float calculateDistance(PointF a, PointF b) { + return (float)Math.hypot(a.x - b.x, a.y - b.y); + } + + private PointF getPointFromMap(ReadableMap map) { + return new PointF((float)map.getDouble("x"), (float)map.getDouble("y")); + } + + // Simplistic routine to find the offset along Bezier that is + // `distance` away from `point`. `offset` is the offset used to + // generate `point`, and saves us the trouble of recalculating it + // This routine just walks forward until it finds a point at least + // `distance` away. Good optimizations here would reduce the number + // of guesses, but this is tricky since if we go too far out, the + // curve might loop back on leading to incorrect results. Tuning + // kStep is good start. + private float offsetAtDistance(float distance, PointF point, float offset) { + float kStep = 0.001f; // 0.0001 - 0.001 work well + float newDistance = 0; + float newOffset = offset + kStep; + while (newDistance <= distance && newOffset < 1.0) { + newOffset += kStep; + newDistance = calculateDistance(point, pointAtOffset(newOffset)); + } + + mLastDistance = newDistance; + return newOffset; + } + + private void setControlPoints() { + ReadableArray bezier = mBezierCurves.getArray(mCurrentBezierIndex++); + + if (bezier != null) { + // set start point + if (bezier.size() == 1) { + mLastPoint = mP0 = getPointFromMap(bezier.getMap(0)); + setControlPoints(); + } else if (bezier.size() == 3) { + mP1 = getPointFromMap(bezier.getMap(0)); + mP2 = getPointFromMap(bezier.getMap(1)); + mP3 = getPointFromMap(bezier.getMap(2)); + } + } + } + + public Matrix getTransformAtDistance(float distance) { + distance += mStartOffset; + mReachedStart = distance >= 0; + + if (mReachedEnd || !mReachedStart) { + return new Matrix(); + } + + float offset = offsetAtDistance(distance - mLastRecord, mLastPoint, mLastOffset); + + if (offset < 1) { + PointF glyphPoint = pointAtOffset(offset); + mLastOffset = offset; + mLastPoint = glyphPoint; + mLastRecord = distance; + Matrix matrix = new Matrix(); + matrix.setRotate((float)Math.toDegrees(angleAtOffset(offset))); + matrix.postTranslate(glyphPoint.x, glyphPoint.y); + return matrix; + } else if (mBezierCurves.size() == mCurrentBezierIndex) { + mReachedEnd = true; + return new Matrix(); + } else { + mLastOffset = 0; + mLastPoint = mP0 = mP3; + mLastRecord += mLastDistance; + setControlPoints(); + return getTransformAtDistance(distance - mStartOffset); + } + } + + public boolean hasReachedEnd() { + return mReachedEnd; + } + + public boolean hasReachedStart() { + return mReachedStart; + } +} diff --git a/android/src/main/java/com/horcrux/svg/CircleShadowNode.java b/android/src/main/java/com/horcrux/svg/CircleShadowNode.java index c3f3b9c7..d11a9f61 100644 --- a/android/src/main/java/com/horcrux/svg/CircleShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/CircleShadowNode.java @@ -9,13 +9,12 @@ package com.horcrux.svg; + import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; - import com.facebook.react.uimanager.annotations.ReactProp; - /** * Shadow node for virtual RNSVGPath view */ diff --git a/android/src/main/java/com/horcrux/svg/GlyphContext.java b/android/src/main/java/com/horcrux/svg/GlyphContext.java new file mode 100644 index 00000000..8f06d6dc --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/GlyphContext.java @@ -0,0 +1,187 @@ +/** + * 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.PointF; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class GlyphContext { + + private ArrayList mFontContext; + private ArrayList mLocationContext; + private ArrayList> mDeltaXContext; + private ArrayList> mDeltaYContext; + private ArrayList mXContext; + private @Nonnull PointF mCurrentLocation; + private float mScale; + private float mWidth; + private float mHeight; + private int mContextLength = 0; + private static final float DEFAULT_FONT_SIZE = 12f; + + GlyphContext(float scale, float width, float height) { + mScale = scale; + mWidth = width; + mHeight = height; + mCurrentLocation = new PointF(); + mFontContext = new ArrayList<>(); + mLocationContext = new ArrayList<>(); + mDeltaXContext = new ArrayList<>(); + mDeltaYContext = new ArrayList<>(); + mXContext = new ArrayList<>(); + } + + public void pushContext(@Nullable ReadableMap font, @Nullable ReadableArray deltaX, @Nullable ReadableArray deltaY, @Nullable String positionX, @Nullable String positionY) { + PointF location = mCurrentLocation; + + if (positionX != null) { + location.x = PropHelper.fromPercentageToFloat(positionX, mWidth, 0, mScale); + } + + if (positionY != null) { + location.y = PropHelper.fromPercentageToFloat(positionY, mHeight, 0, mScale); + } + + mLocationContext.add(location); + mFontContext.add(font); + mDeltaXContext.add(getFloatArrayListFromReadableArray(deltaX)); + mDeltaYContext.add(getFloatArrayListFromReadableArray(deltaY)); + mXContext.add(location.x); + + mCurrentLocation = clonePointF(location); + mContextLength++; + } + + public void popContext() { + float x = mXContext.get(mContextLength - 1); + mFontContext.remove(mContextLength - 1); + mLocationContext.remove(mContextLength - 1); + mDeltaXContext.remove(mContextLength - 1); + mDeltaYContext.remove(mContextLength - 1); + mXContext.remove(mContextLength - 1); + + mContextLength--; + + if (mContextLength != 0) { + mXContext.set(mContextLength - 1, x); + PointF lastLocation = mLocationContext.get(mContextLength - 1); + mCurrentLocation = clonePointF(lastLocation); + mCurrentLocation.x = lastLocation.x = x; + } + } + + public PointF getNextGlyphPoint(float offset, float glyphWidth) { + float dx = getNextDelta(mDeltaXContext); + mCurrentLocation.x += dx; + + float dy = getNextDelta(mDeltaYContext); + mCurrentLocation.y += dy; + + for (PointF point: mLocationContext) { + point.x += dx; + point.y += dy; + } + + mXContext.set(mXContext.size() - 1, mCurrentLocation.x + offset + glyphWidth); + + return new PointF(mCurrentLocation.x + offset, mCurrentLocation.y); + + } + + private float getNextDelta(ArrayList> deltaContext) { + float value = 0; + boolean valueSet = false; + int index = mContextLength - 1; + + for (; index >= 0; index--) { + ArrayList delta = deltaContext.get(index); + + if (delta.size() != 0) { + if (!valueSet) { + value = delta.get(0); + valueSet = true; + } + + delta.remove(0); + } + } + + return value; + } + + public ReadableMap getGlyphFont() { + String fontFamily = null; + float fontSize = DEFAULT_FONT_SIZE; + boolean fontSizeSet = false; + String fontWeight = null; + String fontStyle = null; + + int index = mContextLength - 1; + + for (; index >= 0; index--) { + ReadableMap font = mFontContext.get(index); + + if (fontFamily == null && font.hasKey("fontFamily")) { + fontFamily = font.getString("fontFamily"); + } + + if (!fontSizeSet && font.hasKey("fontSize")) { + fontSize = (float)font.getDouble("fontSize"); + fontSizeSet = true; + } + + if (fontWeight == null && font.hasKey("fontWeight")) { + fontWeight = font.getString("fontWeight"); + } + if (fontStyle == null && font.hasKey("fontStyle")) { + fontStyle = font.getString("fontStyle"); + } + + if (fontFamily != null && fontSizeSet && fontWeight != null && fontStyle != null) { + break; + } + } + + WritableMap map = Arguments.createMap(); + map.putString("fontFamily", fontFamily); + map.putDouble("fontSize", fontSize); + map.putString("fontWeight", fontWeight); + map.putString("fontStyle", fontStyle); + + return map; + } + + private ArrayList getFloatArrayListFromReadableArray(ReadableArray readableArray) { + ArrayList arrayList = new ArrayList<>(); + + if (readableArray != null) { + for (int i = 0; i < readableArray.size(); i++) { + arrayList.add((float)readableArray.getDouble(i)); + } + } + + return arrayList; + } + + private PointF clonePointF(PointF point) { + return new PointF(point.x, point.y); + } +} diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index 1e5f0d83..5a3b71e2 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -26,30 +26,36 @@ import javax.annotation.Nullable; public class GroupShadowNode extends RenderableShadowNode { public void draw(final Canvas canvas, final Paint paint, final float opacity) { + if (opacity > MIN_OPACITY_FOR_DRAW) { + clip(canvas, paint); + drawGroup(canvas, paint, opacity); + } + } + + protected void drawGroup(final Canvas canvas, final Paint paint, final float opacity) { + int count = saveAndSetupCanvas(canvas); final SvgViewShadowNode svg = getSvgShadowNode(); final VirtualNode self = this; + traverseChildren(new NodeRunnable() { + public boolean run(VirtualNode node) { + node.setupDimensions(canvas); - if (opacity > MIN_OPACITY_FOR_DRAW) { - int count = saveAndSetupCanvas(canvas); - clip(canvas, paint); + node.mergeProperties(self, mAttributeList, true); + node.draw(canvas, paint, opacity * mOpacity); + node.markUpdateSeen(); - traverseChildren(new NodeRunnable() { - public boolean run(VirtualNode node) { - node.setupDimensions(canvas); - - node.mergeProperties(self, mAttributeList, true); - node.draw(canvas, paint, opacity * mOpacity); - node.markUpdateSeen(); - - if (node.isResponsible()) { - svg.enableTouchEvents(); - } - return true; + if (node.isResponsible()) { + svg.enableTouchEvents(); } - }); + return true; + } + }); - restoreCanvas(canvas, count); - } + restoreCanvas(canvas, count); + } + + protected void drawPath(Canvas canvas, Paint paint, float opacity) { + super.draw(canvas, paint, opacity); } @Override diff --git a/android/src/main/java/com/horcrux/svg/PathShadowNode.java b/android/src/main/java/com/horcrux/svg/PathShadowNode.java index ad0f48e0..5ac1b033 100644 --- a/android/src/main/java/com/horcrux/svg/PathShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/PathShadowNode.java @@ -42,11 +42,12 @@ import java.util.regex.Pattern; public class PathShadowNode extends RenderableShadowNode { private Path mPath; + private PropHelper.PathParser mD; @ReactProp(name = "d") public void setD(String d) { - PropHelper.PathParser parser = new PropHelper.PathParser(d, mScale); - mPath = parser.getPath(); + mD = new PropHelper.PathParser(d, mScale); + mPath = mD.getPath(); markUpdated(); } @@ -54,4 +55,8 @@ public class PathShadowNode extends RenderableShadowNode { protected Path getPath(Canvas canvas, Paint paint) { return mPath; } + + public ReadableArray getBezierCurves() { + return mD.getBezierCurves(); + } } diff --git a/android/src/main/java/com/horcrux/svg/PropHelper.java b/android/src/main/java/com/horcrux/svg/PropHelper.java index 52472629..f31531b7 100644 --- a/android/src/main/java/com/horcrux/svg/PropHelper.java +++ b/android/src/main/java/com/horcrux/svg/PropHelper.java @@ -11,18 +11,21 @@ package com.horcrux.svg; import android.graphics.Color; import android.graphics.Path; +import android.graphics.PointF; 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.Arguments; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,7 +33,7 @@ import java.util.regex.Pattern; /** * Contains static helper methods for accessing props. */ -/* package */ class PropHelper { +class PropHelper { /** * Converts {@link ReadableArray} to an array of {@code float}. Returns newly created array. @@ -213,16 +216,19 @@ import java.util.regex.Pattern; private String mLastCommand; private String mLastValue; + private WritableArray mBezierCurves; + private WritableMap mLastStartPoint; public PathParser(String d, float scale) { mScale = scale; mString = d; - mPath = new Path(); - mMatcher = PATH_REG_EXP.matcher(DECIMAL_REG_EXP.matcher(mString).replaceAll("$1,")); + } - while (mMatcher.find() && mValid) { - executeCommand(mMatcher.group()); + public ReadableArray getBezierCurves() { + if (mBezierCurves == null) { + getPath(); } + return mBezierCurves; } private void executeCommand(String command) { @@ -320,9 +326,30 @@ import java.util.regex.Pattern; } public Path getPath() { + mPath = new Path(); + mBezierCurves = Arguments.createArray(); + mMatcher = PATH_REG_EXP.matcher(DECIMAL_REG_EXP.matcher(mString).replaceAll("$1,")); + + while (mMatcher.find() && mValid) { + executeCommand(mMatcher.group()); + } return mPath; } + private WritableMap getPointMap(float x, float y) { + WritableMap map = Arguments.createMap(); + map.putDouble("x", x * mScale); + map.putDouble("y", y * mScale); + return map; + } + + private WritableMap clonePointMap(WritableMap map) { + WritableMap cloned = Arguments.createMap(); + cloned.putDouble("x", map.getDouble("x")); + cloned.putDouble("y", map.getDouble("y")); + return cloned; + } + private boolean getNextBoolean() { if (mMatcher.find()) { return mMatcher.group().equals("1"); @@ -354,6 +381,11 @@ import java.util.regex.Pattern; mPivotX = mPenX = x; mPivotY = mPenY = y; mPath.moveTo(x * mScale, y * mScale); + + mLastStartPoint = getPointMap(x ,y); + WritableArray points = Arguments.createArray(); + points.pushMap(getPointMap(x, y)); + mBezierCurves.pushArray(points); } private void line(float x, float y) { @@ -365,6 +397,12 @@ import java.util.regex.Pattern; mPivotX = mPenX = x; mPivotY = mPenY = y; mPath.lineTo(x * mScale, y * mScale); + + WritableArray points = Arguments.createArray(); + points.pushMap(getPointMap(x, y)); + points.pushMap(getPointMap(x, y)); + points.pushMap(getPointMap(x, y)); + mBezierCurves.pushArray(points); } private void curve(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { @@ -375,6 +413,12 @@ import java.util.regex.Pattern; mPivotX = c2x; mPivotY = c2y; cubicTo(c1x, c1y, c2x, c2y, ex, ey); + + WritableArray points = Arguments.createArray(); + points.pushMap(getPointMap(c1x, c1y)); + points.pushMap(getPointMap(c2x, c2y)); + points.pushMap(getPointMap(ex, ey)); + mBezierCurves.pushArray(points); } private void cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) { @@ -532,6 +576,12 @@ import java.util.regex.Pattern; mPenY = mPenDownY; mPendDownSet = false; mPath.close(); + + WritableArray points = Arguments.createArray(); + points.pushMap(clonePointMap(mLastStartPoint)); + points.pushMap(clonePointMap(mLastStartPoint)); + points.pushMap(clonePointMap(mLastStartPoint)); + mBezierCurves.pushArray(points); } } diff --git a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java index 7d9e7fa3..1bdfaf01 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java @@ -20,9 +20,6 @@ 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.Arguments; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -137,7 +134,7 @@ abstract public class RenderableShadowNode extends VirtualNode { markUpdated(); } - @ReactProp(name = "strokeWidth", defaultFloat = 0f) + @ReactProp(name = "strokeWidth", defaultFloat = 1f) public void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; markUpdated(); diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index d52b6f96..68291fa2 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -25,6 +25,8 @@ public class RenderableViewManager extends ViewManager { /* package */ static final String CLASS_GROUP = "RNSVGGroup"; /* package */ static final String CLASS_PATH = "RNSVGPath"; /* package */ static final String CLASS_TEXT = "RNSVGText"; + /* package */ static final String CLASS_TSPAN = "RNSVGTSpan"; + /* package */ static final String CLASS_TEXT_PATH = "RNSVGTextPath"; /* package */ static final String CLASS_IMAGE = "RNSVGImage"; /* package */ static final String CLASS_CIRCLE = "RNSVGCircle"; /* package */ static final String CLASS_ELLIPSE = "RNSVGEllipse"; @@ -36,7 +38,6 @@ public class RenderableViewManager extends ViewManager { /* package */ static final String CLASS_VIEW_BOX = "RNSVGViewBox"; /* package */ static final String CLASS_LINEAR_GRADIENT = "RNSVGLinearGradient"; /* package */ static final String CLASS_RADIAL_GRADIENT = "RNSVGRadialGradient"; - /* package */ static final String CLASS_SPAN = "RNSVGSpan"; private final String mClassName; @@ -53,6 +54,14 @@ public class RenderableViewManager extends ViewManager { return new RenderableViewManager(CLASS_TEXT); } + public static RenderableViewManager createTSpanViewManager() { + return new RenderableViewManager(CLASS_TSPAN); + } + + public static RenderableViewManager createTextPathViewManager() { + return new RenderableViewManager(CLASS_TEXT_PATH); + } + public static RenderableViewManager createImageViewManager() { return new RenderableViewManager(CLASS_IMAGE); } @@ -101,10 +110,6 @@ public class RenderableViewManager extends ViewManager { mClassName = className; } - public static RNSVGRenderableViewManager createRNSVGSpanManager() { - return new RNSVGRenderableViewManager(CLASS_SPAN); - } - @Override public String getName() { return mClassName; @@ -127,6 +132,10 @@ public class RenderableViewManager extends ViewManager { return new RectShadowNode(); case CLASS_TEXT: return new TextShadowNode(); + case CLASS_TSPAN: + return new TSpanShadowNode(); + case CLASS_TEXT_PATH: + return new TextPathShadowNode(); case CLASS_IMAGE: return new ImageShadowNode(); case CLASS_CLIP_PATH: @@ -141,8 +150,6 @@ public class RenderableViewManager extends ViewManager { return new LinearGradientShadowNode(); case CLASS_RADIAL_GRADIENT: return new RadialGradientShadowNode(); - case CLASS_SPAN: - return new SpanShadowNode(); default: throw new IllegalStateException("Unexpected type " + mClassName); } @@ -165,6 +172,10 @@ public class RenderableViewManager extends ViewManager { return RectShadowNode.class; case CLASS_TEXT: return TextShadowNode.class; + case CLASS_TSPAN: + return TSpanShadowNode.class; + case CLASS_TEXT_PATH: + return TextPathShadowNode.class; case CLASS_IMAGE: return ImageShadowNode.class; case CLASS_CLIP_PATH: @@ -179,8 +190,6 @@ public class RenderableViewManager extends ViewManager { return LinearGradientShadowNode.class; case CLASS_RADIAL_GRADIENT: return RadialGradientShadowNode.class; - case CLASS_SPAN: - return SpanShadowNode.class; default: throw new IllegalStateException("Unexpected type " + mClassName); } diff --git a/android/src/main/java/com/horcrux/svg/SpanShadowNode.java b/android/src/main/java/com/horcrux/svg/SpanShadowNode.java deleted file mode 100644 index 23d7bddf..00000000 --- a/android/src/main/java/com/horcrux/svg/SpanShadowNode.java +++ /dev/null @@ -1,155 +0,0 @@ -/** - * 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.Matrix; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.util.Log; - -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.uimanager.annotations.ReactProp; - -import javax.annotation.Nullable; - -/** - * Shadow node for virtual RNSVGPath view - */ -public class SpanShadowNode extends PathShadowNode { - - 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 float mDx; - private float mDy; - private @Nullable String mPx; - private @Nullable String mPy; - private ReadableMap mFont; - private String mContent; - - @ReactProp(name = "dx") - public void setDx(float dx) { - mDx = dx * mScale; - markUpdated(); - } - - @ReactProp(name = "dy") - public void setDy(float dy) { - mDy = dy * mScale; - markUpdated(); - } - - @ReactProp(name = "px") - public void setPx(String px) { - mPx = px; - markUpdated(); - } - - @ReactProp(name = "py") - public void setPy(String py) { - mPy = py; - markUpdated(); - } - - @ReactProp(name = "font") - public void setFont(ReadableMap font) { - mFont = font; - markUpdated(); - } - - @ReactProp(name = "content") - public void setContent(String content) { - mContent = content; - 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(); - RNSVGTextShadowNode text = (RNSVGTextShadowNode)getParent(); - - if (text == null) { - return path; - } - - applyTextPropertiesToPaint(paint); - paint.getTextPath(mContent, 0, mContent.length(), 0, 0, path); - - if (!mContent.isEmpty()) { - if (mPx != null) { - text.setOffsetX(PropHelper.fromPercentageToFloat(mPx, mCanvasWidth, 0, mScale), false); - } - - if (mPy != null) { - text.setOffsetY(PropHelper.fromPercentageToFloat(mPy, mCanvasHeight, 0, mScale) - paint.ascent(), false); - } - - text.setOffsetX(mDx, true); - text.setOffsetY(mDy, true); - - Matrix matrix = new Matrix(); - matrix.setTranslate(text.getOffsetX(), text.getOffsetY()); - - text.setOffsetX(getBox(paint).width(), true); - - path.transform(matrix); - } else { - text.setOffsetX(mDx, true); - text.setOffsetY(mDy, true); - } - - return path; - } - - private void applyTextPropertiesToPaint(Paint paint) { - paint.setTextAlign(Paint.Align.LEFT); - - float fontSize = DEFAULT_FONT_SIZE; - if (mFont.hasKey(PROP_FONT_SIZE)) { - fontSize = (float) mFont.getDouble(PROP_FONT_SIZE); - } - paint.setTextSize(fontSize * mScale); - boolean isBold = mFont.hasKey(PROP_FONT_WEIGHT) && "bold".equals(mFont.getString(PROP_FONT_WEIGHT)); - boolean isItalic = mFont.hasKey(PROP_FONT_STYLE) && "italic".equals(mFont.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(mFont.getString(PROP_FONT_FAMILY), fontStyle)); - } - - public RectF getBox(Paint paint) { - applyTextPropertiesToPaint(paint); - Rect bound = new Rect(); - paint.getTextBounds(mContent, 0, mContent.length(), bound); - return new RectF(bound); - } -} diff --git a/android/src/main/java/com/horcrux/svg/SvgPackage.java b/android/src/main/java/com/horcrux/svg/SvgPackage.java index 5081de14..fca7663e 100644 --- a/android/src/main/java/com/horcrux/svg/SvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/SvgPackage.java @@ -25,22 +25,23 @@ public class SvgPackage implements ReactPackage { @Override public List createViewManagers(ReactApplicationContext reactContext) { return Arrays.asList( - RenderableViewManager.createGroupViewManager(), - RenderableViewManager.createPathViewManager(), - RenderableViewManager.createCircleViewManager(), - RenderableViewManager.createEllipseViewManager(), - RenderableViewManager.createLineViewManager(), - RenderableViewManager.createRectViewManager(), - RenderableViewManager.createTextViewManager(), - RenderableViewManager.createImageViewManager(), - RenderableViewManager.createClipPathViewManager(), - RenderableViewManager.createDefsViewManager(), - RenderableViewManager.createUseViewManager(), - RenderableViewManager.createViewBoxViewManager(), - RenderableViewManager.createLinearGradientManager(), - RenderableViewManager.createRadialGradientManager(), - RenderableViewManager.createSpanManager(), - new SvgViewManager()); + RenderableViewManager.createGroupViewManager(), + RenderableViewManager.createPathViewManager(), + RenderableViewManager.createCircleViewManager(), + RenderableViewManager.createEllipseViewManager(), + RenderableViewManager.createLineViewManager(), + RenderableViewManager.createRectViewManager(), + RenderableViewManager.createTextViewManager(), + RenderableViewManager.createTSpanViewManager(), + RenderableViewManager.createTextPathViewManager(), + RenderableViewManager.createImageViewManager(), + RenderableViewManager.createClipPathViewManager(), + RenderableViewManager.createDefsViewManager(), + RenderableViewManager.createUseViewManager(), + RenderableViewManager.createViewBoxViewManager(), + RenderableViewManager.createLinearGradientManager(), + RenderableViewManager.createRadialGradientManager(), + new SvgViewManager()); } @Override diff --git a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java new file mode 100644 index 00000000..d16c9631 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java @@ -0,0 +1,192 @@ +/** + * 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.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; +import android.util.Log; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.react.uimanager.annotations.ReactProp; + +import javax.annotation.Nullable; + +public class TSpanShadowNode extends TextShadowNode { + + private BezierTransformer mBezierTransformer; + private Path mCache; + private @Nullable String mContent; + + 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"; + + @ReactProp(name = "content") + public void setContent(@Nullable String content) { + mContent = content; + markUpdated(); + } + + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + if (mContent != null) { + drawPath(canvas, paint, opacity); + } else { + clip(canvas, paint); + drawGroup(canvas, paint, opacity); + } + } + + @Override + protected void releaseCachedPath() { + mCache = null; + } + + @Override + protected Path getPath(Canvas canvas, Paint paint) { + if (mCache != null) { + return mCache; + } + + String text = mContent; + + if (text == null) { + return getGroupPath(canvas, paint); + } + + setupTextPath(); + setupDimensions(canvas); + + Path path = new Path(); + + pushGlyphContext(); + applyTextPropertiesToPaint(paint); + getLinePath(mContent + " ", paint, path); + + mCache = path; + popGlyphContext(); + + RectF box = new RectF(); + path.computeBounds(box, true); + + return path; + } + + private Path getLinePath(String line, Paint paint, Path path) { + float[] widths = new float[line.length()]; + paint.getTextWidths(line, widths); + + float glyphPosition = 0f; + + for (int index = 0; index < line.length(); index++) { + String letter = line.substring(index, index + 1); + Path glyph = new Path(); + float width = widths[index]; + + paint.getTextPath(letter, 0, 1, 0, -paint.ascent(), glyph); + PointF glyphPoint = getGlyphPointFromContext(glyphPosition, width); + glyphPosition += width; + Matrix matrix = new Matrix(); + + if (mBezierTransformer != null) { + matrix = mBezierTransformer.getTransformAtDistance(glyphPoint.x); + + if (textPathHasReachedEnd()) { + break; + } else if (!textPathHasReachedStart()) { + continue; + } + + matrix.postTranslate(0, glyphPoint.y); + } else { + matrix.setTranslate(glyphPoint.x, glyphPoint.y); + } + + + glyph.transform(matrix); + path.addPath(glyph); + } + + if (mBezierTransformer != null) { + Matrix matrix = new Matrix(); + matrix.postTranslate(0, paint.ascent() * 1.1f); + path.transform(matrix); + } + + return path; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + + + private void applyTextPropertiesToPaint(Paint paint) { + ReadableMap font = getFontFromContext(); + + paint.setTextAlign(Paint.Align.LEFT); + + float 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)); + } + + private void setupTextPath() { + ReactShadowNode parent = getParent(); + + while (parent != null) { + if (parent.getClass() == TextPathShadowNode.class) { + TextPathShadowNode textPath = (TextPathShadowNode)parent; + mBezierTransformer = textPath.getBezierTransformer(); + break; + } else if (!(parent instanceof TextShadowNode)) { + break; + } + + parent = parent.getParent(); + } + } + + private boolean textPathHasReachedEnd() { + return mBezierTransformer.hasReachedEnd(); + } + + private boolean textPathHasReachedStart() { + return mBezierTransformer.hasReachedStart(); + } +} diff --git a/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java b/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java new file mode 100644 index 00000000..b852618b --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/TextPathShadowNode.java @@ -0,0 +1,73 @@ +/** + * 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.drawable.shapes.PathShape; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.annotations.ReactProp; + +import javax.annotation.Nullable; + +public class TextPathShadowNode extends TextShadowNode { + + private String mHref; + private @Nullable String mStartOffset; + + @ReactProp(name = "href") + public void setHref(String href) { + mHref = href; + markUpdated(); + } + + @ReactProp(name = "startOffset") + public void setStartOffset(@Nullable String startOffset) { + mStartOffset = startOffset; + markUpdated(); + } + + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + drawGroup(canvas, paint, opacity); + } + + public BezierTransformer getBezierTransformer() { + SvgViewShadowNode svg = getSvgShadowNode(); + VirtualNode template = svg.getDefinedTemplate(mHref); + + if (template == null || template.getClass() != PathShadowNode.class) { + // warning about this. + return null; + } + + PathShadowNode path = (PathShadowNode)template; + + return new BezierTransformer(path.getBezierCurves(), PropHelper.fromPercentageToFloat(mStartOffset, mCanvasWidth, 0, mScale)); + } + + @Override + protected Path getPath(Canvas canvas, Paint paint) { + return getGroupPath(canvas, paint); + } + + @Override + protected void pushGlyphContext() { + // do nothing + } + + @Override + protected void popGlyphContext() { + // do nothing + } + +} diff --git a/android/src/main/java/com/horcrux/svg/TextShadowNode.java b/android/src/main/java/com/horcrux/svg/TextShadowNode.java index f25585c0..9f64858b 100644 --- a/android/src/main/java/com/horcrux/svg/TextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TextShadowNode.java @@ -15,136 +15,218 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; + +import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.annotations.ReactProp; /** - * Shadow node for virtual RNSVGText view + * Shadow node for virtual Text view */ -public class RNSVGTextShadowNode extends RNSVGGroupShadowNode { +public class TextShadowNode extends GroupShadowNode { private float mOffsetX = 0; private float mOffsetY = 0; - private static final int TEXT_ALIGNMENT_LEFT = 0; - private static final int TEXT_ALIGNMENT_RIGHT = 1; + private static final int TEXT_ANCHOR_AUTO = 0; + private static final int TEXT_ANCHOR_START = 1; + private static final int TEXT_ANCHOR_MIDDLE = 2; + private static final int TEXT_ANCHOR_END = 3; - private int mTextAlignment = TEXT_ALIGNMENT_LEFT; - private Path mTextPath; + private int mTextAnchor = TEXT_ANCHOR_AUTO; + private @Nullable ReadableArray mDeltaX; + private @Nullable ReadableArray mDeltaY; + private @Nullable String mPositionX; + private @Nullable String mPositionY; + private @Nullable ReadableMap mFont; - @ReactProp(name = "alignment", defaultInt = TEXT_ALIGNMENT_LEFT) - public void setAlignment(int alignment) { - mTextAlignment = alignment; + private GlyphContext mGlyphContext; + private TextShadowNode mTextRoot; + + @ReactProp(name = "textAnchor", defaultInt = TEXT_ANCHOR_AUTO) + public void setTextAnchor(int textAnchor) { + mTextAnchor = textAnchor; + markUpdated(); } - @ReactProp(name = "path") - public void setPath(@Nullable ReadableArray textPath) { - float[] pathData = PropHelper.toFloatArray(textPath); - mTextPath = new Path(); - super.createPath(pathData, mTextPath); + @ReactProp(name = "deltaX") + public void setDeltaX(@Nullable ReadableArray deltaX) { + mDeltaX = deltaX; + markUpdated(); + } + + @ReactProp(name = "deltaY") + public void setDeltaY(@Nullable ReadableArray deltaY) { + mDeltaY = deltaY; + markUpdated(); + } + + @ReactProp(name = "positionX") + public void setPositionX(@Nullable String positionX) { + mPositionX = positionX; + markUpdated(); + } + + @ReactProp(name = "positionY") + public void setPositionY(@Nullable String positionY) { + mPositionY = positionY; + markUpdated(); + } + + @ReactProp(name = "font") + public void setFont(@Nullable ReadableMap font) { + mFont = font; markUpdated(); } @Override public void draw(Canvas canvas, Paint paint, float opacity) { - float shift = getShift(paint); + if (opacity > MIN_OPACITY_FOR_DRAW) { + clip(canvas, paint); - final int count = canvas.save(); + final int count = canvas.save(); + setupGlyphContext(canvas); - Matrix matrix = new Matrix(); - matrix.postTranslate(-shift, 0); - canvas.concat(matrix); - super.draw(canvas, paint, opacity); + Path path = getGroupPath(canvas, paint); + Matrix matrix = getAlignMatrix(path); + canvas.concat(matrix); + drawGroup(canvas, paint, opacity); + releaseCachedPath(); - restoreCanvas(canvas, count); - markUpdateSeen(); + restoreCanvas(canvas, count); + markUpdateSeen(); + + // todo: set hit area + } } @Override protected Path getPath(Canvas canvas, Paint paint) { - Path path = getPathFromSuper(canvas, paint); + setupGlyphContext(canvas); + Path groupPath = getGroupPath(canvas, paint); + Matrix matrix = getAlignMatrix(groupPath); + groupPath.transform(matrix); + + releaseCachedPath(); + return groupPath; + } + + protected void drawGroup(Canvas canvas, Paint paint, float opacity) { + pushGlyphContext(); + super.drawGroup(canvas, paint, opacity); + popGlyphContext(); + } + + public int getTextAnchor() { + return mTextAnchor; + } + + private int getComputedTextAnchor() { + int anchor = mTextAnchor; + + if (getChildCount() > 0) { + TextShadowNode child = (TextShadowNode)getChildAt(0); + + while (child.getChildCount() > 0 && anchor == TEXT_ANCHOR_AUTO) { + anchor = child.getTextAnchor(); + child = (TextShadowNode)child.getChildAt(0); + } + } + return anchor; + } + + protected TextShadowNode getTextRoot() { + if (mTextRoot == null) { + mTextRoot = this; + + while (mTextRoot != null) { + if (mTextRoot.getClass() == TextShadowNode.class) { + break; + } + + ReactShadowNode parent = mTextRoot.getParent(); + + if (!(parent instanceof TextShadowNode)) { + //todo: throw exception here + mTextRoot = null; + } else { + mTextRoot = (TextShadowNode)parent; + } + } + } + + return mTextRoot; + } + + protected void setupGlyphContext(Canvas canvas) { + setupDimensions(canvas); + mGlyphContext = new GlyphContext(mScale, mCanvasWidth, mCanvasHeight); + } + + protected void releaseCachedPath() { + traverseChildren(new NodeRunnable() { + public boolean run(VirtualNode node) { + TextShadowNode text = (TextShadowNode)node; + text.releaseCachedPath(); + return true; + } + }); + } + + protected Path getGroupPath(Canvas canvas, Paint paint) { + pushGlyphContext(); + Path groupPath = super.getPath(canvas, paint); + popGlyphContext(); + + return groupPath; + } + + protected GlyphContext getGlyphContext() { + return mGlyphContext; + } + + protected void pushGlyphContext() { + getTextRoot().getGlyphContext().pushContext(mFont, mDeltaX, mDeltaY, mPositionX, mPositionY); + } + + protected void popGlyphContext() { + getTextRoot().getGlyphContext().popContext(); + } + + protected ReadableMap getFontFromContext() { + return getTextRoot().getGlyphContext().getGlyphFont(); + } + + protected PointF getGlyphPointFromContext(float offset, float glyphWidth) { + return getTextRoot().getGlyphContext().getNextGlyphPoint(offset, glyphWidth); + } + + private Matrix getAlignMatrix(Path path) { + RectF box = new RectF(); + path.computeBounds(box, true); + + float width = box.width(); + float x = 0; + + switch (getComputedTextAnchor()) { + case TEXT_ANCHOR_MIDDLE: + x = -width / 2; + break; + case TEXT_ANCHOR_END: + x = -width; + break; + } - float shift = getShift(paint); Matrix matrix = new Matrix(); - matrix.setTranslate(shift, 0); - path.transform(matrix); - - return path; - } - - private Path getPathFromSuper(Canvas canvas, Paint paint) { - Path path = super.getPath(canvas, paint); - // reset offsetX and offsetY - mOffsetX = mOffsetY = 0; - return path; - } - - public void setOffsetX(float x, boolean increase) { - if (increase) { - mOffsetX += x; - } else { - mOffsetX = x; - } - } - - public void setOffsetY(float y, boolean increase) { - if (increase) { - mOffsetY += y; - } else { - mOffsetY = y; - } - } - - public float getOffsetX() { - return mOffsetX; - } - - public float getOffsetY() { - return mOffsetY; - } - - private float getShift(Paint paint) { - Rect rect = new Rect(); - - for (int i = getChildCount() - 1; i >= 0; i--) { - if (!(getChildAt(i) instanceof RNSVGVirtualNode)) { - continue; - } - - RectF box = ((RNSVGSpanShadowNode) getChildAt(i)).getBox(paint); - - if (rect.top > box.top) { - rect.top = (int)box.top; - } - if (rect.right < box.right) { - rect.right = (int)box.right; - } - if (rect.bottom < box.bottom) { - rect.bottom = (int)box.bottom; - } - if (rect.left > box.left) { - rect.left = (int)box.left; - } - } - - - float width = rect.width(); - float shift; - - switch (mTextAlignment) { - case TEXT_ALIGNMENT_RIGHT: - shift = width; - break; - case TEXT_ALIGNMENT_LEFT: - shift = 0; - break; - default: - shift = width / 2; - } - return shift; + matrix.setTranslate(x, 0); + return matrix; } } diff --git a/android/src/main/java/com/horcrux/svg/VirtualNode.java b/android/src/main/java/com/horcrux/svg/VirtualNode.java index ea64da33..d22960be 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/VirtualNode.java @@ -154,47 +154,6 @@ public abstract class VirtualNode extends LayoutShadowNode { mMatrix.setValues(sRawMatrix); } - /** - * 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 - * @param path the {@link Path} that can be drawn to a canvas - */ - protected void createPath(float[] data, Path 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; - default: - throw new JSApplicationIllegalArgumentException( - "Unrecognized drawing instruction " + type); - } - } - } - protected @Nullable Path getClipPath(Canvas canvas, Paint paint) { if (mClipPath != null) { VirtualNode node = getSvgShadowNode().getDefinedClipPath(mClipPath); diff --git a/ios/Text/RNSVGBezierTransformer.h b/ios/Text/RNSVGBezierTransformer.h index bf7e2872..f8995577 100644 --- a/ios/Text/RNSVGBezierTransformer.h +++ b/ios/Text/RNSVGBezierTransformer.h @@ -11,10 +11,9 @@ @interface RNSVGBezierTransformer : NSObject -+ (BOOL) hasReachedEnd:(CGAffineTransform)transform; -+ (BOOL) hasReachedStart:(CGAffineTransform) transform; - - (instancetype)initWithBezierCurvesAndStartOffset:(NSArray *)bezierCurves startOffset:(CGFloat)startOffset; - (CGAffineTransform)getTransformAtDistance:(CGFloat)distance; +- (BOOL)hasReachedEnd; +- (BOOL)hasReachedStart; @end diff --git a/ios/Text/RNSVGBezierTransformer.m b/ios/Text/RNSVGBezierTransformer.m index 595b3297..d8efd142 100644 --- a/ios/Text/RNSVGBezierTransformer.m +++ b/ios/Text/RNSVGBezierTransformer.m @@ -26,6 +26,7 @@ CGPoint _P2; CGPoint _P3; BOOL _reachedEnd; + BOOL _reachedStart; } - (instancetype)initWithBezierCurvesAndStartOffset:(NSArray *)bezierCurves startOffset:(CGFloat)startOffset @@ -39,24 +40,6 @@ return self; } -static CGAffineTransform getReachedEndTansform() { - return CGAffineTransformMakeScale(0, 1); -} - -static CGAffineTransform getUnreadedTransform() { - return CGAffineTransformMakeScale(1, 0); -} - -+ (BOOL) hasReachedEnd:(CGAffineTransform)transform -{ - return transform.a == 0; -} - -+ (BOOL) hasReachedStart:(CGAffineTransform) transform -{ - return transform.d == 0; -} - static CGFloat calculateBezier(CGFloat t, CGFloat P0, CGFloat P1, CGFloat P2, CGFloat P3) { return (1-t)*(1-t)*(1-t)*P0+3*(1-t)*(1-t)*t*P1+3*(1-t)*t*t*P2+t*t*t*P3; } @@ -124,10 +107,9 @@ static CGFloat calculateDistance(CGPoint a, CGPoint b) { - (CGAffineTransform)getTransformAtDistance:(CGFloat)distance { distance += _startOffset; - if (_reachedEnd) { - return getReachedEndTansform(); - } else if (distance < 0) { - return getUnreadedTransform(); + _reachedStart = distance >= 0; + if (_reachedEnd || !_reachedStart) { + return CGAffineTransformIdentity; } CGFloat offset = [self offsetAtDistance:distance - _lastRecord @@ -142,7 +124,7 @@ static CGFloat calculateDistance(CGPoint a, CGPoint b) { return CGAffineTransformRotate(CGAffineTransformMakeTranslation(glyphPoint.x, glyphPoint.y), [self angleAtOffset:offset]); } else if (_bezierCurves.count == _currentBezierIndex) { _reachedEnd = YES; - return getReachedEndTansform(); + return CGAffineTransformIdentity; } else { _lastOffset = 0; _lastPoint = _P0 = _P3; @@ -152,4 +134,14 @@ static CGFloat calculateDistance(CGPoint a, CGPoint b) { } } +- (BOOL)hasReachedEnd +{ + return _reachedEnd; +} + +- (BOOL)hasReachedStart +{ + return _reachedStart; +} + @end