Skip to content

[Android] Handle hover in Touchable#4282

Open
m-bert wants to merge 4 commits into
mainfrom
@mbert/touchable-hover-android
Open

[Android] Handle hover in Touchable#4282
m-bert wants to merge 4 commits into
mainfrom
@mbert/touchable-hover-android

Conversation

@m-bert

@m-bert m-bert commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR brings hover effects for the Touchable component on Android. Previously, animations based on hover were possible only on web.

Hovering out is handled in post-frame callback. This is due to the fact that on Android ACTION_HOVER_EXIT is dispatched right before ACTION_DOWN, so without delaying hover out effect we would be left with flickers.

Test plan

Tested on existing Touchable example and on the code below:
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  GestureHandlerRootView,
  Touchable as GHTouchable,
} from 'react-native-gesture-handler';

const SLOW = 700;

export default function App() {
  const [pressCount, setPressCount] = React.useState(0);
  const [disabled, setDisabled] = React.useState(false);

  // Auto-toggle `disabled` every 2s so hover mask/resume can be tested with a
  // mouse held still over the button — tapping a separate control would move
  // the pointer off first. Hover and watch: the visual masks to default while
  // disabled and resumes the hover look when re-enabled, pointer unmoved.
  React.useEffect(() => {
    const id = setInterval(() => setDisabled((d) => !d), 2000);
    return () => clearInterval(id);
  }, []);

  return (
    <GestureHandlerRootView style={styles.root}>
      <Text style={styles.title}>Slow hover + press</Text>
      <Text style={styles.hint}>
        Use a mouse / stylus. Hover to grow & fade, press to shrink & fade more.
        Transitions should never flicker through the default state.
      </Text>

      <View style={styles.stage}>
        <GHTouchable
          // Press (active) visuals
          defaultOpacity={1}
          defaultScale={1}
          activeOpacity={0.3}
          activeScale={0.8}
          // Hover visuals
          hoverOpacity={0.6}
          hoverScale={1.2}
          // Underlay so the change is extra visible
          underlayColor="black"
          defaultUnderlayOpacity={0}
          hoverUnderlayOpacity={0.15}
          activeUnderlayOpacity={0.35}
          // Slow everything down: in/out for both tap and hover
          animationDuration={{
            tap: { in: SLOW, out: SLOW },
            hover: { in: SLOW, out: SLOW },
          }}
          disabled={disabled}
          style={styles.button}
          onPress={() => setPressCount((c) => c + 1)}>
          <Text style={styles.buttonText}>Hover / Press me</Text>
        </GHTouchable>
      </View>

      <Text style={styles.counter}>
        {disabled ? 'DISABLED' : 'enabled'} · Presses: {pressCount}
      </Text>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  root: {
    flex: 1,
    backgroundColor: '#ecf0f1',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 8,
  },
  hint: {
    fontSize: 14,
    color: '#7f8c8d',
    textAlign: 'center',
    marginBottom: 40,
  },
  stage: {
    width: 260,
    height: 260,
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    width: 180,
    height: 180,
    borderRadius: 24,
    backgroundColor: '#8e44ad',
    alignItems: 'center',
    justifyContent: 'center',
  },
  buttonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: 'bold',
  },
  counter: {
    marginTop: 40,
    fontSize: 16,
    color: '#2c3e50',
    fontWeight: 'bold',
  },
});

Copilot AI review requested due to automatic review settings June 24, 2026 08:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Android hover animations to Touchable by extending the native RNGestureHandlerButton implementation to react to hover enter/exit, aligning behavior with the existing web hover support and avoiding hover→press flicker via a post-frame hover-out.

Changes:

  • Extend the codegen native component spec to include hover-related props (opacity/scale/underlay + hover in/out durations).
  • Update Android RNGestureHandlerButtonViewManager to animate hover in/out and to let press-out settle on hover values when appropriate.
  • Update JS/TS docs for GestureHandlerButton hover props to indicate Android support.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts Adds hover props to the native component spec (including sentinel defaults for hover values).
packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx Updates prop docs to reflect hover support on Android.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt Implements Android hover handling, delayed hover-out to avoid flicker, and press-out targeting logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert m-bert marked this pull request as ready for review June 24, 2026 10:51
@m-bert m-bert requested a review from Copilot June 24, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +445 to +451
// Resting (non-pressed) animation targets. While the pointer hovers the
// button, the press-out animation settles on the hover values instead of
// the defaults, mirroring the web priority order (pressed > hovered >
// default).
private val restingOpacity get() = if (isHovered) hoverOpacity else defaultOpacity
private val restingScale get() = if (isHovered) hoverScale else defaultScale
private val restingUnderlayOpacity get() = if (isHovered) hoverUnderlayOpacity else defaultUnderlayOpacity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants