Skip to content

[General] Add minimumAnimationDuration prop to button component#4046

Draft
j-piasecki wants to merge 1 commit into@jpiasecki/refactor-buttonfrom
@jpiasecki/add-minimum-animation-duration
Draft

[General] Add minimumAnimationDuration prop to button component#4046
j-piasecki wants to merge 1 commit into@jpiasecki/refactor-buttonfrom
@jpiasecki/add-minimum-animation-duration

Conversation

@j-piasecki
Copy link
Copy Markdown
Member

Description

Native platforms delay touches when the button is inside a ScrollView. This may cause situations where press-out is triggered immediately after press-in, effectively running with no animation.

This PR adds minimumAnimationDuration, which can be used to set a minimum time for the press-in animation before press-out is allowed to animate.

Test plan

Updated the button underlay example

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

Adds a new minimumAnimationDuration prop to GestureHandlerButton to ensure the “pressed in” visual state remains visible for at least a minimum time (mitigating native touch delays inside scrollables that can cause immediate press-out).

Changes:

  • Introduces minimumAnimationDuration to the button’s codegen spec and TS props.
  • Implements minimum-duration press-out deferral on Web, iOS, and Android.
  • Updates the common-app underlay example to exercise the new prop.

Reviewed changes

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

Show a summary per file
File Description
packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts Adds minimumAnimationDuration to native component codegen props with a default.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx Implements min-duration press-out deferral for web button visuals.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx Documents the new prop on the public ButtonProps interface.
packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm Wires the new prop from Fabric props into the native button view.
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm Adds iOS minimum-duration handling between press-in and press-out animations.
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h Exposes minimumAnimationDuration on the native button class.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt Adds Android prop setter + min-duration delayed press-out implementation.
apps/common-app/src/new_api/components/button_underlay/index.tsx Updates example props to demonstrate minimumAnimationDuration.

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

export const ButtonComponent = ({
enabled = true,
animationDuration = 100,
minimumAnimationDuration = 0,
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

minimumAnimationDuration defaults to 0 here, but the public TS docs for ButtonProps and the native codegen spec define a default of 50ms. This creates cross-platform behavior differences when the prop is omitted; consider defaulting to 50 here to match the documented/native default.

Suggested change
minimumAnimationDuration = 0,
minimumAnimationDuration = 50,

Copilot uses AI. Check for mistakes.
Comment on lines 58 to +67
const pressOut = React.useCallback(() => {
setPressed(false);
}, []);
const elapsed = Date.now() - pressInTimestamp.current;
const remaining =
Math.min(animationDuration, minimumAnimationDuration) - elapsed;

if (remaining > 0) {
pressOutTimer.current = setTimeout(() => {
pressOutTimer.current = null;
setPressed(false);
}, remaining);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

In pressOut, a new timeout is assigned to pressOutTimer.current without clearing any previously scheduled timeout. If pressOut fires multiple times (or after a quick re-press), an older timeout can still run later and flip pressed to false unexpectedly. Clear any existing pressOutTimer.current before scheduling a new one (and consider nulling it on the immediate path).

Copilot uses AI. Check for mistakes.

var exclusive = true
var animationDuration: Int = 100
var minimumAnimationDuration: Int = 0
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

minimumAnimationDuration is initialized to 0, but the JS API docs and native codegen spec define a default of 50ms. To keep behavior consistent when the prop is omitted (similar to how animationDuration defaults are handled), initialize this to 50 instead of 0.

Suggested change
var minimumAnimationDuration: Int = 0
var minimumAnimationDuration: Int = 50

Copilot uses AI. Check for mistakes.
Comment on lines 537 to +550
private fun animatePressOut() {
animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity)
val animationTime = SystemClock.uptimeMillis() - pressInTimestamp
val remainingAnimationTime = min(animationDuration, minimumAnimationDuration) - animationTime

if (remainingAnimationTime <= 0) {
animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity)
} else {
val runnable = Runnable {
pendingPressOut = null
animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity)
}
pendingPressOut = runnable
handler.postDelayed(runnable, remainingAnimationTime)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

animatePressOut schedules a new Runnable and overwrites pendingPressOut without removing any previously posted runnable. If animatePressOut is invoked multiple times before the delay fires, earlier runnables can still execute after a subsequent animatePressIn, causing an unexpected press-out animation. Remove callbacks for any existing pendingPressOut before posting a new one (and avoid losing references to already-posted runnables).

Copilot uses AI. Check for mistakes.
_defaultScale = 1.0;
_activeUnderlayOpacity = 0.0;
_defaultUnderlayOpacity = 0.0;
_minimumAnimationDuration = 0;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

_minimumAnimationDuration is initialized to 0, but the JS API docs and codegen spec define a default of 50ms. Initializing this to 50 would keep default behavior consistent across platforms/architectures when the prop is omitted.

Suggested change
_minimumAnimationDuration = 0;
_minimumAnimationDuration = 50;

Copilot uses AI. Check for mistakes.
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