[General] Update button's animation duration props#4046
[General] Update button's animation duration props#4046j-piasecki wants to merge 10 commits into@jpiasecki/refactor-buttonfrom
Conversation
There was a problem hiding this comment.
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
minimumAnimationDurationto 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.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx
Show resolved
Hide resolved
...ndroid/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
Outdated
Show resolved
Hide resolved
...ndroid/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
Show resolved
Hide resolved
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm
Outdated
Show resolved
Hide resolved
minimumAnimationDuration prop to button componentThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx
Outdated
Show resolved
Hide resolved
...ndroid/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx
Show resolved
Hide resolved
|
|
||
| if (elapsed >= pressAndHoldAnimationDuration) { | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, pressAndHoldAnimationDuration.toLong()) | ||
| // elapsed * 2 to ensure there is at least half of the tapAnimationDuration left for the animation to play | ||
| } else if (elapsed * 2 >= tapAnimationDuration) { | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, elapsed) | ||
| } else { | ||
| val remaining = tapAnimationDuration - elapsed | ||
| animateTo(activeOpacity, activeScale, activeUnderlayOpacity, remaining) | ||
|
|
||
| val runnable = Runnable { | ||
| pendingPressOut = null | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, tapAnimationDuration.toLong()) |
There was a problem hiding this comment.
elapsed is a Long, while pressAndHoldAnimationDuration/tapAnimationDuration are Int. Kotlin doesn’t allow ordering comparisons or arithmetic between Long and Int, so these comparisons will fail to compile. Convert the durations to Long once (e.g., val pressAndHoldMs = pressAndHoldAnimationDuration.toLong(), val tapMs = tapAnimationDuration.toLong()) and use those consistently in comparisons and animator durations.
| if (elapsed >= pressAndHoldAnimationDuration) { | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, pressAndHoldAnimationDuration.toLong()) | |
| // elapsed * 2 to ensure there is at least half of the tapAnimationDuration left for the animation to play | |
| } else if (elapsed * 2 >= tapAnimationDuration) { | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, elapsed) | |
| } else { | |
| val remaining = tapAnimationDuration - elapsed | |
| animateTo(activeOpacity, activeScale, activeUnderlayOpacity, remaining) | |
| val runnable = Runnable { | |
| pendingPressOut = null | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, tapAnimationDuration.toLong()) | |
| val pressAndHoldMs = pressAndHoldAnimationDuration.toLong() | |
| val tapMs = tapAnimationDuration.toLong() | |
| if (elapsed >= pressAndHoldMs) { | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, pressAndHoldMs) | |
| // elapsed * 2 to ensure there is at least half of the tapAnimationDuration left for the animation to play | |
| } else if (elapsed * 2 >= tapMs) { | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, elapsed) | |
| } else { | |
| val remaining = tapMs - elapsed | |
| animateTo(activeOpacity, activeScale, activeUnderlayOpacity, remaining) | |
| val runnable = Runnable { | |
| pendingPressOut = null | |
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, tapMs) |
| } else { | ||
| val remaining = tapAnimationDuration - elapsed | ||
| animateTo(activeOpacity, activeScale, activeUnderlayOpacity, remaining) | ||
|
|
||
| val runnable = Runnable { | ||
| pendingPressOut = null | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity, tapAnimationDuration.toLong()) | ||
| } | ||
| pendingPressOut = runnable | ||
| handler.postDelayed(runnable, remaining) |
There was a problem hiding this comment.
val remaining = tapAnimationDuration - elapsed mixes Int and Long (won’t compile), and handler.postDelayed(runnable, remaining) expects a non-negative Long delay. After converting tapAnimationDuration to Long, ensure remaining is computed as Long and clamped to >= 0 before calling postDelayed.
| private var currentAnimator: AnimatorSet? = null | ||
| private var underlayDrawable: PaintDrawable? = null | ||
| private var pressInTimestamp = 0L | ||
| private var pendingPressOut: Runnable? = null | ||
|
|
There was a problem hiding this comment.
A pending Runnable is now stored in pendingPressOut, but there’s no lifecycle cleanup (e.g., onDetachedFromWindow) to remove it and cancel currentAnimator. If the view is unmounted while a delayed press-out is scheduled (or if tapAnimationDuration is set high), the runnable can keep the view referenced and run animations after detach. Consider overriding onDetachedFromWindow to removeCallbacks for pendingPressOut, null it out, and cancel currentAnimator.
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:
animationDurationtopressAndHoldAnimationDurationtapAnimationDurationpressAndHoldAnimationDurationdefaults totapAnimationDurationwhen unspecified;tapAnimationDurationdefaults to 100ms.Press out handler now has three branches:
pressAndHoldAnimationDuration-> press out animation is started immediately with the duration ofpressAndHoldAnimationDurationtapAnimationDuration,pressAndHoldAnimationDuration] (*2 so there's at least half oftapAnimationDurationfor the remaining animation) -> press out animation is started immediately with the duration of the press so fartapAnimationDuration / 2-> pressIn animation is played fully in the remaining time intapAnimationDuration, and pressOut is scheduled after with the duration oftapAnimationDurationTest plan
Updated the button underlay example