From fa07f6ef8a340f855a5db720c70ce0bd5d10839d Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 8 Feb 2019 01:16:38 +0200 Subject: [PATCH 1/2] [ios] Workaround CGPathContainsPoint bug in iOS 12 https://github.com/react-native-community/react-native-svg/issues/794#issuecomment-461524902 https://stackoverflow.com/questions/54026261/cgpathcontainspoint-broken-in-ios-12-is-a-workaround-possible --- ios/Elements/RNSVGGroup.m | 12 ++++++++++-- ios/Elements/RNSVGImage.m | 1 + ios/RNSVGNode.h | 2 ++ ios/RNSVGNode.m | 1 + ios/RNSVGRenderable.m | 30 ++++++++++++++++-------------- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index 803a209a..fc75689a 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -74,9 +74,13 @@ return YES; }]; - [self setHitArea:[self getPath:context]]; + CGPathRef path = [self getPath:context]; + [self setHitArea:path]; if (!CGRectEqualToRect(bounds, CGRectNull)) { self.clientRect = bounds; + const CGRect fillBounds = CGPathGetBoundingBox(path); + const CGRect strokeBounds = CGPathGetBoundingBox(self.strokePath); + self.pathBounds = CGRectUnion(fillBounds, strokeBounds); CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); @@ -150,6 +154,10 @@ CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); transformed = CGPointApplyAffineTransform(transformed, self.invTransform); + if (!CGRectContainsPoint(self.pathBounds, transformed)) { + return nil; + } + if (self.clipPath) { RNSVGClipPath *clipNode = (RNSVGClipPath*)[self.svgView getDefinedClipPath:self.clipPath]; if ([clipNode isSimpleClipPath]) { @@ -169,7 +177,7 @@ NSPredicate *const anyActive = [NSPredicate predicateWithFormat:@"active == TRUE"]; NSArray *const filtered = [self.subviews filteredArrayUsingPredicate:anyActive]; if ([filtered count] != 0) { - return [filtered.firstObject hitTest:transformed withEvent:event]; + return [filtered.lastObject hitTest:transformed withEvent:event]; } } diff --git a/ios/Elements/RNSVGImage.m b/ios/Elements/RNSVGImage.m index 1f4c80ba..3d8dc444 100644 --- a/ios/Elements/RNSVGImage.m +++ b/ios/Elements/RNSVGImage.m @@ -117,6 +117,7 @@ CGPathRef hitAreaPath = CGPathCreateWithRect(hitArea, nil); [self setHitArea:hitAreaPath]; CGPathRelease(hitAreaPath); + self.pathBounds = hitArea; // apply viewBox transform on Image render. CGRect imageBounds = CGRectMake(0, 0, _imageSize.width, _imageSize.height); diff --git a/ios/RNSVGNode.h b/ios/RNSVGNode.h index 5ff803bb..bb94d891 100644 --- a/ios/RNSVGNode.h +++ b/ios/RNSVGNode.h @@ -40,7 +40,9 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, assign) BOOL dirty; @property (nonatomic, assign) BOOL merging; @property (nonatomic, assign) CGPathRef path; +@property (nonatomic, assign) CGPathRef strokePath; @property (nonatomic, assign) CGRect clientRect; +@property (nonatomic, assign) CGRect pathBounds; @property (nonatomic, copy) RCTDirectEventBlock onLayout; diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index 0086e58d..7d491e21 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -550,6 +550,7 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; - (void)dealloc { CGPathRelease(_cachedClipPath); + CGPathRelease(_strokePath); CGImageRelease(_clipMask); CGPathRelease(_path); _clipMask = nil; diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 37741ffc..d8067aae 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -18,7 +18,6 @@ NSArray *_attributeList; NSArray *_sourceStrokeDashArray; CGFloat *_strokeDashArrayData; - CGPathRef _strokePath; CGPathRef _srcHitPath; CGPathRef _hitArea; } @@ -157,7 +156,6 @@ - (void)dealloc { CGPathRelease(_hitArea); - CGPathRelease(_strokePath); _sourceStrokeDashArray = nil; if (_strokeDashArrayData) { free(_strokeDashArrayData); @@ -295,15 +293,15 @@ UInt32 saturate(CGFloat value) { self.path = CGPathRetain(path); } [self setHitArea:path]; + const CGRect fillBounds = CGPathGetBoundingBox(path); + const CGRect strokeBounds = CGPathGetBoundingBox(self.strokePath); + self.pathBounds = CGRectUnion(fillBounds, strokeBounds); } - - const CGRect fillBounds = CGPathGetBoundingBox(path); - const CGRect strokeBounds = CGPathGetBoundingBox(_strokePath); - const CGRect pathBounding = CGRectUnion(fillBounds, strokeBounds); + const CGRect pathBounds = self.pathBounds; CGAffineTransform current = CGContextGetCTM(context); CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM); - CGRect clientRect = CGRectApplyAffineTransform(pathBounding, svgToClientTransform); + CGRect clientRect = CGRectApplyAffineTransform(pathBounds, svgToClientTransform); self.clientRect = clientRect; @@ -312,7 +310,7 @@ UInt32 saturate(CGFloat value) { CGAffineTransform matrix = CGAffineTransformConcat(transform, vbmatrix); CGRect bounds = CGRectMake(0, 0, CGRectGetWidth(clientRect), CGRectGetHeight(clientRect)); - CGPoint mid = CGPointMake(CGRectGetMidX(pathBounding), CGRectGetMidY(pathBounding)); + CGPoint mid = CGPointMake(CGRectGetMidX(pathBounds), CGRectGetMidY(pathBounds)); CGPoint center = CGPointApplyAffineTransform(mid, matrix); self.bounds = bounds; @@ -352,7 +350,7 @@ UInt32 saturate(CGFloat value) { [self.fill paint:context opacity:self.fillOpacity painter:[self.svgView getDefinedPainter:self.fill.brushRef] - bounds:pathBounding + bounds:pathBounds ]; CGContextRestoreGState(context); @@ -409,7 +407,7 @@ UInt32 saturate(CGFloat value) { [self.stroke paint:context opacity:self.strokeOpacity painter:[self.svgView getDefinedPainter:self.stroke.brushRef] - bounds:pathBounding + bounds:pathBounds ]; return; } @@ -426,13 +424,13 @@ UInt32 saturate(CGFloat value) { } _srcHitPath = path; CGPathRelease(_hitArea); - CGPathRelease(_strokePath); + CGPathRelease(self.strokePath); _hitArea = CGPathCreateCopy(path); - _strokePath = nil; + self.strokePath = nil; if (self.stroke && self.strokeWidth) { // Add stroke to hitArea CGFloat width = [self relativeOnOther:self.strokeWidth]; - _strokePath = CGPathRetain(CFAutorelease(CGPathCreateCopyByStrokingPath(path, nil, width, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit))); + self.strokePath = CGPathRetain(CFAutorelease(CGPathCreateCopyByStrokingPath(path, nil, width, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit))); // TODO add dashing // CGPathCreateCopyByDashingPath(CGPathRef _Nullable path, const CGAffineTransform * _Nullable transform, CGFloat phase, const CGFloat * _Nullable lengths, size_t count) } @@ -460,9 +458,13 @@ UInt32 saturate(CGFloat value) { CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); transformed = CGPointApplyAffineTransform(transformed, self.invTransform); + if (!CGRectContainsPoint(self.pathBounds, transformed)) { + return nil; + } + BOOL evenodd = self.fillRule == kRNSVGCGFCRuleEvenodd; if (!CGPathContainsPoint(_hitArea, nil, transformed, evenodd) && - !CGPathContainsPoint(_strokePath, nil, transformed, NO)) { + !CGPathContainsPoint(self.strokePath, nil, transformed, NO)) { return nil; } From abb17bc7f0985c0b5eaa85ead7351e5c74a731f8 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 8 Feb 2019 03:19:21 +0200 Subject: [PATCH 2/2] Fix re-rendering of emoji when path data is cached, closes #927 --- .../main/java/com/horcrux/svg/TSpanView.java | 33 +++++++++++++++---- ios/Text/RNSVGTSpan.m | 26 +++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/TSpanView.java b/android/src/main/java/com/horcrux/svg/TSpanView.java index 04c95760..32248517 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanView.java +++ b/android/src/main/java/com/horcrux/svg/TSpanView.java @@ -28,6 +28,8 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.views.text.ReactFontManager; +import java.util.ArrayList; + import javax.annotation.Nullable; import static android.graphics.Matrix.MTRANS_X; @@ -47,6 +49,8 @@ class TSpanView extends TextView { @Nullable String mContent; private TextPathView textPath; + ArrayList emoji = new ArrayList<>(); + ArrayList emojiTransforms = new ArrayList<>(); public TSpanView(ReactContext reactContext) { super(reactContext); @@ -61,6 +65,20 @@ class TSpanView extends TextView { @Override void draw(Canvas canvas, Paint paint, float opacity) { if (mContent != null) { + int numEmoji = emoji.size(); + if (numEmoji > 0) { + GlyphContext gc = getTextRootGlyphContext(); + FontData font = gc.getFont(); + applyTextPropertiesToPaint(paint, font); + for (int i = 0; i < numEmoji; i++) { + String current = emoji.get(i); + Matrix mid = emojiTransforms.get(i); + canvas.save(); + canvas.concat(mid); + canvas.drawText(current, 0, 0, paint); + canvas.restore(); + } + } drawPath(canvas, paint, opacity); } else { clip(canvas, paint); @@ -676,6 +694,9 @@ class TSpanView extends TextView { final float[] midPointMatrixData = new float[9]; final float[] endPointMatrixData = new float[9]; + emoji.clear(); + emojiTransforms.clear(); + for (int index = 0; index < length; index++) { char currentChar = chars[index]; String current = String.valueOf(currentChar); @@ -855,12 +876,12 @@ class TSpanView extends TextView { glyph.computeBounds(bounds, true); float width = bounds.width(); if (width == 0) { // Render unicode emoji - mid.getValues(midPointMatrixData); - double midX = midPointMatrixData[MTRANS_X]; - double midY = midPointMatrixData[MTRANS_Y]; - canvas.rotate((float) r, (float)midX, (float)midY); - canvas.drawText(current, (float)midX, (float)midY, paint); - canvas.rotate((float) -r, (float)midX, (float)midY); + canvas.save(); + canvas.concat(mid); + emoji.add(current); + emojiTransforms.add(new Matrix(mid)); + canvas.drawText(current, 0, 0, paint); + canvas.restore(); } else { glyph.transform(mid); path.addPath(glyph); diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index c9171aae..70b11adc 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -23,6 +23,8 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; NSArray *lines; NSUInteger lineCount; BOOL isClosed; + NSMutableArray *emoji; + NSMutableArray *emojiTransform; } - (id)init @@ -33,6 +35,9 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; RNSVGTSpan_separators = [NSCharacterSet whitespaceCharacterSet]; } + emoji = [NSMutableArray arrayWithCapacity:0]; + emojiTransform = [NSMutableArray arrayWithCapacity:0]; + return self; } @@ -48,6 +53,21 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect { if (self.content) { + if (self.path) { + NSUInteger count = [emoji count]; + RNSVGGlyphContext* gc = [self.textRoot getGlyphContext]; + CGFloat fontSize = [gc getFontSize]; + for (NSUInteger i = 0; i < count; i++) { + UILabel *label = [emoji objectAtIndex:i]; + NSValue *transformValue = [emojiTransform objectAtIndex:i]; + CGAffineTransform transform = [transformValue CGAffineTransformValue]; + CGContextConcatCTM(context, transform); + CGContextTranslateCTM(context, 0, -fontSize); + [label.layer renderInContext:context]; + CGContextTranslateCTM(context, 0, fontSize); + CGContextConcatCTM(context, CGAffineTransformInvert(transform)); + } + } [self renderPathTo:context rect:rect]; } else { [self clip:context]; @@ -658,6 +678,9 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; } } + [emoji removeAllObjects]; + [emojiTransform removeAllObjects]; + CFArrayRef runs = CTLineGetGlyphRuns(line); CFIndex runEnd = CFArrayGetCount(runs); for (CFIndex r = 0; r < runEnd; r++) { @@ -829,6 +852,9 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; [label.layer renderInContext:context]; CGContextTranslateCTM(context, 0, fontSize); CGContextConcatCTM(context, CGAffineTransformInvert(transform)); + + [emoji addObject:label]; + [emojiTransform addObject:[NSValue valueWithCGAffineTransform:transform]]; } else { transform = CGAffineTransformScale(transform, 1.0, -1.0); CGPathAddPath(path, &transform, glyphPath);