Skip to content

Commit 0460512

Browse files
authored
Restoring pointer events box_none behavior on Android (#1808)
This PR is a bug fix and makes the pointer-events box-none work again after react-native 66 changes. It changes a bit how it should be used as I'll describe below. This solves #1807 . I created a getHitSlopRect in the RenderableView that responds with a Rect that make the hit test detection fail on the react-native forcing it to run reactTagForTouch like it used to in previous versions. This PR impacts pointer events box-none usage. This is an important change for me because it enables the usage of overlapping SVG keeping only the painted area as touchable elements, and not the whole SVG box.
1 parent b3d2b76 commit 0460512

File tree

3 files changed

+134
-6
lines changed

3 files changed

+134
-6
lines changed

TestsExample/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ColorTest from './src/ColorTest';
55
import Test1718 from './src/Test1718';
66
import Test1813 from './src/Test1813';
77
import Test1845 from './src/Test1845';
8+
import PointerEventsBoxNone from './src/PointerEventsBoxNone';
89

910
export default function App() {
1011
return <ColorTest />;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as React from 'react';
2+
3+
import {Alert, Button, Platform, Text, View} from 'react-native';
4+
import Svg, {Path} from 'react-native-svg';
5+
6+
/*
7+
On iOS each SVG has as it's touchable area it's drawings, ignoring the box where the element is rendered.
8+
9+
| |
10+
| ***** |
11+
| ***** |
12+
| |
13+
SVG depicted above is a box of 7x4 (without a background) with a colored rectangle inside occupying 5x2
14+
15+
On iOS only the area with the actual drawing reactangle with 5x2 can be tapped.
16+
17+
However on Android things are different. For React Native on Android the whole SVG element will be touchable
18+
regardless of having or not a visible drawing in it.
19+
20+
This is not a major issue for plain SVG like icons, however if you are trying to position one SVG on top of another
21+
things won't work as expected.
22+
23+
In order to make Android behave like iOS we need to set a pointerEvents property to the SVG element and to the drawing
24+
itself you are trying to make it actionable. This will a allow to have a behavior equal to the one iOS has by default.
25+
26+
In the Demo bellow try to touch the blue region under the red one. Use the toggle button to turn to box-none on and
27+
off.
28+
29+
TLDR; Use pointerEvents={'box-none'} to make only the drawable area clickable
30+
31+
Keep in mind that the box-none value does not exist on iOS therefore you have to check the platform before using it.
32+
Like in the demo below.
33+
*/
34+
35+
export default function PointerEventsBoxNone() {
36+
const [boxNone, setBoxNone] = React.useState(false);
37+
38+
const pointerEvents = Platform.OS === 'ios' || !boxNone ? 'auto' : 'box-none';
39+
40+
return (
41+
<View style={{backgroundColor: '#fff'}}>
42+
<View
43+
style={{
44+
position: 'absolute',
45+
top: 150,
46+
left: 75,
47+
transform: [{scale: 2}],
48+
}}
49+
>
50+
<Text style={{position: 'absolute', top: -25, left: 20, fontSize: 10}}>
51+
Try to touch the blue shape
52+
</Text>
53+
54+
{/* BLUE SECTION */}
55+
<Svg
56+
width={180}
57+
height={115}
58+
fill="none"
59+
style={{
60+
position: 'absolute',
61+
top: 0,
62+
left: 0,
63+
}}
64+
pointerEvents={pointerEvents}
65+
>
66+
<Path
67+
opacity={1}
68+
pointerEvents={pointerEvents}
69+
onPress={() => Alert.alert('TAPPED THE BLUE SECTION')}
70+
d="M178.829 50.333c-24.775-17.185-77.2-39.96-102.06-49.895a1.96 1.96 0 0 0-2.017.348L2.169 63C.045 64.77-.147 65.888.07 66.352c.05.109.128.19.199.287C5.078 73.19 33.718 86.532 40.669 91c7 4.5 37.5 23 42.5 23.5 4 .4 18-9.167 24.5-14l71.102-46.917c1.162-.767 1.202-2.457.058-3.25Z"
71+
fill="#2E90FA"
72+
/>
73+
</Svg>
74+
75+
{/* RED SECTION */}
76+
<Svg
77+
width={183}
78+
height={74}
79+
fill="none"
80+
pointerEvents={pointerEvents}
81+
style={{top: 10, transform: [{scale: 1.5}]}}
82+
>
83+
<Path
84+
opacity={1}
85+
pointerEvents={pointerEvents}
86+
onPress={() => Alert.alert('TAPPED THE RED SECTION')}
87+
d="M90.526 57.812c-10.077-4.064-37.05-11.661-51.738-15.6a1.968 1.968 0 0 0-1.506.203L3.964 61.705c-1.51.874-1.256 3.121.418 3.618C16.216 68.833 35.162 74 41.5 74c6.637 0 33.328-8.214 48.82-13.285 1.545-.506 1.715-2.295.206-2.903ZM96 48c-6.411 0-26.778-6.079-39.282-10.117-1.641-.53-1.861-2.744-.368-3.606l31.02-17.913a1.885 1.885 0 0 1 1.344-.221c7.337 1.601 34.439 10.85 51.556 16.842 1.822.638 1.769 3.232-.075 3.802C126.617 40.981 103.736 48 96 48ZM146.5 23c5.313.332 20.18-3.677 30.418-6.765 1.825-.55 1.874-3.08.081-3.729L143.098.216a2 2 0 0 0-1.212-.047l-32.895 9.046c-1.918.528-1.971 3.215-.07 3.804 13.037 4.04 32.169 9.643 37.579 9.981Z"
88+
fill="#F63D68"
89+
/>
90+
</Svg>
91+
</View>
92+
93+
<View
94+
style={{
95+
position: 'absolute',
96+
top: 370,
97+
left: '50%',
98+
width: 150,
99+
transform: [{translateX: -75}],
100+
}}
101+
>
102+
<Button
103+
title="Toggle box-none property"
104+
onPress={() => setBoxNone(!boxNone)}
105+
/>
106+
<Text style={{color: '#fff', fontSize: 15, paddingTop: 10}}>
107+
Box none is{' '}
108+
<Text style={{color: '#f00'}}>{boxNone ? 'ON' : 'OFF'}</Text>
109+
</Text>
110+
</View>
111+
</View>
112+
);
113+
}

android/src/main/java/com/horcrux/svg/RenderableView.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@
2828
import com.facebook.react.bridge.ReadableMap;
2929
import com.facebook.react.bridge.ReadableType;
3030
import com.facebook.react.uimanager.PointerEvents;
31+
import com.facebook.react.touch.ReactHitSlopView;
32+
3133
import java.lang.reflect.Field;
3234
import java.util.ArrayList;
3335
import java.util.regex.Matcher;
3436
import java.util.regex.Pattern;
3537
import javax.annotation.Nullable;
3638

3739
@SuppressWarnings({"WeakerAccess", "RedundantSuppression"})
38-
public abstract class RenderableView extends VirtualView {
40+
abstract public class RenderableView extends VirtualView implements ReactHitSlopView {
3941

4042
RenderableView(ReactContext reactContext) {
4143
super(reactContext);
@@ -94,11 +96,23 @@ public abstract class RenderableView extends VirtualView {
9496

9597
private static final Pattern regex = Pattern.compile("[0-9.-]+");
9698

97-
@Override
98-
public void setId(int id) {
99-
super.setId(id);
100-
RenderableViewManager.setRenderableView(id, this);
101-
}
99+
@Nullable
100+
public Rect getHitSlopRect() {
101+
/*
102+
* In order to make the isTouchPointInView fail we need to return a very improbable Rect for the View
103+
* This way an SVG with box_none carrying its last descendent with box_none will have the expected behavior of just having events on the actual painted area
104+
*/
105+
if (mPointerEvents == PointerEvents.BOX_NONE) {
106+
return new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
107+
}
108+
return null;
109+
}
110+
111+
@Override
112+
public void setId(int id) {
113+
super.setId(id);
114+
RenderableViewManager.setRenderableView(id, this);
115+
}
102116

103117
public void setVectorEffect(int vectorEffect) {
104118
this.vectorEffect = vectorEffect;

0 commit comments

Comments
 (0)