Skip to content

[Native] Snap Touchable state on sub-frame animations#4181

Merged
j-piasecki merged 2 commits into
mainfrom
@jpiasecki/touchable-snap-sub-frame-animations
May 20, 2026
Merged

[Native] Snap Touchable state on sub-frame animations#4181
j-piasecki merged 2 commits into
mainfrom
@jpiasecki/touchable-snap-sub-frame-animations

Conversation

@j-piasecki
Copy link
Copy Markdown
Member

@j-piasecki j-piasecki commented May 19, 2026

Description

Updates how Touchable handles animations:

  • On Android, when the animation would take less than a single frame, apply the values immediately. ObjectAnimator would run the animation on the next frame, and if a new animation was started immediately after (like a quick tap in a scroll view), it would read the "resting" state instead of the one just applied.
  • On iOS, do the same thing and read values from the presentation layer only when an animation is running (similarly to Android, the animation would start on the next frame).
  • On Android, it also changes LinearOutSlowInInterpolator to FastOutSlowInInterpolator to better match the animation curve from web and iOS

Test plan

- Before After
Android
Screen.Recording.2026-05-19.at.08.22.53.mov
Screen.Recording.2026-05-19.at.08.20.50.mov
iOS
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-05-19.at.08.22.14.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-05-19.at.08.21.09.mov

(Yes, iOS before is being pressed on constantly, it's just animating default -> default)

import * as React from 'react';
import { StyleSheet } from 'react-native';
import { ScrollView, Touchable } from 'react-native-gesture-handler';

export default function TapToLikeExample() {
  return (
    <ScrollView
      style={styles.container}
      contentContainerStyle={{ flexGrow: 1 }}>
      <Touchable
        style={{ width: 200, height: 40, backgroundColor: 'red' }}
        activeOpacity={0.2}
        animationDuration={{ in: 0, out: 250 }}></Touchable>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    gap: 24,
    padding: 24,
  },
});

Copilot AI review requested due to automatic review settings May 19, 2026 06:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts native Touchable press animations to avoid “stale start value” glitches when an animation’s duration is shorter than a display frame, ensuring state updates land immediately instead of being deferred to the next frame.

Changes:

  • iOS/macOS: snap opacity/scale immediately for sub-frame durations and only read from the presentation layer when an animation is actually in flight.
  • Android: snap opacity/scale/underlay immediately for sub-frame durations to avoid ObjectAnimator deferring property writes.
  • Android: add a Display.minimumFrameTime helper (and switch the press animation interpolator).

Reviewed changes

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

File Description
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm Adds per-screen frame-duration threshold and snaps Touchable state updates for sub-frame animations; presentation-layer reads only when animating.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt Snaps updates for sub-frame durations to avoid deferred ObjectAnimator writes; changes interpolator.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt Introduces Display.minimumFrameTime utility used to determine sub-frame thresholds.

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 1 comment.

@j-piasecki j-piasecki marked this pull request as ready for review May 19, 2026 08:11
@j-piasecki j-piasecki requested a review from m-bert May 19, 2026 08:11
Copy link
Copy Markdown
Collaborator

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

Sadly, I can't see difference on Android - on emulator it is still like in "before" video and on my physical device it works fine in both cases 😞

Comment on lines +270 to +272
if (@available(macOS 12.0, *)) {
maxFps = screen.maximumFramesPerSecond;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

12.0 seems pretty old, can't we assume that it's true?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We have 10.15 in podspec as minimum. I think we can change that, but outside of the scope of this PR.

@j-piasecki j-piasecki merged commit 38025e7 into main May 20, 2026
10 checks passed
@j-piasecki j-piasecki deleted the @jpiasecki/touchable-snap-sub-frame-animations branch May 20, 2026 05:22
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.

3 participants