feat: introduce hitSlop prop (#2407)

# Summary

Explain the **motivation** for making this change: here are some points
to help you:

* What issues does the pull request solve? Please tag them so that they
will get automatically closed once the PR is merged
* What is the feature? (if applicable)
* How did you implement the solution?
* What areas of the library does it impact?

## Test Plan

Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes UI.

### What's required for testing (prerequisites)?

### What are the steps to reproduce (after prerequisites)?

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |         |
| MacOS   |          |
| Android |          |
| Web     |          |
This commit is contained in:
Jakub Grzywacz
2024-08-19 09:11:07 +02:00
committed by GitHub
parent 7cf90f2f10
commit ca1c35caa9
13 changed files with 113 additions and 23 deletions

View File

@@ -10,8 +10,10 @@ package com.horcrux.svg;
import android.graphics.Rect;
import android.util.SparseArray;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ThemedReactContext;
@@ -285,24 +287,37 @@ class SvgViewManager extends ReactViewManager
}
@Override
public void setHitSlop(SvgView view, @Nullable ReadableMap hitSlopMap) {
public void setHitSlop(SvgView view, Dynamic hitSlop) {
// we don't call super here since its signature changed in RN 0.69 and we want backwards
// compatibility
if (hitSlopMap != null) {
view.setHitSlopRect(
new Rect(
hitSlopMap.hasKey("left")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("left"))
: 0,
hitSlopMap.hasKey("top")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("top"))
: 0,
hitSlopMap.hasKey("right")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("right"))
: 0,
hitSlopMap.hasKey("bottom")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("bottom"))
: 0));
switch (hitSlop.getType()) {
case Map:
ReadableMap hitSlopMap = hitSlop.asMap();
view.setHitSlopRect(
new Rect(
hitSlopMap.hasKey("left")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("left"))
: 0,
hitSlopMap.hasKey("top")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("top"))
: 0,
hitSlopMap.hasKey("right")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("right"))
: 0,
hitSlopMap.hasKey("bottom")
? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("bottom"))
: 0));
break;
case Number:
int hitSlopValue = (int) PixelUtil.toPixelFromDIP(hitSlop.asDouble());
view.setHitSlopRect(new Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue));
break;
default:
FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value " + hitSlop.getType());
/* falls through */
case Null:
view.setHitSlopRect(null);
break;
}
}

View File

@@ -127,7 +127,7 @@ public class RNSVGSvgViewAndroidManagerDelegate<T extends View, U extends BaseVi
mViewManager.setNeedsOffscreenAlphaCompositing(view, value == null ? false : (boolean) value);
break;
case "hitSlop":
mViewManager.setHitSlop(view, (ReadableMap) value);
mViewManager.setHitSlop(view, new DynamicFromObject(value));
break;
case "borderTopColor":
mViewManager.setBorderTopColor(view, ColorPropConverter.getColor(value, view.getContext()));

View File

@@ -49,7 +49,7 @@ public interface RNSVGSvgViewAndroidManagerInterface<T extends View> {
void setBackfaceVisibility(T view, @Nullable String value);
void setBorderStyle(T view, @Nullable String value);
void setNeedsOffscreenAlphaCompositing(T view, boolean value);
void setHitSlop(T view, @Nullable ReadableMap value);
void setHitSlop(T view, Dynamic value);
void setBorderTopColor(T view, @Nullable Integer value);
void setNextFocusLeft(T view, int value);
void setBorderTopRightRadius(T view, double value);

View File

@@ -33,6 +33,7 @@
@property (nonatomic, assign) CGAffineTransform initialCTM;
@property (nonatomic, assign) CGAffineTransform invInitialCTM;
@property (nonatomic, assign) CGAffineTransform viewBoxTransform;
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
/**
* define <ClipPath></ClipPath> content as clipPath template.

View File

@@ -317,6 +317,15 @@ using namespace facebook::react;
[self drawToContext:context withRect:[self bounds]];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [super pointInside:point withEvent:event];
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
- (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint transformed = point;
@@ -339,7 +348,8 @@ using namespace facebook::react;
return (node.responsible || (node != hitChild)) ? hitChild : self;
}
}
return nil;
BOOL isPointInside = [self pointInside:point withEvent:event];
return isPointInside ? self : nil;
}
- (NSString *)getDataURLWithBounds:(CGRect)bounds

View File

@@ -28,5 +28,20 @@ RCT_EXPORT_VIEW_PROPERTY(align, NSString)
RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS)
RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(color, tintColor, UIColor)
RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RNSVGSvgView)
{
if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) {
if (json) {
UIEdgeInsets hitSlopInsets = [RCTConvert UIEdgeInsets:json];
[view setHitTestEdgeInsets:UIEdgeInsetsMake(
-hitSlopInsets.top,
-hitSlopInsets.left,
-hitSlopInsets.bottom,
-hitSlopInsets.right)];
} else {
view.hitTestEdgeInsets = defaultView.hitTestEdgeInsets;
}
}
}
@end

View File

@@ -26,6 +26,7 @@ import Test2327 from './src/Test2327';
import Test2233 from './src/Test2233';
import Test2366 from './src/Test2366';
import Test2397 from './src/Test2397';
import Test2407 from './src/Test2407';
export default function App() {
return <ColorTest />;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import {SafeAreaView, StyleSheet} from 'react-native';
import Svg, {Rect} from 'react-native-svg';
export default () => {
return (
<SafeAreaView style={styles.container}>
<Svg
width={200}
height={200}
viewBox="0 0 200 200"
hitSlop={50}
// hitSlop={{top: 50, left: 50, right: 50, bottom: 50}}
onPress={() => console.log('press')}>
<Rect
width={200}
height={200}
x={0}
y={0}
fill="red"
onPress={() => console.log('rect press')}
/>
</Svg>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@@ -7,12 +7,12 @@ import type {
MeasureOnSuccessCallback,
NativeMethods,
StyleProp,
ViewProps,
ViewStyle,
} from 'react-native';
import { findNodeHandle, Platform, StyleSheet } from 'react-native';
import type {
extractedProps,
HitSlop,
NumberProp,
ResponderInstanceProps,
} from '../lib/extract/types';
@@ -25,6 +25,7 @@ import RNSVGSvgAndroid from '../fabric/AndroidSvgViewNativeComponent';
import RNSVGSvgIOS from '../fabric/IOSSvgViewNativeComponent';
import type { Spec } from '../fabric/NativeSvgViewModule';
import extractOpacity from '../lib/extract/extractOpacity';
import { ViewProps } from '../fabric/utils';
const styles = StyleSheet.create({
svg: {
@@ -34,7 +35,7 @@ const styles = StyleSheet.create({
});
const defaultStyle = styles.svg;
export interface SvgProps extends GProps, ViewProps {
export interface SvgProps extends GProps, ViewProps, HitSlop {
width?: NumberProp;
height?: NumberProp;
viewBox?: string;

View File

@@ -61,7 +61,7 @@ interface NativeProps extends ViewProps {
backfaceVisibility?: string;
borderStyle?: string;
needsOffscreenAlphaCompositing?: boolean;
hitSlop?: HitSlop;
hitSlop?: UnsafeMixed<HitSlop | null | number | undefined>;
borderTopColor?: ColorValue;
nextFocusLeft?: Int32;
// TODO: those props are present in the `ReactPropGroup` but are not supported

View File

@@ -6,6 +6,13 @@ import type { ViewProps } from './utils';
import type { UnsafeMixed } from './codegenUtils';
import { NumberProp } from '../lib/extract/types';
type HitSlop = Readonly<{
left?: Float;
top?: Float;
right?: Float;
bottom?: Float;
}>;
interface NativeProps extends ViewProps {
bbWidth?: UnsafeMixed<NumberProp>;
bbHeight?: UnsafeMixed<NumberProp>;
@@ -18,6 +25,7 @@ interface NativeProps extends ViewProps {
tintColor?: ColorValue;
color?: ColorValue;
pointerEvents?: string;
hitSlop?: UnsafeMixed<HitSlop | null | number | undefined>;
}
export default codegenNativeComponent<NativeProps>('RNSVGSvgView', {

View File

@@ -1,5 +1,5 @@
import type { ViewProps as VP } from 'react-native';
type ViewProps = Omit<VP, 'pointerEvents'>;
type ViewProps = Omit<VP, 'pointerEvents' | 'hitSlop'>;
export type { ViewProps };

View File

@@ -2,6 +2,7 @@ import type {
ColorValue,
GestureResponderEvent,
GestureResponderHandlers,
Insets,
LayoutChangeEvent,
TransformsStyle,
} from 'react-native';
@@ -272,6 +273,10 @@ export interface CommonPathProps
NativeProps,
AccessibilityProps {}
export interface HitSlop {
hitSlop?: Insets | number | undefined;
}
export type ResponderInstanceProps = {
touchableHandleResponderMove?: (e: GestureResponderEvent) => void;
touchableHandleResponderGrant?: (e: GestureResponderEvent) => void;