Skip to content

Commit e0f2293

Browse files
authored
Fix nested buttons on Android and buttons becoming unresponsive in a list (#2187)
## Description Should fix #2160 This PR changes the logic of responder by splitting it in two: one for sound and one for touch exclusivity. Button can become the touch responder when `setPressed` method is called with `pressed` set to `true` and it also is the touch responder. The sound responder is freed in the `performClick` method - after that point `setPressed` should only be called with `pressed` set to `false` for existing pointers. This prevents the sound effect from being played more than one time when pressing on nested buttons. It also adds more ways to free the responder: it reverts a change from a while back that frees responder on gesture end and frees the responder when button holding it receives `POINTER_CANCEL` event. Because of that, the responder should always be freed correctly, preventing the buttons from becoming unresponsive. ## Test plan Tested on the example app (using the new and existing example) and on the code from the issue.
1 parent 1e6de08 commit e0f2293

File tree

4 files changed

+124
-15
lines changed

4 files changed

+124
-15
lines changed

android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
167167
* [com.swmansion.gesturehandler.NativeViewGestureHandler.onHandle] */
168168
@SuppressLint("ClickableViewAccessibility")
169169
override fun onTouchEvent(event: MotionEvent): Boolean {
170+
if (event.action == MotionEvent.ACTION_CANCEL) {
171+
tryFreeingResponder()
172+
}
173+
170174
val eventTime = event.eventTime
171175
val action = event.action
172176
// always true when lastEventTime or lastAction have default value (-1)
@@ -267,7 +271,7 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
267271
}
268272

269273
override fun drawableHotspotChanged(x: Float, y: Float) {
270-
if (responder == null || responder === this) {
274+
if (touchResponder == null || touchResponder === this) {
271275
super.drawableHotspotChanged(x, y)
272276
}
273277
}
@@ -280,19 +284,31 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
280284
return isResponder
281285
}
282286

287+
override fun afterGestureEnd(event: MotionEvent) {
288+
tryFreeingResponder()
289+
isTouched = false
290+
}
291+
292+
private fun tryFreeingResponder() {
293+
if (touchResponder === this) {
294+
touchResponder = null
295+
soundResponder = this
296+
}
297+
}
298+
283299
private fun tryGrabbingResponder(): Boolean {
284300
if (isChildTouched()) {
285301
return false
286302
}
287303

288-
if (responder == null) {
289-
responder = this
304+
if (touchResponder == null) {
305+
touchResponder = this
290306
return true
291307
}
292308
return if (exclusive) {
293-
responder === this
309+
touchResponder === this
294310
} else {
295-
!(responder?.exclusive ?: false)
311+
!(touchResponder?.exclusive ?: false)
296312
}
297313
}
298314

@@ -313,7 +329,8 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
313329
override fun performClick(): Boolean {
314330
// don't preform click when a child button is pressed (mainly to prevent sound effect of
315331
// a parent button from playing)
316-
return if (!isChildTouched()) {
332+
return if (!isChildTouched() && soundResponder == this) {
333+
soundResponder = null
317334
super.performClick()
318335
} else {
319336
false
@@ -327,22 +344,23 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
327344
// when canStart is called eventually, tryGrabbingResponder will return true if the button
328345
// already is a responder
329346
if (pressed) {
330-
tryGrabbingResponder()
347+
if (tryGrabbingResponder()) {
348+
soundResponder = this
349+
}
331350
}
332351

333352
// button can be pressed alongside other button if both are non-exclusive and it doesn't have
334353
// any pressed children (to prevent pressing the parent when children is pressed).
335-
val canBePressedAlongsideOther = !exclusive && responder?.exclusive != true && !isChildTouched()
354+
val canBePressedAlongsideOther = !exclusive && touchResponder?.exclusive != true && !isChildTouched()
336355

337-
if (!pressed || responder === this || canBePressedAlongsideOther) {
356+
if (!pressed || touchResponder === this || canBePressedAlongsideOther) {
338357
// we set pressed state only for current responder or any non-exclusive button when responder
339358
// is null or non-exclusive, assuming it doesn't have pressed children
340359
isTouched = pressed
341360
super.setPressed(pressed)
342361
}
343-
if (!pressed && responder === this) {
362+
if (!pressed && touchResponder === this) {
344363
// if the responder is no longer pressed we release button responder
345-
responder = null
346364
isTouched = false
347365
}
348366
}
@@ -354,7 +372,8 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>(), R
354372

355373
companion object {
356374
var resolveOutValue = TypedValue()
357-
var responder: ButtonViewGroup? = null
375+
var touchResponder: ButtonViewGroup? = null
376+
var soundResponder: ButtonViewGroup? = null
358377
var dummyClickListener = OnClickListener { }
359378
}
360379
}

example/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { TouchablesIndex, TouchableExample } from './release_tests/touchables';
1717
import Rows from './release_tests/rows';
1818
import Fling from './release_tests/fling';
1919
import NestedTouchables from './release_tests/nestedTouchables';
20+
import NestedButtons from './release_tests/nestedButtons';
2021
import NestedGestureHandlerRootViewWithModal from './release_tests/nestedGHRootViewWithModal';
2122
import { PinchableBox } from './recipes/scaleAndRotate';
2223
import PanAndScroll from './recipes/panAndScroll';
@@ -99,6 +100,10 @@ const EXAMPLES: ExamplesSection[] = [
99100
name: 'Nested Touchables - issue #784',
100101
component: NestedTouchables as React.ComponentType,
101102
},
103+
{
104+
name: 'Nested buttons (sound & ripple on Android)',
105+
component: NestedButtons,
106+
},
102107
{ name: 'Double pinch & rotate', component: DoublePinchRotate },
103108
{ name: 'Double draggable', component: DoubleDraggable },
104109
{ name: 'Rows', component: Rows },
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
import { View } from 'react-native';
3+
4+
import { RectButton } from 'react-native-gesture-handler';
5+
6+
export default function Example() {
7+
return (
8+
<View style={{ flex: 1 }}>
9+
<RectButton
10+
style={{ width: 400, height: 800, backgroundColor: 'yellow' }}>
11+
<RectButton style={{ width: 375, height: 700, backgroundColor: 'red' }}>
12+
<RectButton
13+
style={{ width: 350, height: 650, backgroundColor: 'green' }}>
14+
<RectButton
15+
style={{
16+
width: 325,
17+
height: 600,
18+
backgroundColor: 'blue',
19+
}}>
20+
<RectButton
21+
style={{ width: 300, height: 550, backgroundColor: 'cyan' }}>
22+
<RectButton
23+
style={{
24+
width: 275,
25+
height: 500,
26+
backgroundColor: 'purple',
27+
}}>
28+
<RectButton
29+
style={{
30+
width: 250,
31+
height: 450,
32+
backgroundColor: 'orange',
33+
}}>
34+
<RectButton
35+
style={{
36+
width: 225,
37+
height: 400,
38+
backgroundColor: 'gray',
39+
}}>
40+
<RectButton
41+
style={{
42+
width: 200,
43+
height: 350,
44+
backgroundColor: 'magenta',
45+
}}>
46+
<RectButton
47+
style={{
48+
width: 175,
49+
height: 300,
50+
backgroundColor: 'wheat',
51+
}}>
52+
<RectButton
53+
style={{
54+
width: 150,
55+
height: 250,
56+
backgroundColor: 'pink',
57+
}}>
58+
<RectButton
59+
style={{
60+
width: 125,
61+
height: 200,
62+
backgroundColor: 'lightgreen',
63+
}}>
64+
<RectButton
65+
style={{
66+
width: 100,
67+
height: 150,
68+
backgroundColor: 'navy',
69+
}}
70+
/>
71+
</RectButton>
72+
</RectButton>
73+
</RectButton>
74+
</RectButton>
75+
</RectButton>
76+
</RectButton>
77+
</RectButton>
78+
</RectButton>
79+
</RectButton>
80+
</RectButton>
81+
</RectButton>
82+
</RectButton>
83+
</View>
84+
);
85+
}

example/yarn.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3699,9 +3699,9 @@
36993699
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
37003700

37013701
"@types/react-native@^0.69.6":
3702-
version "0.69.6"
3703-
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.69.6.tgz#b792b7eb024a14869fdbbe97536e6014cb3be731"
3704-
integrity sha512-jx1QdJT3CdQc42EpoIGu22F1wrPZjmC/CNkfR5sRs5GxloJzthuICK7CKqAGEo2SekPs+YYzhbzrJGi1IrG5Lg==
3702+
version "0.69.13"
3703+
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.69.13.tgz#f947b2bd00439126f8101441945739698bb5051e"
3704+
integrity sha512-jEKIkqKjmYI7NeRssLrn/zRtFeI6S1FNe5p45OKbBxrQArXpF2lJelztFTEZKIv1PSfYKcAk5x6+1sdZFEFAkA==
37053705
dependencies:
37063706
"@types/react" "*"
37073707

0 commit comments

Comments
 (0)