Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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