You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Android] Clear the pending HOVER_EXIT finish when the hover handler is reset - #4593
HoverGestureHandler posts finishRunnable with a 4 ms delay on HOVER_EXIT, so a touch-down (which synthesizes HOVER_EXIT right before ACTION_DOWN) can cancel it and the hover survives the press. The runnable was only ever cleared on ACTION_DOWN. If the handler finished by another route and got reset before the runnable fired, the runnable stayed armed and ended the next hover gesture that was recorded on the same handler.
This shows up when the UI thread stalls while the pointer leaves the window and comes back: the queued HOVER_EXIT, HOVER_ENTER (outside the view) and HOVER_MOVE (inside the view) flush together, the handler finishes, resets and re-activates within the 4 ms window, and the stale runnable then ends the fresh gesture. The view loses its hover state while the pointer is still resting on it, until the next move.
onReset now clears the handler's pending callbacks, the same way TapGestureHandler does.
Test plan
Hover a view with a stylus or mouse on Android, stall the UI thread for ~2 s (e.g. a busy-wait worklet scheduled on the UI runtime), and during the stall move the pointer out of the window, back in outside the view, then onto the view and hold still. Before: the view flickers to its non-hovered state and stays there after the stall. After: it stays hovered.
Tested on the following code
importReact,{useCallback,useEffect,useState}from'react';import{StyleSheet,Switch,Text,View}from'react-native';import{GestureDetector,useHoverGesture}from'react-native-gesture-handler';importAnimated,{Easing,useAnimatedStyle,useSharedValue,withRepeat,withTiming,}from'react-native-reanimated';import{scheduleOnRN,scheduleOnUI}from'react-native-worklets';// Repro: Android HoverGestureHandler.onReset doesn't clear the 4 ms finishRunnable// posted on HOVER_EXIT. Steps (mouse on Android):// 1. enable the periodic stall, hover the box// 2. while the spinner is frozen: move into the status bar and back onto the box, stop there// 3. after the stall the box should stay highlighted - with the bug it goes dark// and the log shows `finalize` a few ms after `activate` (marked with <<<).constSTALL_MS=2000;constPERIOD_MS=6000;typeEntry={t: number;name: string};functionstallUiThread(ms: number){'worklet';constend=Date.now()+ms;while(Date.now()<end){// busy-wait on the UI thread so input events queue up}}exportdefaultfunctionEmptyExample(){constactive=useSharedValue(0);constspin=useSharedValue(0);const[log,setLog]=useState<Entry[]>([]);const[cursor,setCursor]=useState('-');const[stallEnabled,setStallEnabled]=useState(false);const[countdown,setCountdown]=useState(0);constaddLog=useCallback((name: string,t: number)=>{setLog((prev)=>[...prev.slice(-13),{ t, name }]);},[]);useEffect(()=>{spin.value=withRepeat(withTiming(360,{duration: 1500,easing: Easing.linear}),-1,false);},[spin]);useEffect(()=>{if(!stallEnabled){setCountdown(0);return;}letnext=Date.now()+PERIOD_MS;constid=setInterval(()=>{constleft=next-Date.now();if(left<=0){next=Date.now()+PERIOD_MS;addLog('stall',Date.now());scheduleOnUI(stallUiThread,STALL_MS);}setCountdown(Math.max(0,Math.ceil(left/1000)));},250);return()=>clearInterval(id);},[stallEnabled,addLog]);constboxHover=useHoverGesture({onBegin: ()=>{scheduleOnRN(addLog,'begin',Date.now());},onActivate: ()=>{active.value=1;scheduleOnRN(addLog,'activate',Date.now());},onDeactivate: ()=>{scheduleOnRN(addLog,'deactivate',Date.now());},onFinalize: ()=>{active.value=0;scheduleOnRN(addLog,'finalize',Date.now());},});constcursorHover=useHoverGesture({onUpdate: (e)=>{scheduleOnRN(setCursor,`${Math.round(e.absoluteX)}, ${Math.round(e.absoluteY)}`);},});constboxStyle=useAnimatedStyle(()=>({backgroundColor: active.value ? '#2e7d32' : '#263238',}));constspinStyle=useAnimatedStyle(()=>({transform: [{rotate: `${spin.value}deg`}],}));return(<GestureDetectorgesture={cursorHover}><Viewstyle={styles.container}><Viewstyle={styles.row}><Switchvalue={stallEnabled}onValueChange={setStallEnabled}/><Textstyle={styles.text}>{stallEnabled
? countdown>0
? `UI stall in ${countdown}s`
: 'UI STALLED'
: 'Periodic 2s UI stall'}</Text><Animated.Viewstyle={[styles.spinner,spinStyle]}/></View><Textstyle={styles.text}>cursor (dp): {cursor}</Text><GestureDetectorgesture={boxHover}><Animated.Viewstyle={[styles.box,boxStyle]}/></GestureDetector><Viewstyle={styles.log}>{log.map((e,i)=>{constprev=log[i-1];constdt=prev ? e.t-prev.t : 0;conststale=e.name==='finalize'&&prev?.name==='activate'&&dt<20;return(<Textkey={e.t+e.name+i}style={styles.logLine}>{`+${dt} ms ${e.name}${stale ? ' <<< stale finish' : ''}`}</Text>);})}</View></View></GestureDetector>);}conststyles=StyleSheet.create({container: {flex: 1,alignItems: 'center',paddingTop: 24,},row: {flexDirection: 'row',alignItems: 'center',gap: 12,},text: {fontSize: 16,},spinner: {width: 20,height: 20,backgroundColor: '#ef6c00',},box: {width: 200,height: 200,marginVertical: 24,borderRadius: 12,},log: {alignSelf: 'stretch',paddingHorizontal: 16,},logLine: {fontFamily: 'monospace',fontSize: 13,},});
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.
📝 Summary
Summary by CodeRabbit
Bug Fixes
Pending callbacks and messages are now cleared when hover gesture handling is reset. This prevents scheduled work from carrying over after a reset and improves the reliability of subsequent hover interactions.
HoverGestureHandler.onReset() now removes pending callbacks and messages from its handler after resetting stylus data.
Changes: Hover gesture reset
Layer / File(s)
Summary
Clear pending handler work packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt
onReset() removes pending callbacks and messages from the handler after resetting stylus data.
Priority: ⬇️ Low
Merge Risk:⚪ Minimal · up to b053a
Reset cancels the stale hover-finish callback without affecting unrelated work, so no merge-blocking issue is identified.
Pre-merge checks: 3
Passed checks (3 passed)
Check name
Status
Explanation
Title check
The title clearly and concisely describes the main change: clearing the pending HOVER_EXIT finish when HoverGestureHandler resets.
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
HoverGestureHandler posts finishRunnable with a 4 ms delay on HOVER_EXIT, so a touch-down (which synthesizes HOVER_EXIT right before ACTION_DOWN) can cancel it and the hover survives the press. The runnable was only ever cleared on ACTION_DOWN. If the handler finished by another route and got reset before the runnable fired, the runnable stayed armed and ended the next hover gesture that was recorded on the same handler.
This shows up when the UI thread stalls while the pointer leaves the window and comes back: the queued HOVER_EXIT, HOVER_ENTER (outside the view) and HOVER_MOVE (inside the view) flush together, the handler finishes, resets and re-activates within the 4 ms window, and the stale runnable then ends the fresh gesture. The view loses its hover state while the pointer is still resting on it, until the next move.
onReset now clears the handler's pending callbacks, the same way TapGestureHandler does.
Test plan
Hover a view with a stylus or mouse on Android, stall the UI thread for ~2 s (e.g. a busy-wait worklet scheduled on the UI runtime), and during the stall move the pointer out of the window, back in outside the view, then onto the view and hold still. Before: the view flickers to its non-hovered state and stays there after the stall. After: it stays hovered.
Tested on the following code