@@ -26,15 +26,19 @@ import android.view.accessibility.AccessibilityNodeInfo
2626import androidx.core.view.children
2727import androidx.interpolator.view.animation.FastOutSlowInInterpolator
2828import com.facebook.react.R
29+ import com.facebook.react.bridge.Arguments
2930import com.facebook.react.bridge.Dynamic
31+ import com.facebook.react.bridge.ReactContext
3032import com.facebook.react.bridge.ReadableArray
33+ import com.facebook.react.bridge.ReadableMap
3134import com.facebook.react.module.annotations.ReactModule
3235import com.facebook.react.uimanager.BackgroundStyleApplicator
3336import com.facebook.react.uimanager.LengthPercentage
3437import com.facebook.react.uimanager.PixelUtil
3538import com.facebook.react.uimanager.PointerEvents
3639import com.facebook.react.uimanager.ReactPointerEventsView
3740import com.facebook.react.uimanager.ThemedReactContext
41+ import com.facebook.react.uimanager.UIManagerHelper
3842import com.facebook.react.uimanager.ViewGroupManager
3943import com.facebook.react.uimanager.ViewManagerDelegate
4044import com.facebook.react.uimanager.ViewProps
@@ -48,6 +52,7 @@ import com.swmansion.gesturehandler.core.GestureHandler
4852import com.swmansion.gesturehandler.core.HoverGestureHandler
4953import com.swmansion.gesturehandler.core.NativeViewGestureHandler
5054import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager.ButtonViewGroup
55+ import com.swmansion.gesturehandler.react.events.RNGestureHandlerButtonEvent
5156
5257@ReactModule(name = RNGestureHandlerButtonViewManager .REACT_CLASS )
5358class RNGestureHandlerButtonViewManager :
@@ -63,6 +68,90 @@ class RNGestureHandlerButtonViewManager :
6368
6469 public override fun createViewInstance (context : ThemedReactContext ) = ButtonViewGroup (context)
6570
71+ override fun onDropViewInstance (view : ButtonViewGroup ) {
72+ view.managedHandlerTag?.let { tag ->
73+ getGestureHandlerModule(view).dropGestureHandler(tag)
74+ }
75+
76+ super .onDropViewInstance(view)
77+ }
78+
79+ private fun getGestureHandlerModule (view : ButtonViewGroup ) =
80+ (view.context as ReactContext ).getNativeModule(RNGestureHandlerModule ::class .java)!!
81+
82+ private fun updateManagedHandlerConfig (view : ButtonViewGroup , config : ReadableMap ) {
83+ view.managedHandlerTag?.let { getGestureHandlerModule(view).updateGestureHandlerConfig(it, config) }
84+ }
85+
86+ @ReactProp(name = " handlerTag" )
87+ override fun setHandlerTag (view : ButtonViewGroup , handlerTag : Double ) {
88+ if (view.managedHandlerTag == handlerTag) {
89+ return
90+ }
91+
92+ view.managedHandlerTag?.let { oldTag ->
93+ getGestureHandlerModule(view).dropGestureHandler(oldTag)
94+ }
95+
96+ view.managedHandlerTag = handlerTag
97+ val module = getGestureHandlerModule(view)
98+
99+ module.createGestureHandler(
100+ " NativeViewGestureHandler" ,
101+ handlerTag,
102+ Arguments .createMap().apply {
103+ putBoolean(" shouldActivateOnStart" , false )
104+ putBoolean(" disallowInterruption" , true )
105+ putBoolean(" yieldsToContinuousGestures" , true )
106+ putBoolean(" enabled" , view.isEnabled)
107+ view.managedHandlerCancelOnLeave?.let { putBoolean(" shouldCancelWhenOutside" , it) }
108+ view.managedHandlerTestID?.let { putString(" testID" , it) }
109+ view.managedHandlerHitSlop?.let { putMap(" hitSlop" , it) }
110+ },
111+ )
112+ module.attachGestureHandler(handlerTag, view.id.toDouble(), GestureHandler .ACTION_TYPE_NONE .toDouble())
113+ }
114+
115+ @ReactProp(name = " cancelOnLeave" )
116+ override fun setCancelOnLeave (view : ButtonViewGroup , cancelOnLeave : Boolean ) {
117+ view.managedHandlerCancelOnLeave = cancelOnLeave
118+ updateManagedHandlerConfig(
119+ view,
120+ Arguments .createMap().apply {
121+ putBoolean(" shouldCancelWhenOutside" , cancelOnLeave)
122+ },
123+ )
124+ }
125+
126+ @ReactProp(name = " gestureTestID" )
127+ override fun setGestureTestID (view : ButtonViewGroup , gestureTestID : String? ) {
128+ view.managedHandlerTestID = gestureTestID
129+ updateManagedHandlerConfig(
130+ view,
131+ Arguments .createMap().apply {
132+ putString(" testID" , gestureTestID)
133+ },
134+ )
135+ }
136+
137+ @ReactProp(name = " gestureHitSlop" )
138+ override fun setGestureHitSlop (view : ButtonViewGroup , gestureHitSlop : ReadableMap ? ) {
139+ view.managedHandlerHitSlop = gestureHitSlop
140+ if (gestureHitSlop != null ) {
141+ updateManagedHandlerConfig(
142+ view,
143+ Arguments .createMap().apply {
144+ putMap(" hitSlop" , gestureHitSlop)
145+ },
146+ )
147+ }
148+ }
149+
150+ @ReactProp(name = " hasLongPressHandler" )
151+ override fun setHasLongPressHandler (view : ButtonViewGroup , hasLongPressHandler : Boolean ) {
152+ view.hasLongPressHandler = hasLongPressHandler
153+ }
154+
66155 @ReactProp(name = " foreground" )
67156 override fun setForeground (view : ButtonViewGroup , useDrawableOnForeground : Boolean ) {
68157 view.useDrawableOnForeground = useDrawableOnForeground
@@ -81,6 +170,13 @@ class RNGestureHandlerButtonViewManager :
81170 @ReactProp(name = " enabled" )
82171 override fun setEnabled (view : ButtonViewGroup , enabled : Boolean ) {
83172 view.isEnabled = enabled
173+
174+ updateManagedHandlerConfig(
175+ view,
176+ Arguments .createMap().apply {
177+ putBoolean(" enabled" , enabled)
178+ },
179+ )
84180 }
85181
86182 @ReactProp(name = " borderWidth" )
@@ -397,7 +493,18 @@ class RNGestureHandlerButtonViewManager :
397493 }
398494 var useBorderlessDrawable = false
399495
496+ var managedHandlerTag: Double? = null
497+
498+ // Config props may be applied before `handlerTag` (prop order is not guaranteed) and
499+ // `updateManagedHandlerConfig` no-ops until the handler exists, so the values are cached
500+ // here and seeded into the config when `setHandlerTag` creates the handler. This also
501+ // re-applies them when the handler is recreated for a new tag.
502+ var managedHandlerCancelOnLeave: Boolean? = null
503+ var managedHandlerTestID: String? = null
504+ var managedHandlerHitSlop: ReadableMap ? = null
505+
400506 var exclusive = true
507+ var hasLongPressHandler = false
401508 var tapAnimationInDuration: Int = 50
402509 var tapAnimationOutDuration: Int = 100
403510 var longPressDuration: Int = - 1
@@ -436,6 +543,7 @@ class RNGestureHandlerButtonViewManager :
436543 private var underlayDrawable: PaintDrawable ? = null
437544 private var pressInTimestamp = 0L
438545 private var pendingPressOut: Runnable ? = null
546+ private var pendingLongPress: Runnable ? = null
439547 private var pendingHoverOut: Choreographer .FrameCallback ? = null
440548 private var isPointerInsideBounds = false
441549 private var isHovered = false
@@ -520,6 +628,92 @@ class RNGestureHandlerButtonViewManager :
520628 }
521629 }
522630
631+ private var longPressDetected = false
632+
633+ // Cannot rely on isPointerInsideBounds because it's set to false during motion event processing
634+ // which happens before the final state change is handled (state change is triggered by the motion
635+ // event).
636+ private var lastEventWasInside = false
637+
638+ override fun onHandlerUpdate (handler : NativeViewGestureHandler ) {
639+ if (managedHandlerTag == null || handler.isWithinBounds == lastEventWasInside) {
640+ return
641+ }
642+
643+ if (handler.isWithinBounds) {
644+ dispatchJSEvent(EventType .PressIn , handler)
645+ } else {
646+ dispatchJSEvent(EventType .PressOut , handler)
647+
648+ pendingLongPress?.let {
649+ this .handler?.removeCallbacks(it)
650+ pendingLongPress = null
651+ }
652+ }
653+ }
654+
655+ override fun onHandlerStateChange (handler : NativeViewGestureHandler , newState : Int , prevState : Int ) {
656+ if (managedHandlerTag == null ) {
657+ return
658+ }
659+
660+ // Capture local copy, since lastEventWasInside can change during this method
661+ // Specifically PressOut -> Press scenario on STATE_END
662+ val localLastEventWasInside = lastEventWasInside
663+
664+ if (newState == GestureHandler .STATE_BEGAN ) {
665+ dispatchJSEvent(EventType .PressIn , handler)
666+ longPressDetected = false
667+
668+ if (hasLongPressHandler && longPressDuration >= 0 ) {
669+ val runnable = Runnable {
670+ pendingLongPress = null
671+ longPressDetected = true
672+ dispatchJSEvent(EventType .LongPress , handler)
673+ }
674+ pendingLongPress = runnable
675+ this .handler?.postDelayed(runnable, longPressDuration.toLong())
676+ }
677+ }
678+
679+ if (newState == GestureHandler .STATE_END ||
680+ newState == GestureHandler .STATE_FAILED ||
681+ newState == GestureHandler .STATE_CANCELLED
682+ ) {
683+ if (localLastEventWasInside) {
684+ dispatchJSEvent(EventType .PressOut , handler)
685+ }
686+
687+ pendingLongPress?.let {
688+ this .handler?.removeCallbacks(it)
689+ pendingLongPress = null
690+ }
691+ }
692+
693+ if (newState == GestureHandler .STATE_END && ! longPressDetected && localLastEventWasInside) {
694+ dispatchJSEvent(EventType .Press , handler)
695+ }
696+
697+ if (newState == GestureHandler .STATE_END ||
698+ newState == GestureHandler .STATE_FAILED ||
699+ newState == GestureHandler .STATE_CANCELLED
700+ ) {
701+ dispatchJSEvent(EventType .InteractionFinished , handler)
702+ }
703+ }
704+
705+ private fun dispatchJSEvent (type : EventType , handler : NativeViewGestureHandler ) {
706+ val reactContext = context as ? ReactContext ? : return
707+ val eventDispatcher = UIManagerHelper .getEventDispatcher(reactContext) ? : return
708+ eventDispatcher.dispatchEvent(RNGestureHandlerButtonEvent .obtain(this , handler, type))
709+
710+ if (type == EventType .PressIn ) {
711+ lastEventWasInside = true
712+ } else if (type == EventType .PressOut ) {
713+ lastEventWasInside = false
714+ }
715+ }
716+
523717 override fun onInterceptTouchEvent (event : MotionEvent ): Boolean {
524718 if (super .onInterceptTouchEvent(event)) {
525719 return true
@@ -912,6 +1106,8 @@ class RNGestureHandlerButtonViewManager :
9121106 override fun onDetachedFromWindow () {
9131107 pendingPressOut?.let { handler?.removeCallbacks(it) }
9141108 pendingPressOut = null
1109+ pendingLongPress?.let { handler?.removeCallbacks(it) }
1110+ pendingLongPress = null
9151111 cancelPendingHoverOut()
9161112 currentAnimator?.cancel()
9171113 currentAnimator = null
@@ -1104,6 +1300,14 @@ class RNGestureHandlerButtonViewManager :
11041300 }
11051301 }
11061302 }
1303+
1304+ enum class EventType {
1305+ Press ,
1306+ PressIn ,
1307+ PressOut ,
1308+ LongPress ,
1309+ InteractionFinished ,
1310+ }
11071311 }
11081312
11091313 companion object {
0 commit comments