Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3872ac1
Remove legacy view manager
j-piasecki Mar 18, 2026
cdd3681
Update native button spec
j-piasecki Mar 18, 2026
fb3693a
Implement ios
j-piasecki Mar 18, 2026
8018824
Implement android
j-piasecki Mar 18, 2026
145117a
Implement web
j-piasecki Mar 18, 2026
875a454
Update props definition
j-piasecki Mar 18, 2026
52443ed
Use `nil` as default background color
j-piasecki Mar 18, 2026
3d28579
Fix pointer release
j-piasecki Mar 18, 2026
de8a29b
Fix layer size
j-piasecki Mar 19, 2026
be8bff4
Use layer transform
j-piasecki Mar 19, 2026
46f95c5
Don't animate when not set
j-piasecki Mar 19, 2026
9ec7832
Apply resting styles in React
j-piasecki Mar 19, 2026
cce5ca1
Omit the new props from existing buttons
j-piasecki Mar 19, 2026
0adf9da
Align interpolation function
j-piasecki Mar 19, 2026
71e8692
Keep underlay layer at the bottom
j-piasecki Mar 19, 2026
f202327
Change order
j-piasecki Mar 19, 2026
89eaa20
Update background on start opacity change
j-piasecki Mar 19, 2026
e10e612
Update legacy types
j-piasecki Mar 19, 2026
5a1423e
Don't pass activeOpacity to native view
j-piasecki Mar 19, 2026
8681383
`start*` -> `default*`
j-piasecki Mar 19, 2026
4de84a7
Don't pass activeOpacity in legacy rect button
j-piasecki Mar 20, 2026
5be1308
Wrap return in curly braces
j-piasecki Mar 20, 2026
df6fde7
Use aliased color
j-piasecki Mar 20, 2026
2efcfb6
Move self assignment to the if expressions
j-piasecki Mar 20, 2026
d01547e
Animate out when dragged outside
j-piasecki Mar 20, 2026
e5db0f9
Update ref type
j-piasecki Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.swmansion.gesturehandler.react

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
Expand All @@ -19,10 +22,10 @@ import android.util.TypedValue
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.children
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import com.facebook.react.R
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil
Expand Down Expand Up @@ -133,6 +136,46 @@ class RNGestureHandlerButtonViewManager :
view.isSoundEffectsEnabled = !touchSoundDisabled
}

@ReactProp(name = "animationDuration")
override fun setAnimationDuration(view: ButtonViewGroup, animationDuration: Int) {
view.animationDuration = animationDuration
}

@ReactProp(name = "defaultOpacity")
override fun setDefaultOpacity(view: ButtonViewGroup, defaultOpacity: Float) {
view.defaultOpacity = defaultOpacity
}

@ReactProp(name = "activeOpacity")
override fun setActiveOpacity(view: ButtonViewGroup, targetOpacity: Float) {
view.activeOpacity = targetOpacity
}

@ReactProp(name = "defaultScale")
override fun setDefaultScale(view: ButtonViewGroup, defaultScale: Float) {
view.defaultScale = defaultScale
}

@ReactProp(name = "activeScale")
override fun setActiveScale(view: ButtonViewGroup, activeScale: Float) {
view.activeScale = activeScale
}

@ReactProp(name = "underlayColor")
override fun setUnderlayColor(view: ButtonViewGroup, underlayColor: Int?) {
view.underlayColor = underlayColor
}

@ReactProp(name = "defaultUnderlayOpacity")
override fun setDefaultUnderlayOpacity(view: ButtonViewGroup, defaultUnderlayOpacity: Float) {
view.defaultUnderlayOpacity = defaultUnderlayOpacity
}

@ReactProp(name = "activeUnderlayOpacity")
override fun setActiveUnderlayOpacity(view: ButtonViewGroup, activeUnderlayOpacity: Float) {
view.activeUnderlayOpacity = activeUnderlayOpacity
}

@ReactProp(name = ViewProps.POINTER_EVENTS)
override fun setPointerEvents(view: ButtonViewGroup, pointerEvents: String?) {
view.pointerEvents = when (pointerEvents) {
Expand Down Expand Up @@ -212,6 +255,20 @@ class RNGestureHandlerButtonViewManager :
borderBottomRightRadius != 0f

var exclusive = true
var animationDuration: Int = 100
var activeOpacity: Float = 1.0f
var defaultOpacity: Float = 1.0f
var activeScale: Float = 1.0f
var defaultScale: Float = 1.0f
var underlayColor: Int? = null
set(color) = withBackgroundUpdate {
field = color
}
var activeUnderlayOpacity: Float = 0f
var defaultUnderlayOpacity: Float = 0f
set(value) = withBackgroundUpdate {
field = value
}

override var pointerEvents: PointerEvents = PointerEvents.AUTO

Expand All @@ -220,6 +277,8 @@ class RNGestureHandlerButtonViewManager :
private var lastEventTime = -1L
private var lastAction = -1
private var receivedKeyEvent = false
private var currentAnimator: AnimatorSet? = null
private var underlayDrawable: PaintDrawable? = null

var isTouched = false

Expand Down Expand Up @@ -331,7 +390,73 @@ class RNGestureHandlerButtonViewManager :
return false
}

private fun updateBackgroundColor(backgroundColor: Int, borderDrawable: Drawable, selectable: Drawable?) {
private fun applyStartAnimationState() {
(parent as? ViewGroup)?.let {
if (activeOpacity != 1.0f || defaultOpacity != 1.0f) {
it.alpha = defaultOpacity
}
if (activeScale != 1.0f || defaultScale != 1.0f) {
it.scaleX = defaultScale
it.scaleY = defaultScale
}
}
underlayDrawable?.alpha = (defaultUnderlayOpacity * 255).toInt()
}

private fun animateTo(opacity: Float, scale: Float, underlayOpacity: Float) {
val hasOpacity = activeOpacity != 1.0f || defaultOpacity != 1.0f
val hasScale = activeScale != 1.0f || defaultScale != 1.0f
val hasUnderlay = activeUnderlayOpacity != defaultUnderlayOpacity && underlayDrawable != null
if (!hasOpacity && !hasScale && !hasUnderlay) {
return
}

currentAnimator?.cancel()
val animators = ArrayList<Animator>()
if (hasOpacity || hasScale) {
val parent = this.parent as? ViewGroup ?: return
if (hasOpacity) {
animators.add(ObjectAnimator.ofFloat(parent, "alpha", opacity))
}
if (hasScale) {
animators.add(ObjectAnimator.ofFloat(parent, "scaleX", scale))
animators.add(ObjectAnimator.ofFloat(parent, "scaleY", scale))
}
}
if (hasUnderlay) {
animators.add(ObjectAnimator.ofInt(underlayDrawable!!, "alpha", (underlayOpacity * 255).toInt()))
}
currentAnimator = AnimatorSet().apply {
playTogether(animators)
duration = animationDuration.toLong()
interpolator = LinearOutSlowInInterpolator()
start()
}
}

private fun animatePressIn() {
animateTo(activeOpacity, activeScale, activeUnderlayOpacity)
}

private fun animatePressOut() {
animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity)
}

private fun createUnderlayDrawable(): PaintDrawable {
val drawable = PaintDrawable(underlayColor ?: Color.BLACK)
if (hasBorderRadii) {
drawable.setCornerRadii(buildBorderRadii())
}
drawable.alpha = (defaultUnderlayOpacity * 255).toInt()
return drawable
}

private fun updateBackgroundColor(
backgroundColor: Int,
underlay: Drawable,
borderDrawable: Drawable,
selectable: Drawable?,
) {
val colorDrawable = PaintDrawable(backgroundColor)

if (hasBorderRadii) {
Expand All @@ -340,9 +465,9 @@ class RNGestureHandlerButtonViewManager :

val layerDrawable = LayerDrawable(
if (selectable != null) {
arrayOf(colorDrawable, selectable, borderDrawable)
arrayOf(colorDrawable, underlay, selectable, borderDrawable)
} else {
arrayOf(colorDrawable, borderDrawable)
arrayOf(colorDrawable, underlay, borderDrawable)
},
)
background = layerDrawable
Expand All @@ -365,6 +490,8 @@ class RNGestureHandlerButtonViewManager :

val selectable = createSelectableDrawable()
val borderDrawable = createBorderDrawable()
val underlay = createUnderlayDrawable()
underlayDrawable = underlay

if (hasBorderRadii && selectable is RippleDrawable) {
val mask = PaintDrawable(Color.WHITE)
Expand All @@ -375,13 +502,15 @@ class RNGestureHandlerButtonViewManager :
if (useDrawableOnForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
foreground = selectable
if (buttonBackgroundColor != Color.TRANSPARENT) {
updateBackgroundColor(buttonBackgroundColor, borderDrawable, null)
updateBackgroundColor(buttonBackgroundColor, underlay, borderDrawable, null)
}
} else if (buttonBackgroundColor == Color.TRANSPARENT && rippleColor == null) {
background = LayerDrawable(arrayOf(selectable, borderDrawable))
background = LayerDrawable(arrayOf(underlay, selectable, borderDrawable))
} else {
updateBackgroundColor(buttonBackgroundColor, borderDrawable, selectable)
updateBackgroundColor(buttonBackgroundColor, underlay, borderDrawable, selectable)
}

applyStartAnimationState()
}

private fun createBorderDrawable(): Drawable {
Expand Down Expand Up @@ -540,6 +669,12 @@ class RNGestureHandlerButtonViewManager :
// is null or non-exclusive, assuming it doesn't have pressed children
isTouched = pressed
super.setPressed(pressed)

if (pressed) {
animatePressIn()
} else {
animatePressOut()
}
}

if (!pressed && touchResponder === this) {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-gesture-handler/apple/RNGHUIKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ typedef UIWindow RNGHWindow;
typedef UIScrollView RNGHScrollView;
typedef UITouch RNGHUITouch;
typedef UIScrollView RNGHUIScrollView;
typedef UIColor RNGHColor;

#define RNGHGestureRecognizerStateFailed UIGestureRecognizerStateFailed;
#define RNGHGestureRecognizerStatePossible UIGestureRecognizerStatePossible;
Expand All @@ -23,6 +24,7 @@ typedef NSWindow RNGHWindow;
typedef NSScrollView RNGHScrollView;
typedef RCTUITouch RNGHUITouch;
typedef NSScrollView RNGHUIScrollView;
typedef NSColor RNGHColor;

#define RNGHGestureRecognizerStateFailed NSGestureRecognizerStateFailed;
#define RNGHGestureRecognizerStatePossible NSGestureRecognizerStatePossible;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@
@property (nonatomic) BOOL userEnabled;
@property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents;

@property (nonatomic, assign) NSInteger animationDuration;
@property (nonatomic, assign) CGFloat activeOpacity;
@property (nonatomic, assign) CGFloat defaultOpacity;
@property (nonatomic, assign) CGFloat activeScale;
@property (nonatomic, assign) CGFloat defaultScale;
@property (nonatomic, assign) CGFloat defaultUnderlayOpacity;
@property (nonatomic, assign) CGFloat activeUnderlayOpacity;
@property (nonatomic, strong, nullable) RNGHColor *underlayColor;

/**
* The view that press animations are applied to. Defaults to self; set by the
* Fabric component view to its own instance so animations affect the full wrapper.
*/
@property (nonatomic, weak, nullable) RNGHUIView *animationTarget;

/**
* Immediately applies the start* values to the animation target and underlay layer.
* Call after props are updated to ensure the idle visual state is correct.
*/
- (void)applyStartAnimationState;

#if TARGET_OS_OSX
- (void)mountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;
- (void)unmountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;
Expand Down
Loading
Loading