SIGN IN SIGN UP

[Android] Add hover callbacks to Touchable (#4396)

## Description

`Touchable` gains `onHoverIn`/`onHoverOut`, reported for a mouse,
trackpad cursor or hovering stylus. The button already tracked hover to
drive its animation — this exposes it to JS as the new
`onButtonHoverIn`/`onButtonHoverOut` direct events, omitted from
`RawButtonProps` since the deprecated buttons never report hover.

- Reporting follows `isHovered && isEnabled`, the same expression that
drives the hover visual, so disabling a hovered button reports a
hover-out and re-enabling it with the pointer still inside reports a
hover-in.
- No hover events arrive while the button is held, so the transitions
are derived from the touch stream during a press — gated so a press can
only maintain a hover that was already open, never open one. The pointer
type carries over from the previous sample, since those events belong to
the pressing pointer.
- The payload is snapshotted when the pointer is seen, because the
events outlive the `MotionEvent` behind them.

## Test plan

`yarn test` covers the prop forwarding; hover itself needs a device or
emulator with a mouse, trackpad or stylus.

<details>
<summary>Example code</summary>

```tsx
import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
  GestureHandlerRootView,
  Touchable,
} from 'react-native-gesture-handler';

export default function Example() {
  const [log, setLog] = useState<string[]>([]);
  const callbacks = (source: string) => ({
    onHoverIn: () => setLog((l) => [`${source} onHoverIn`, ...l]),
    onHoverOut: () => setLog((l) => [`${source} onHoverOut`, ...l]),
    onPressIn: () => setLog((l) => [`${source} onPressIn`, ...l]),
    onPressOut: () => setLog((l) => [`${source} onPressOut`, ...l]),
  });

  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.row}>
        <Touchable style={styles.box} {...callbacks('Touchable')}>
          <Text style={styles.text}>Touchable</Text>
        </Touchable>
        <Pressable style={styles.box} {...callbacks('Pressable')}>
          <Text style={styles.text}>Pressable</Text>
        </Pressable>
      </View>
      {log.slice(0, 12).map((entry, i) => (
        <Text key={i}>{entry}</Text>
      ))}
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  row: { flexDirection: 'row', gap: 24, marginBottom: 24 },
  box: {
    width: 120,
    height: 120,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#6941C6',
  },
  text: { color: 'white' },
});
```

</details>
J
Jakub Piasecki committed
78cec50115825db34e33d1f921d1d6174783589f
Parent: 41d8392
Committed by GitHub <noreply@github.com> on 8/10/2026, 7:57:31 AM