Skip to content

Commit 1c938b1

Browse files
bastiandevjgonet
andauthored
Fix buttons can get stuck on Android (#1512)
## Description After updating from 1.8.0 to 1.10.3 some of my users reported problems about buttons getting stuck on Android. In #1172 an override for `onTouchEvent` was introduced to block subsequent calls. A check for event times was added with the assumption that every event will have a different event time. You can find further information in #1172 (comment) and in the Javadoc of `onTouchEvent`. Unfortunately, events with the same event time can indeed occur on some devices for different actions, which has glaring consequences. Some of my users and myself experienced situations where - after a quick touch - the ripple effect was halted, the onPress was not fired, no touch sound was played and no other button was accepting any inputs. It seemed like the app thought the finger was still on the button. This is due to the following sequence of events sometimes happening (fictional event times): 1. `ACTION_DOWN` is sent at EventTime 1000 2. `ACTION_MOVE` is sent at EventTime 2000 (Android sends at least one, even without a move) 3. `ACTION_UP` is **also** sent at EventTime 2000, resulting in not being handled and keeping the button touched Furthermore, my thesis is confirmed by the fact that touching the same button again is sending another `ACTION_UP` and thus the and all other buttons are unstucked. The problem can even be forced to a certain extent by combining the quick touch with a small swiping gesture, which sends even more `ACTION_MOVE` events. The only inexplicable part for me is that the frequency of this issue happening varies greatly from device to device. It happened every ~5 touches on a Huawei P30 (and on nearly every touch with the little forced swipe), infrequently but multiple times on a Samsung Galaxy S21, and couldn't be reproduced on an Oppo Find X2 Lite, LG G6, and the Android emulator. It is also worth mentioning that a reproduction on statically placed buttons is easier, since buttons in scrollable views send an `ACTION_CANCEL` instead of an `ACTION_UP` on the slightest movement in the scrolling direction. I have so far not had a case where those event times were the same. ## How it's fixed - Analogous to the check of the last event time, I have added another check for the last action. - Because 0 is a possible value for an action (`ACTION_DOWN` with first pointer), I set the initial value to -1. To keep everything consistent, I've also set the default value of the event time to -1. - By using `getAction` instead of `getActionMasked`, this solution is also pointer-sensitive. - Of course I have also expanded the Javadoc. :) After implementing these changes, I was no longer able to reproduce the issue on the P30 and S21 (or the other devices). ## Considerations This is kind of an extension to @jakub-gonet's assumption that there will not be two or more events that have the same event time and action in immediate succession. There may still be another way in which unexpected consequences can occur, but my change ensures that the problems that have arisen so far in my app in production under normal use are eliminated. In addition, the check in `onTouchEvent` is much more robust. Even if it did not happen in my tests, I can also very well imagine that the same event time could occur within scrollable views between `ACTION_MOVE` and `ACTION_CANCEL`. If so, my change would prevent such problems as well. ## Test plan Tested with the example code of #1172 on the above mentioned 5 devices, where I appended the following `BaseButton` and `RectButton`. The fix naturally applies to all Touchables since they use `BaseButton` internally. ```jsx import { BaseButton, RectButton, } from 'react-native-gesture-handler'; ... <> <BaseButton style={{ width: 200, height: 50, backgroundColor: 'lightgreen', }} onPress={() => console.log('pressed RNGH BaseButton')}> <Text>RNGH BaseButton</Text> </BaseButton> <RectButton style={{ width: 200, height: 50, backgroundColor: 'lightgreen', }} onPress={() => console.log('pressed RNGH RectButton')}> <Text>RNGH RectButton</Text> </RectButton> </> ``` Co-authored-by: Jakub Gonet <jakub.gonet@swmansion.com>
1 parent 297070c commit 1c938b1

File tree

1 file changed

+11
-5
lines changed

1 file changed

+11
-5
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>() {
8585

8686
private var _backgroundColor = Color.TRANSPARENT
8787
private var needBackgroundUpdate = false
88-
private var lastEventTime = 0L
88+
private var lastEventTime = -1L
89+
private var lastAction = -1
8990

9091
init {
9192
// we attach empty click listener to trigger tap sounds (see View#performClick())
@@ -139,17 +140,22 @@ class RNGestureHandlerButtonViewManager : ViewGroupManager<ButtonViewGroup>() {
139140
* This leads to invoking onTouchEvent twice which isn't idempotent in View - it calls OnClickListener
140141
* and plays sound effect if OnClickListener was set.
141142
*
142-
* To mitigate this behavior we use lastEventTime variable to check that we already handled
143-
* the event in [.onInterceptTouchEvent]. We assume here that different events
144-
* will have different event times.
143+
* To mitigate this behavior we use lastEventTime and lastAction variables to check that we already handled
144+
* the event in [onInterceptTouchEvent]. We assume here that different events
145+
* will have different event times or actions.
146+
* Events with same event time can occur on some devices for different actions.
147+
* (e.g. move and up in one gesture; move and cancel)
145148
*
146149
* Reference:
147150
* [com.swmansion.gesturehandler.NativeViewGestureHandler.onHandle] */
148151
@SuppressLint("ClickableViewAccessibility")
149152
override fun onTouchEvent(event: MotionEvent): Boolean {
150153
val eventTime = event.eventTime
151-
if (lastEventTime != eventTime || lastEventTime == 0L) {
154+
val action = event.action
155+
// always true when lastEventTime or lastAction have default value (-1)
156+
if (lastEventTime != eventTime || lastAction != action) {
152157
lastEventTime = eventTime
158+
lastAction = action
153159
return super.onTouchEvent(event)
154160
}
155161
return false

0 commit comments

Comments
 (0)