From 203e53b6acb4456e641b9579de6a7cbb43442fac Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 3 Oct 2019 19:02:16 +0300 Subject: [PATCH] feat: initial implementation of isPointInFill --- .../horcrux/svg/RNSVGRenderableManager.java | 76 ++++++++++++++++++ .../java/com/horcrux/svg/RenderableView.java | 6 ++ .../horcrux/svg/RenderableViewManager.java | 79 ++++++++++++++++++- .../main/java/com/horcrux/svg/SvgPackage.java | 5 +- ios/ViewManagers/RNSVGRenderableManager.m | 52 ++++++++++++ src/elements/Shape.tsx | 70 +++++++++++++++- 6 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java diff --git a/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java new file mode 100644 index 00000000..ca42e2be --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java @@ -0,0 +1,76 @@ +/* + * 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 com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.UiThreadUtil; + +import javax.annotation.Nonnull; + +class RNSVGRenderableManager extends ReactContextBaseJavaModule { + RNSVGRenderableManager(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Nonnull + @Override + public String getName() { + return "RNSVGRenderableManager"; + } + + private static void isPointInFill(final int tag, final float[] src, final Callback successCallback, final int attempt) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); + + if (svg == null) { + if (attempt < 1) { + RenderableViewManager.runWhenViewIsAvailable(tag, new Runnable() { + @Override + public void run() { + RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); + if (svg == null) { // Should never happen + successCallback.invoke(false); + return; + } + isPointInFill(tag, src, successCallback, attempt + 1); + } + }); + } else { + successCallback.invoke(false); + } + return; + } else { + float scale = svg.mScale; + src[0] *= scale; + src[1] *= scale; + int i = svg.hitTest(src); + successCallback.invoke(i != -1); + } + } + } + ); + } + + @SuppressWarnings("unused") + @ReactMethod + public void isPointInFill(int tag, ReadableMap options, Callback successCallback) { + float x = (float)options.getDouble("x"); + float y = (float)options.getDouble("y"); + float[] src = new float[] { x, y }; + isPointInFill(tag, src, successCallback, 0); + } +} diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index b405086b..72d2ce8b 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -94,6 +94,12 @@ abstract public class RenderableView extends VirtualView { private static final Pattern regex = Pattern.compile("[0-9.-]+"); + @Override + public void setId(int id) { + super.setId(id); + RenderableViewManager.setRenderableView(id, this); + } + @ReactProp(name = "vectorEffect") public void setVectorEffect(int vectorEffect) { this.vectorEffect = vectorEffect; diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 4c46282f..c2831820 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -10,6 +10,7 @@ package com.horcrux.svg; import android.graphics.Matrix; +import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; @@ -41,7 +42,57 @@ import static com.facebook.react.uimanager.MatrixMathHelper.v3Cross; import static com.facebook.react.uimanager.MatrixMathHelper.v3Dot; import static com.facebook.react.uimanager.MatrixMathHelper.v3Length; import static com.facebook.react.uimanager.MatrixMathHelper.v3Normalize; -import static com.facebook.react.uimanager.ViewProps.*; +import static com.facebook.react.uimanager.ViewProps.ALIGN_CONTENT; +import static com.facebook.react.uimanager.ViewProps.ALIGN_ITEMS; +import static com.facebook.react.uimanager.ViewProps.ALIGN_SELF; +import static com.facebook.react.uimanager.ViewProps.BORDER_BOTTOM_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_END_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_LEFT_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_RIGHT_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_START_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_TOP_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BORDER_WIDTH; +import static com.facebook.react.uimanager.ViewProps.BOTTOM; +import static com.facebook.react.uimanager.ViewProps.COLLAPSABLE; +import static com.facebook.react.uimanager.ViewProps.DISPLAY; +import static com.facebook.react.uimanager.ViewProps.END; +import static com.facebook.react.uimanager.ViewProps.FLEX; +import static com.facebook.react.uimanager.ViewProps.FLEX_BASIS; +import static com.facebook.react.uimanager.ViewProps.FLEX_DIRECTION; +import static com.facebook.react.uimanager.ViewProps.FLEX_GROW; +import static com.facebook.react.uimanager.ViewProps.FLEX_SHRINK; +import static com.facebook.react.uimanager.ViewProps.FLEX_WRAP; +import static com.facebook.react.uimanager.ViewProps.HEIGHT; +import static com.facebook.react.uimanager.ViewProps.JUSTIFY_CONTENT; +import static com.facebook.react.uimanager.ViewProps.LEFT; +import static com.facebook.react.uimanager.ViewProps.MARGIN; +import static com.facebook.react.uimanager.ViewProps.MARGIN_BOTTOM; +import static com.facebook.react.uimanager.ViewProps.MARGIN_END; +import static com.facebook.react.uimanager.ViewProps.MARGIN_HORIZONTAL; +import static com.facebook.react.uimanager.ViewProps.MARGIN_LEFT; +import static com.facebook.react.uimanager.ViewProps.MARGIN_RIGHT; +import static com.facebook.react.uimanager.ViewProps.MARGIN_START; +import static com.facebook.react.uimanager.ViewProps.MARGIN_TOP; +import static com.facebook.react.uimanager.ViewProps.MARGIN_VERTICAL; +import static com.facebook.react.uimanager.ViewProps.MAX_HEIGHT; +import static com.facebook.react.uimanager.ViewProps.MAX_WIDTH; +import static com.facebook.react.uimanager.ViewProps.MIN_HEIGHT; +import static com.facebook.react.uimanager.ViewProps.MIN_WIDTH; +import static com.facebook.react.uimanager.ViewProps.OVERFLOW; +import static com.facebook.react.uimanager.ViewProps.PADDING; +import static com.facebook.react.uimanager.ViewProps.PADDING_BOTTOM; +import static com.facebook.react.uimanager.ViewProps.PADDING_END; +import static com.facebook.react.uimanager.ViewProps.PADDING_HORIZONTAL; +import static com.facebook.react.uimanager.ViewProps.PADDING_LEFT; +import static com.facebook.react.uimanager.ViewProps.PADDING_RIGHT; +import static com.facebook.react.uimanager.ViewProps.PADDING_START; +import static com.facebook.react.uimanager.ViewProps.PADDING_TOP; +import static com.facebook.react.uimanager.ViewProps.PADDING_VERTICAL; +import static com.facebook.react.uimanager.ViewProps.POSITION; +import static com.facebook.react.uimanager.ViewProps.RIGHT; +import static com.facebook.react.uimanager.ViewProps.START; +import static com.facebook.react.uimanager.ViewProps.TOP; +import static com.facebook.react.uimanager.ViewProps.WIDTH; import static com.horcrux.svg.RenderableView.CAP_ROUND; import static com.horcrux.svg.RenderableView.FILL_RULE_NONZERO; import static com.horcrux.svg.RenderableView.JOIN_ROUND; @@ -1250,4 +1301,30 @@ class RenderableViewManager extends ViewGroupManager { throw new IllegalStateException("Unexpected type " + svgClass.toString()); } } + + private static final SparseArray mTagToRenderableView = new SparseArray<>(); + private static final SparseArray mTagToRunnable = new SparseArray<>(); + + static void setRenderableView(int tag, RenderableView svg) { + mTagToRenderableView.put(tag, svg); + Runnable task = mTagToRunnable.get(tag); + if (task != null) { + task.run(); + mTagToRunnable.delete(tag); + } + } + + static void runWhenViewIsAvailable(int tag, Runnable task) { + mTagToRunnable.put(tag, task); + } + + static @Nullable RenderableView getRenderableViewByTag(int tag) { + return mTagToRenderableView.get(tag); + } + + @Override + public void onDropViewInstance(@Nonnull VirtualView view) { + super.onDropViewInstance(view); + mTagToRenderableView.remove(view.getId()); + } } diff --git a/android/src/main/java/com/horcrux/svg/SvgPackage.java b/android/src/main/java/com/horcrux/svg/SvgPackage.java index 833b549a..51956358 100644 --- a/android/src/main/java/com/horcrux/svg/SvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/SvgPackage.java @@ -54,7 +54,10 @@ public class SvgPackage implements ReactPackage { @Nonnull @Override public List createNativeModules(@Nonnull ReactApplicationContext reactContext) { - return Collections.singletonList(new SvgViewModule(reactContext)); + return Arrays.asList( + new SvgViewModule(reactContext), + new RNSVGRenderableManager(reactContext) + ); } @SuppressWarnings("unused") diff --git a/ios/ViewManagers/RNSVGRenderableManager.m b/ios/ViewManagers/RNSVGRenderableManager.m index fae93de7..22185617 100644 --- a/ios/ViewManagers/RNSVGRenderableManager.m +++ b/ios/ViewManagers/RNSVGRenderableManager.m @@ -6,6 +6,9 @@ * LICENSE file in the root directory of this source tree. */ +#import +#import +#import #import "RNSVGRenderableManager.h" #import "RCTConvert+RNSVG.h" @@ -34,4 +37,53 @@ RCT_EXPORT_VIEW_PROPERTY(strokeMiterlimit, CGFloat) RCT_EXPORT_VIEW_PROPERTY(vectorEffect, int) RCT_EXPORT_VIEW_PROPERTY(propList, NSArray) +- (void)isPointInFill:(nonnull NSNumber *)reactTag point:(CGPoint)point callback:(RCTResponseSenderBlock)callback attempt:(int)attempt { + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + __kindof UIView *view = viewRegistry[reactTag]; + UIView *target; + if (!view) { + if (attempt < 1) { + void (^retryBlock)(void) = ^{ + [self isPointInFill:reactTag point:point callback:callback attempt:(attempt + 1)]; + }; + RCTExecuteOnUIManagerQueue(retryBlock); + } else { + callback(@[[NSNumber numberWithBool:false]]); + } + return; + } + if ([view isKindOfClass:[RNSVGRenderable class]]) { + RNSVGRenderable *svg = view; + target = [svg hitTest:point withEvent:nil]; + BOOL hit = target != nil; + callback(@[[NSNumber numberWithBool:hit]]); + } else { + RCTLogError(@"Invalid svg returned from registry, expecting RNSVGRenderable, got: %@", view); + callback(@[[NSNumber numberWithBool:false]]); + } + }]; +} + +RCT_EXPORT_METHOD(isPointInFill:(nonnull NSNumber *)reactTag options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) +{ + if (options == nil) { + RCTLogError(@"Invalid options given to isPointInFill, got: %@", options); + callback(@[[NSNumber numberWithBool:false]]); + return; + } + id xo = [options objectForKey:@"x"]; + id yo = [options objectForKey:@"y"]; + if (![xo isKindOfClass:NSNumber.class] || + ![yo isKindOfClass:NSNumber.class]) { + RCTLogError(@"Invalid x or y given to isPointInFill"); + callback(@[[NSNumber numberWithBool:false]]); + return; + } + CGFloat x = (CGFloat)[xo floatValue]; + CGFloat y = (CGFloat)[yo floatValue]; + CGPoint point = CGPointMake(x, y); + [self isPointInFill:reactTag point:point callback:callback attempt:0]; +} + + @end diff --git a/src/elements/Shape.tsx b/src/elements/Shape.tsx index 24fa8be2..d95674dd 100644 --- a/src/elements/Shape.tsx +++ b/src/elements/Shape.tsx @@ -1,13 +1,32 @@ import { Component } from 'react'; import SvgTouchableMixin from '../lib/SvgTouchableMixin'; -import { NativeMethodsMixinStatic } from 'react-native'; +import { + NativeModules, + findNodeHandle, + NativeMethodsMixinStatic, +} from 'react-native'; import { TransformProps } from '../lib/extract/types'; +const RNSVGRenderableManager = NativeModules.RNSVGRenderableManager; const { touchableGetInitialState } = SvgTouchableMixin; const touchKeys = Object.keys(SvgTouchableMixin); const touchVals = touchKeys.map(key => SvgTouchableMixin[key]); const numTouchKeys = touchKeys.length; +interface SVGBoundingBoxOptions { + fill: boolean; + stroke: boolean; + markers: boolean; + clipped: boolean; +} + +interface DOMPointInit { + x?: number; + y?: number; + z?: number; + w?: number; +} + export default class Shape

extends Component

{ [x: string]: unknown; root: (Shape

& NativeMethodsMixinStatic) | null = null; @@ -36,4 +55,53 @@ export default class Shape

extends Component

{ ) => { this.root && this.root.setNativeProps(props); }; + getBBox = (callback: () => void, options?: SVGBoundingBoxOptions) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.getBBox(handle, options, callback); // TODO + }; + getCTM = (callback: () => void) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.getCTM(handle, callback); // TODO + }; + getScreenCTM = (callback: () => void) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.getScreenCTM(handle, callback); // TODO + }; + isPointInFill = (callback: () => void, options: DOMPointInit) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.isPointInFill(handle, options, callback); + }; + isPointInStroke = (callback: () => void, options: DOMPointInit) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.isPointInStroke(handle, options, callback); // TODO + }; + getTotalLength = (callback: () => void) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.getTotalLength(handle, callback); // TODO + }; + getPointAtLength = (callback: () => void, options: Object) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root as Component); + RNSVGRenderableManager.getPointAtLength(handle, options, callback); // TODO + }; }