react-native-gesture-handler 3.0.0-beta.4 → 3.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +12 -4
  2. package/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +6 -2
  3. package/android/src/main/java/com/swmansion/gesturehandler/react/Extensions.kt +21 -0
  4. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +113 -49
  5. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +75 -98
  6. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt +7 -10
  7. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRegistry.kt +64 -2
  8. package/apple/RNGestureHandler.mm +50 -27
  9. package/apple/RNGestureHandlerButton.h +4 -2
  10. package/apple/RNGestureHandlerButton.mm +106 -27
  11. package/apple/RNGestureHandlerButtonComponentView.mm +17 -2
  12. package/apple/RNGestureHandlerDetector.mm +99 -75
  13. package/apple/RNGestureHandlerModule.mm +11 -14
  14. package/apple/RNGestureHandlerRegistry.h +14 -0
  15. package/apple/RNGestureHandlerRegistry.m +56 -0
  16. package/lib/module/RNGestureHandlerModule.web.js +5 -1
  17. package/lib/module/RNGestureHandlerModule.web.js.map +1 -1
  18. package/lib/module/components/GestureButtons.js +16 -5
  19. package/lib/module/components/GestureButtons.js.map +1 -1
  20. package/lib/module/components/GestureHandlerButton.js.map +1 -1
  21. package/lib/module/components/GestureHandlerButton.web.js +63 -23
  22. package/lib/module/components/GestureHandlerButton.web.js.map +1 -1
  23. package/lib/module/components/Pressable/Pressable.js +1 -0
  24. package/lib/module/components/Pressable/Pressable.js.map +1 -1
  25. package/lib/module/components/ReanimatedDrawerLayout.js.map +1 -1
  26. package/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js +38 -5
  27. package/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js.map +1 -1
  28. package/lib/module/handlers/gestures/GestureDetector/useDetectorUpdater.js +1 -2
  29. package/lib/module/handlers/gestures/GestureDetector/useDetectorUpdater.js.map +1 -1
  30. package/lib/module/handlers/gestures/GestureDetector/utils.js +0 -47
  31. package/lib/module/handlers/gestures/GestureDetector/utils.js.map +1 -1
  32. package/lib/module/handlers/gestures/reanimatedWrapper.js +14 -2
  33. package/lib/module/handlers/gestures/reanimatedWrapper.js.map +1 -1
  34. package/lib/module/mocks/module.js +3 -2
  35. package/lib/module/mocks/module.js.map +1 -1
  36. package/lib/module/specs/NativeRNGestureHandlerModule.js.map +1 -1
  37. package/lib/module/specs/RNGestureHandlerButtonNativeComponent.ts +28 -13
  38. package/lib/module/v3/NativeProxy.js +5 -3
  39. package/lib/module/v3/NativeProxy.js.map +1 -1
  40. package/lib/module/v3/NativeProxy.web.js +2 -2
  41. package/lib/module/v3/NativeProxy.web.js.map +1 -1
  42. package/lib/module/v3/components/GestureButtons.js +8 -3
  43. package/lib/module/v3/components/GestureButtons.js.map +1 -1
  44. package/lib/module/v3/components/Touchable/Touchable.js +53 -4
  45. package/lib/module/v3/components/Touchable/Touchable.js.map +1 -1
  46. package/lib/module/v3/detectors/HostGestureDetector.web.js +178 -59
  47. package/lib/module/v3/detectors/HostGestureDetector.web.js.map +1 -1
  48. package/lib/module/v3/detectors/NativeDetector.js +3 -2
  49. package/lib/module/v3/detectors/NativeDetector.js.map +1 -1
  50. package/lib/module/v3/detectors/VirtualDetector/InterceptingGestureDetector.js +3 -4
  51. package/lib/module/v3/detectors/VirtualDetector/InterceptingGestureDetector.js.map +1 -1
  52. package/lib/module/v3/detectors/VirtualDetector/VirtualDetector.js +2 -2
  53. package/lib/module/v3/detectors/VirtualDetector/VirtualDetector.js.map +1 -1
  54. package/lib/module/v3/detectors/useGestureRelationsUpdater.js +23 -0
  55. package/lib/module/v3/detectors/useGestureRelationsUpdater.js.map +1 -0
  56. package/lib/module/v3/detectors/utils.js +10 -8
  57. package/lib/module/v3/detectors/utils.js.map +1 -1
  58. package/lib/module/v3/hooks/useGesture.js +3 -18
  59. package/lib/module/v3/hooks/useGesture.js.map +1 -1
  60. package/lib/module/v3/hooks/utils/configUtils.js +1 -3
  61. package/lib/module/v3/hooks/utils/configUtils.js.map +1 -1
  62. package/lib/module/v3/hooks/utils/eventHandlersUtils.js +31 -29
  63. package/lib/module/v3/hooks/utils/eventHandlersUtils.js.map +1 -1
  64. package/lib/module/v3/hooks/utils/reanimatedUtils.js +8 -2
  65. package/lib/module/v3/hooks/utils/reanimatedUtils.js.map +1 -1
  66. package/lib/module/web/tools/NodeManager.js +44 -0
  67. package/lib/module/web/tools/NodeManager.js.map +1 -1
  68. package/lib/typescript/RNGestureHandlerModule.web.d.ts +1 -1
  69. package/lib/typescript/RNGestureHandlerModule.web.d.ts.map +1 -1
  70. package/lib/typescript/components/GestureButtons.d.ts +14 -6
  71. package/lib/typescript/components/GestureButtons.d.ts.map +1 -1
  72. package/lib/typescript/components/GestureHandlerButton.d.ts +62 -8
  73. package/lib/typescript/components/GestureHandlerButton.d.ts.map +1 -1
  74. package/lib/typescript/components/GestureHandlerButton.web.d.ts +10 -3
  75. package/lib/typescript/components/GestureHandlerButton.web.d.ts.map +1 -1
  76. package/lib/typescript/components/Pressable/Pressable.d.ts.map +1 -1
  77. package/lib/typescript/components/Pressable/PressableProps.d.ts +1 -1
  78. package/lib/typescript/components/Pressable/PressableProps.d.ts.map +1 -1
  79. package/lib/typescript/components/ReanimatedDrawerLayout.d.ts +16 -14
  80. package/lib/typescript/components/ReanimatedDrawerLayout.d.ts.map +1 -1
  81. package/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeable.d.ts +2 -1
  82. package/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeable.d.ts.map +1 -1
  83. package/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts +30 -34
  84. package/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts.map +1 -1
  85. package/lib/typescript/handlers/gestures/GestureDetector/useDetectorUpdater.d.ts.map +1 -1
  86. package/lib/typescript/handlers/gestures/GestureDetector/utils.d.ts +0 -1
  87. package/lib/typescript/handlers/gestures/GestureDetector/utils.d.ts.map +1 -1
  88. package/lib/typescript/handlers/gestures/reanimatedWrapper.d.ts.map +1 -1
  89. package/lib/typescript/mocks/module.d.ts +1 -1
  90. package/lib/typescript/mocks/module.d.ts.map +1 -1
  91. package/lib/typescript/specs/NativeRNGestureHandlerModule.d.ts +2 -2
  92. package/lib/typescript/specs/NativeRNGestureHandlerModule.d.ts.map +1 -1
  93. package/lib/typescript/specs/RNGestureHandlerButtonNativeComponent.d.ts +19 -11
  94. package/lib/typescript/specs/RNGestureHandlerButtonNativeComponent.d.ts.map +1 -1
  95. package/lib/typescript/v3/NativeProxy.d.ts +1 -1
  96. package/lib/typescript/v3/NativeProxy.d.ts.map +1 -1
  97. package/lib/typescript/v3/NativeProxy.web.d.ts +1 -1
  98. package/lib/typescript/v3/NativeProxy.web.d.ts.map +1 -1
  99. package/lib/typescript/v3/components/GestureButtons.d.ts +1 -38
  100. package/lib/typescript/v3/components/GestureButtons.d.ts.map +1 -1
  101. package/lib/typescript/v3/components/GestureButtonsProps.d.ts +1 -1
  102. package/lib/typescript/v3/components/GestureButtonsProps.d.ts.map +1 -1
  103. package/lib/typescript/v3/components/Touchable/Touchable.d.ts.map +1 -1
  104. package/lib/typescript/v3/components/Touchable/TouchableProps.d.ts +39 -1
  105. package/lib/typescript/v3/components/Touchable/TouchableProps.d.ts.map +1 -1
  106. package/lib/typescript/v3/detectors/HostGestureDetector.web.d.ts.map +1 -1
  107. package/lib/typescript/v3/detectors/NativeDetector.d.ts.map +1 -1
  108. package/lib/typescript/v3/detectors/VirtualDetector/InterceptingGestureDetector.d.ts.map +1 -1
  109. package/lib/typescript/v3/detectors/useGestureRelationsUpdater.d.ts +3 -0
  110. package/lib/typescript/v3/detectors/useGestureRelationsUpdater.d.ts.map +1 -0
  111. package/lib/typescript/v3/detectors/utils.d.ts +3 -3
  112. package/lib/typescript/v3/detectors/utils.d.ts.map +1 -1
  113. package/lib/typescript/v3/hooks/useGesture.d.ts.map +1 -1
  114. package/lib/typescript/v3/hooks/utils/configUtils.d.ts.map +1 -1
  115. package/lib/typescript/v3/hooks/utils/eventHandlersUtils.d.ts.map +1 -1
  116. package/lib/typescript/v3/hooks/utils/reanimatedUtils.d.ts +1 -0
  117. package/lib/typescript/v3/hooks/utils/reanimatedUtils.d.ts.map +1 -1
  118. package/lib/typescript/web/tools/NodeManager.d.ts +7 -0
  119. package/lib/typescript/web/tools/NodeManager.d.ts.map +1 -1
  120. package/package.json +3 -3
  121. package/src/RNGestureHandlerModule.web.ts +5 -1
  122. package/src/components/GestureButtons.tsx +23 -7
  123. package/src/components/GestureHandlerButton.tsx +70 -8
  124. package/src/components/GestureHandlerButton.web.tsx +97 -29
  125. package/src/components/Pressable/Pressable.tsx +1 -0
  126. package/src/components/Pressable/PressableProps.tsx +2 -1
  127. package/src/components/ReanimatedDrawerLayout.tsx +27 -23
  128. package/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx +51 -5
  129. package/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts +31 -39
  130. package/src/handlers/gestures/GestureDetector/useDetectorUpdater.ts +1 -2
  131. package/src/handlers/gestures/GestureDetector/utils.ts +0 -52
  132. package/src/handlers/gestures/reanimatedWrapper.ts +20 -2
  133. package/src/mocks/module.tsx +4 -2
  134. package/src/specs/NativeRNGestureHandlerModule.ts +2 -4
  135. package/src/specs/RNGestureHandlerButtonNativeComponent.ts +28 -13
  136. package/src/v3/NativeProxy.ts +9 -7
  137. package/src/v3/NativeProxy.web.ts +2 -2
  138. package/src/v3/components/GestureButtons.tsx +13 -5
  139. package/src/v3/components/GestureButtonsProps.ts +1 -0
  140. package/src/v3/components/Touchable/Touchable.tsx +65 -4
  141. package/src/v3/components/Touchable/TouchableProps.ts +49 -1
  142. package/src/v3/detectors/HostGestureDetector.web.tsx +265 -108
  143. package/src/v3/detectors/NativeDetector.tsx +3 -2
  144. package/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx +3 -4
  145. package/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +2 -2
  146. package/src/v3/detectors/useGestureRelationsUpdater.ts +30 -0
  147. package/src/v3/detectors/utils.ts +28 -12
  148. package/src/v3/hooks/useGesture.ts +4 -14
  149. package/src/v3/hooks/utils/configUtils.ts +2 -3
  150. package/src/v3/hooks/utils/eventHandlersUtils.ts +43 -32
  151. package/src/v3/hooks/utils/reanimatedUtils.ts +10 -10
  152. package/src/web/tools/NodeManager.ts +57 -0
  153. package/lib/module/RNRenderer.js +0 -6
  154. package/lib/module/RNRenderer.js.map +0 -1
  155. package/lib/module/RNRenderer.web.js +0 -6
  156. package/lib/module/RNRenderer.web.js.map +0 -1
  157. package/lib/typescript/RNRenderer.d.ts +0 -2
  158. package/lib/typescript/RNRenderer.d.ts.map +0 -1
  159. package/lib/typescript/RNRenderer.web.d.ts +0 -4
  160. package/lib/typescript/RNRenderer.web.d.ts.map +0 -1
  161. package/src/RNRenderer.ts +0 -3
  162. package/src/RNRenderer.web.ts +0 -3
@@ -28,7 +28,6 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) :
28
28
  @DoNotStrip
29
29
  @Suppress("unused")
30
30
  private var mHybridData: HybridData = initHybrid()
31
- private var isReanimatedAvailable = false
32
31
  private var uiRuntimeDecorated = false
33
32
  private val registry: RNGestureHandlerRegistry
34
33
  get() = registries[moduleId]!!
@@ -61,16 +60,10 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) :
61
60
  }
62
61
 
63
62
  @ReactMethod
64
- override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap): Boolean {
65
- if (isReanimatedAvailable && !uiRuntimeDecorated) {
66
- uiRuntimeDecorated = decorateUIRuntime()
67
- }
68
-
63
+ override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap) {
69
64
  val handlerTag = handlerTagDouble.toInt()
70
65
 
71
66
  createGestureHandlerHelper<GestureHandler>(handlerName, handlerTag, config)
72
-
73
- return true
74
67
  }
75
68
 
76
69
  @ReactMethod
@@ -125,8 +118,12 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) :
125
118
  override fun flushOperations() = Unit
126
119
 
127
120
  @ReactMethod
128
- override fun setReanimatedAvailable(isAvailable: Boolean) {
129
- isReanimatedAvailable = isAvailable
121
+ override fun installUIRuntimeBindings(): Boolean {
122
+ if (!uiRuntimeDecorated) {
123
+ uiRuntimeDecorated = decorateUIRuntime()
124
+ }
125
+
126
+ return uiRuntimeDecorated
130
127
  }
131
128
 
132
129
  @DoNotStrip
@@ -11,10 +11,72 @@ class RNGestureHandlerRegistry : GestureHandlerRegistry {
11
11
  private val handlers = SparseArray<GestureHandler>()
12
12
  private val attachedTo = SparseArray<Int?>()
13
13
  private val handlersForView = SparseArray<ArrayList<GestureHandler>>()
14
+ private val observers = mutableMapOf<Int, MutableMap<Any, (GestureHandler) -> Unit>>()
14
15
 
15
- @Synchronized
16
16
  fun registerHandler(handler: GestureHandler) {
17
- handlers.put(handler.tag, handler)
17
+ val hasObservers = synchronized(this) {
18
+ handlers.put(handler.tag, handler)
19
+ observers[handler.tag]?.isNotEmpty() == true
20
+ }
21
+
22
+ if (!hasObservers) {
23
+ return
24
+ }
25
+
26
+ // `createGestureHandler` runs on the JS thread, but observer callbacks read detector
27
+ // view state (childCount, getChildAt) and may attach native handlers, so they must run
28
+ // on the UI thread. Re-resolve the observer list on the UI thread so a cancellation that
29
+ // happens between this post and `notify` running (e.g. detector detach) actually prevents
30
+ // the callback.
31
+ val notify = {
32
+ val callbacks = synchronized(this) {
33
+ observers[handler.tag]?.values?.toList().orEmpty()
34
+ }
35
+ for (callback in callbacks) {
36
+ callback(handler)
37
+ }
38
+ }
39
+
40
+ if (UiThreadUtil.isOnUiThread()) {
41
+ notify()
42
+ } else {
43
+ UiThreadUtil.runOnUiThread(notify)
44
+ }
45
+ }
46
+
47
+ // Invokes `block` every time a handler with `tag` is registered, and synchronously once now if
48
+ // the handler already exists. The observation persists until explicitly cancelled: the registry
49
+ // holds both `owner` and `block` strongly, so callers MUST call `cancelObservation` or
50
+ // `cancelAllObservationsForOwner` when the owner is going away (typically in detach / dispose
51
+ // paths) to avoid leaking the owner. Observing the same tag twice with the same `owner` replaces
52
+ // the previous block.
53
+ fun observeHandler(tag: Int, owner: Any, block: (GestureHandler) -> Unit) {
54
+ val existing = synchronized(this) {
55
+ observers.getOrPut(tag) { mutableMapOf() }[owner] = block
56
+ handlers[tag]
57
+ }
58
+ existing?.let { block(it) }
59
+ }
60
+
61
+ @Synchronized
62
+ fun cancelObservation(tag: Int, owner: Any) {
63
+ val observersForTag = observers[tag] ?: return
64
+ observersForTag.remove(owner)
65
+ if (observersForTag.isEmpty()) {
66
+ observers.remove(tag)
67
+ }
68
+ }
69
+
70
+ @Synchronized
71
+ fun cancelAllObservationsForOwner(owner: Any) {
72
+ val iterator = observers.entries.iterator()
73
+ while (iterator.hasNext()) {
74
+ val entry = iterator.next()
75
+ entry.value.remove(owner)
76
+ if (entry.value.isEmpty()) {
77
+ iterator.remove()
78
+ }
79
+ }
18
80
  }
19
81
 
20
82
  @Synchronized
@@ -254,6 +254,43 @@ static NSHashTable<RNGestureHandler *> *allGestureHandlers;
254
254
  return [view isKindOfClass:[RCTParagraphComponentView class]];
255
255
  }
256
256
 
257
+ /**
258
+ * Recursively searches the view subtree rooted at `view` for any descendant whose
259
+ * `touchEventEmitterAtPoint:` returns an emitter tag matching `virtualViewTag`.
260
+ * `point` must be in `view`'s coordinate space.
261
+ *
262
+ * Most Fabric views inherit a base `touchEventEmitterAtPoint:` that returns their own emitter
263
+ * (tag == their own reactTag). Views that render multiple logical children — like
264
+ * `RCTParagraphComponentView` for inline text spans — override the method to return
265
+ * per-child emitters, making them distinguishable by tag. This helper exploits that
266
+ * property without hardcoding any specific view class.
267
+ */
268
+ - (BOOL)isVirtualViewTag:(NSNumber *)virtualViewTag touchedAtPoint:(CGPoint)point inView:(RNGHUIView *)view
269
+ {
270
+ if (!CGRectContainsPoint(view.bounds, point)) {
271
+ return NO;
272
+ }
273
+
274
+ if ([view respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
275
+ auto emitter = [(id<RCTTouchableComponentViewProtocol>)view touchEventEmitterAtPoint:point];
276
+ if (emitter) {
277
+ auto eventTarget = emitter->getEventTarget();
278
+ if (eventTarget != nullptr && eventTarget->getTag() == [virtualViewTag intValue]) {
279
+ return YES;
280
+ }
281
+ }
282
+ }
283
+
284
+ for (RNGHUIView *subview in view.subviews) {
285
+ CGPoint pointInSubview = [view convertPoint:point toView:subview];
286
+ if ([self isVirtualViewTag:virtualViewTag touchedAtPoint:pointInSubview inView:subview]) {
287
+ return YES;
288
+ }
289
+ }
290
+
291
+ return NO;
292
+ }
293
+
257
294
  - (void)bindToView:(RNGHUIView *)view
258
295
  {
259
296
  self.recognizer.delegate = self;
@@ -753,6 +790,11 @@ static NSHashTable<RNGestureHandler *> *allGestureHandlers;
753
790
 
754
791
  - (BOOL)containsPointInView
755
792
  {
793
+ if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {
794
+ CGPoint point = [_recognizer locationInView:_recognizer.view];
795
+ return [self isVirtualViewTag:_virtualViewTag touchedAtPoint:point inView:_recognizer.view];
796
+ }
797
+
756
798
  RNGHUIView *viewToHitTest = _recognizer.view;
757
799
 
758
800
  if (_shouldCancelWhenOutside && [self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) {
@@ -767,6 +809,11 @@ static NSHashTable<RNGestureHandler *> *allGestureHandlers;
767
809
 
768
810
  - (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point
769
811
  {
812
+ if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {
813
+ // point is in _recognizer.view (detector) coordinate space; search the whole subtree
814
+ return [self isVirtualViewTag:_virtualViewTag touchedAtPoint:point inView:_recognizer.view];
815
+ }
816
+
770
817
  RNGHUIView *viewToHitTest = _recognizer.view;
771
818
 
772
819
  if ([self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) {
@@ -774,20 +821,6 @@ static NSHashTable<RNGestureHandler *> *allGestureHandlers;
774
821
  point = [_recognizer.view convertPoint:point toView:viewToHitTest];
775
822
  }
776
823
 
777
- if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {
778
- // In this case, logic detector is attached to the DetectorView, which has a single subview representing
779
- // the actual target view in the RN hierarchy
780
- if ([viewToHitTest respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
781
- // If the view has touchEventEmitterAtPoint: method, it can be used to determine the viewtag
782
- // of the view under the touch point
783
- facebook::react::SharedTouchEventEmitter eventEmitter =
784
- [(id<RCTTouchableComponentViewProtocol>)viewToHitTest touchEventEmitterAtPoint:point];
785
- auto viewUnderTouch = eventEmitter->getEventTarget()->getTag();
786
-
787
- return viewUnderTouch == [_virtualViewTag intValue];
788
- }
789
- }
790
-
791
824
  CGRect hitFrame = RNGHHitSlopInsetRect(viewToHitTest.bounds, _hitSlop);
792
825
  return CGRectContainsPoint(hitFrame, point);
793
826
  }
@@ -810,19 +843,9 @@ static NSHashTable<RNGestureHandler *> *allGestureHandlers;
810
843
 
811
844
  // Logic detector has a virtual view tag set only if the real hierarchy was folded into a single View
812
845
  if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {
813
- // In this case, logic detector is attached to the DetectorView, which has a single subview representing
814
- // the actual target view in the RN hierarchy
815
- RNGHUIView *view = _recognizer.view.subviews[0];
816
- if ([view respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
817
- // If the view has touchEventEmitterAtPoint: method, it can be used to determine the viewtag
818
- // of the view under the touch point
819
- facebook::react::SharedTouchEventEmitter eventEmitter =
820
- [(id<RCTTouchableComponentViewProtocol>)view touchEventEmitterAtPoint:[_recognizer locationInView:view]];
821
- auto viewUnderTouch = eventEmitter->getEventTarget()->getTag();
822
-
823
- if (viewUnderTouch != [_virtualViewTag intValue]) {
824
- return NO;
825
- }
846
+ CGPoint point = [_recognizer locationInView:_recognizer.view];
847
+ if (![self isVirtualViewTag:_virtualViewTag touchedAtPoint:point inView:_recognizer.view]) {
848
+ return NO;
826
849
  }
827
850
  }
828
851
 
@@ -27,8 +27,10 @@
27
27
  @property (nonatomic) BOOL userEnabled;
28
28
  @property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents;
29
29
 
30
- @property (nonatomic, assign) NSInteger pressAndHoldAnimationDuration;
31
- @property (nonatomic, assign) NSInteger tapAnimationDuration;
30
+ @property (nonatomic, assign) NSInteger tapAnimationInDuration;
31
+ @property (nonatomic, assign) NSInteger tapAnimationOutDuration;
32
+ @property (nonatomic, assign) NSInteger longPressDuration;
33
+ @property (nonatomic, assign) NSInteger longPressAnimationOutDuration;
32
34
  @property (nonatomic, assign) CGFloat activeOpacity;
33
35
  @property (nonatomic, assign) CGFloat defaultOpacity;
34
36
  @property (nonatomic, assign) CGFloat activeScale;
@@ -49,7 +49,7 @@
49
49
  dispatch_block_t _pendingPressOutBlock;
50
50
  }
51
51
 
52
- @synthesize pressAndHoldAnimationDuration = _pressAndHoldAnimationDuration;
52
+ @synthesize longPressAnimationOutDuration = _longPressAnimationOutDuration;
53
53
 
54
54
  - (void)commonInit
55
55
  {
@@ -57,8 +57,10 @@
57
57
  _hitTestEdgeInsets = UIEdgeInsetsZero;
58
58
  _userEnabled = YES;
59
59
  _pointerEvents = RNGestureHandlerPointerEventsAuto;
60
- _pressAndHoldAnimationDuration = -1;
61
- _tapAnimationDuration = 100;
60
+ _tapAnimationInDuration = 50;
61
+ _tapAnimationOutDuration = 100;
62
+ _longPressDuration = -1;
63
+ _longPressAnimationOutDuration = -1;
62
64
  _activeOpacity = 1.0;
63
65
  _defaultOpacity = 1.0;
64
66
  _activeScale = 1.0;
@@ -145,6 +147,10 @@
145
147
  [super viewWillMoveToWindow:newWindow];
146
148
  if (newWindow == nil) {
147
149
  [self cancelPendingPressOutAnimation];
150
+ [self applyStartAnimationState];
151
+ _isTouchInsideBounds = NO;
152
+ _suppressSuperControlActionDispatch = NO;
153
+ _pressInTimestamp = 0;
148
154
  }
149
155
  }
150
156
  #else
@@ -153,13 +159,17 @@
153
159
  [super willMoveToWindow:newWindow];
154
160
  if (newWindow == nil) {
155
161
  [self cancelPendingPressOutAnimation];
162
+ [self applyStartAnimationState];
163
+ _isTouchInsideBounds = NO;
164
+ _suppressSuperControlActionDispatch = NO;
165
+ _pressInTimestamp = 0;
156
166
  }
157
167
  }
158
168
  #endif
159
169
 
160
- - (NSInteger)pressAndHoldAnimationDuration
170
+ - (NSInteger)longPressAnimationOutDuration
161
171
  {
162
- return _pressAndHoldAnimationDuration < 0 ? _tapAnimationDuration : _pressAndHoldAnimationDuration;
172
+ return _longPressAnimationOutDuration < 0 ? _tapAnimationOutDuration : _longPressAnimationOutDuration;
163
173
  }
164
174
 
165
175
  - (void)setUnderlayColor:(RNGHColor *)underlayColor
@@ -193,8 +203,13 @@
193
203
 
194
204
  - (void)animateUnderlayToOpacity:(float)toOpacity duration:(NSTimeInterval)durationMs
195
205
  {
196
- _underlayLayer.opacity =
197
- _underlayLayer.presentationLayer ? [_underlayLayer.presentationLayer opacity] : _underlayLayer.opacity;
206
+ // Only sync the model from the presentation layer when an animation is actually
207
+ // in flight.
208
+ CALayer *presentation = _underlayLayer.presentationLayer;
209
+ BOOL hasInFlightAnimation = presentation != nil && _underlayLayer.animationKeys.count > 0;
210
+ if (hasInFlightAnimation) {
211
+ _underlayLayer.opacity = presentation.opacity;
212
+ }
198
213
  [_underlayLayer removeAllAnimations];
199
214
 
200
215
  // CABasicAnimation with duration 0 resolves to the current CATransaction's
@@ -256,19 +271,60 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
256
271
  #endif
257
272
  }
258
273
 
274
+ // Duration of a single frame at the current screen's max refresh rate, in ms.
275
+ - (NSTimeInterval)minFrameDurationMs
276
+ {
277
+ #if !TARGET_OS_OSX
278
+ UIScreen *screen = self.window.screen ?: UIScreen.mainScreen;
279
+ NSInteger maxFps = screen.maximumFramesPerSecond;
280
+ #else
281
+ NSScreen *screen = self.window.screen ?: NSScreen.mainScreen;
282
+ NSInteger maxFps = 60;
283
+ if (@available(macOS 12.0, *)) {
284
+ maxFps = screen.maximumFramesPerSecond;
285
+ }
286
+ #endif
287
+ return maxFps > 0 ? 1000.0 / (NSTimeInterval)maxFps : 1000.0 / 60.0;
288
+ }
289
+
259
290
  - (void)animateTarget:(RNGHUIView *)target
260
291
  toOpacity:(CGFloat)opacity
261
292
  scale:(CGFloat)scale
262
293
  duration:(NSTimeInterval)durationMs
263
294
  {
264
- target.layer.transform =
265
- target.layer.presentationLayer ? target.layer.presentationLayer.transform : target.layer.transform;
266
- NSTimeInterval duration = durationMs / 1000.0;
295
+ CALayer *layer = target.layer;
296
+ CALayer *presentation = layer.presentationLayer;
297
+ NSTimeInterval snapThresholdMs = [self minFrameDurationMs];
298
+
299
+ // Only snap to the presentation layer when an animation is in flight,
300
+ // that's the only case where it tells us something the model layer doesn't.
301
+ BOOL hasInFlightAnimation = presentation != nil && layer.animationKeys.count > 0;
302
+ if (hasInFlightAnimation) {
303
+ layer.transform = presentation.transform;
304
+ }
267
305
 
268
306
  #if !TARGET_OS_OSX
269
- target.alpha = target.layer.presentationLayer ? target.layer.presentationLayer.opacity : target.alpha;
270
- [target.layer removeAllAnimations];
307
+ if (hasInFlightAnimation) {
308
+ target.alpha = presentation.opacity;
309
+ }
310
+ [layer removeAllAnimations];
311
+
312
+ // Sub-frame durations: snap with implicit actions disabled instead of
313
+ // routing through UIView.animate. Same rationale as animateUnderlayToOpacity.
314
+ if (durationMs < snapThresholdMs) {
315
+ [CATransaction begin];
316
+ [CATransaction setDisableActions:YES];
317
+ if (_activeOpacity != 1.0 || _defaultOpacity != 1.0) {
318
+ target.alpha = opacity;
319
+ }
320
+ if (_activeScale != 1.0 || _defaultScale != 1.0) {
321
+ layer.transform = CATransform3DMakeScale(scale, scale, 1.0);
322
+ }
323
+ [CATransaction commit];
324
+ return;
325
+ }
271
326
 
327
+ NSTimeInterval duration = durationMs / 1000.0;
272
328
  [UIView animateWithDuration:duration
273
329
  delay:0
274
330
  options:UIViewAnimationOptionCurveEaseInOut
@@ -283,9 +339,25 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
283
339
  completion:nil];
284
340
  #else
285
341
  target.wantsLayer = YES;
286
- target.alphaValue = target.layer.presentationLayer ? target.layer.presentationLayer.opacity : target.alphaValue;
287
- [target.layer removeAllAnimations];
342
+ if (hasInFlightAnimation) {
343
+ target.alphaValue = presentation.opacity;
344
+ }
345
+ [layer removeAllAnimations];
288
346
 
347
+ if (durationMs < snapThresholdMs) {
348
+ [CATransaction begin];
349
+ [CATransaction setDisableActions:YES];
350
+ if (_activeOpacity != 1.0 || _defaultOpacity != 1.0) {
351
+ target.alphaValue = opacity;
352
+ }
353
+ if (_activeScale != 1.0 || _defaultScale != 1.0) {
354
+ layer.transform = RNGHCenterScaleTransform(target.bounds, scale);
355
+ }
356
+ [CATransaction commit];
357
+ return;
358
+ }
359
+
360
+ NSTimeInterval duration = durationMs / 1000.0;
289
361
  [NSAnimationContext
290
362
  runAnimationGroup:^(NSAnimationContext *context) {
291
363
  context.allowsImplicitAnimation = YES;
@@ -310,9 +382,9 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
310
382
  }
311
383
  _pressInTimestamp = CACurrentMediaTime();
312
384
  RNGHUIView *target = self.animationTarget ?: self;
313
- [self animateTarget:target toOpacity:_activeOpacity scale:_activeScale duration:self.pressAndHoldAnimationDuration];
385
+ [self animateTarget:target toOpacity:_activeOpacity scale:_activeScale duration:_tapAnimationInDuration];
314
386
  if (_activeUnderlayOpacity != _defaultUnderlayOpacity) {
315
- [self animateUnderlayToOpacity:_activeUnderlayOpacity duration:self.pressAndHoldAnimationDuration];
387
+ [self animateUnderlayToOpacity:_activeUnderlayOpacity duration:_tapAnimationInDuration];
316
388
  }
317
389
  }
318
390
 
@@ -323,17 +395,24 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
323
395
  }
324
396
 
325
397
  NSTimeInterval elapsed = (CACurrentMediaTime() - _pressInTimestamp) * 1000.0;
326
- NSInteger pressAndHoldAnimationDuration = self.pressAndHoldAnimationDuration;
327
398
 
328
- if (elapsed >= pressAndHoldAnimationDuration) {
329
- // Press-in animation fully finished, animate out in pressAndHoldAnimationDuration
399
+ if (_longPressDuration >= 0 && elapsed >= _longPressDuration) {
400
+ // Long-press release - use the configured long-press out duration.
401
+ NSInteger longPressOut = self.longPressAnimationOutDuration;
402
+ RNGHUIView *target = self.animationTarget ?: self;
403
+ [self animateTarget:target toOpacity:_defaultOpacity scale:_defaultScale duration:longPressOut];
404
+ if (_activeUnderlayOpacity != _defaultUnderlayOpacity) {
405
+ [self animateUnderlayToOpacity:_defaultUnderlayOpacity duration:longPressOut];
406
+ }
407
+ } else if (elapsed >= _tapAnimationInDuration) {
408
+ // Press-in animation fully finished - release with the configured out duration.
330
409
  RNGHUIView *target = self.animationTarget ?: self;
331
- [self animateTarget:target toOpacity:_defaultOpacity scale:_defaultScale duration:pressAndHoldAnimationDuration];
410
+ [self animateTarget:target toOpacity:_defaultOpacity scale:_defaultScale duration:_tapAnimationOutDuration];
332
411
  if (_activeUnderlayOpacity != _defaultUnderlayOpacity) {
333
- [self animateUnderlayToOpacity:_defaultUnderlayOpacity duration:pressAndHoldAnimationDuration];
412
+ [self animateUnderlayToOpacity:_defaultUnderlayOpacity duration:_tapAnimationOutDuration];
334
413
  }
335
- // elapsed * 2 to ensure there is at least half of the minDuration left for the animation to play
336
- } else if (elapsed * 2 >= _tapAnimationDuration) {
414
+ // elapsed * 2 to ensure there is at least half of the tapAnimationOutDuration left for the animation to play
415
+ } else if (elapsed * 2 >= _tapAnimationOutDuration) {
337
416
  // Past minimum but press-in animation still playing, animate out in elapsed time
338
417
  RNGHUIView *target = self.animationTarget ?: self;
339
418
  [self animateTarget:target toOpacity:_defaultOpacity scale:_defaultScale duration:elapsed];
@@ -341,8 +420,8 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
341
420
  [self animateUnderlayToOpacity:_defaultUnderlayOpacity duration:elapsed];
342
421
  }
343
422
  } else {
344
- // Before minimum duration, finish press-in in remaining time then animate out in minDuration
345
- NSTimeInterval remaining = _tapAnimationDuration - elapsed;
423
+ // Before minimum duration, finish press-in in remaining time then animate out in tapAnimationOutDuration.
424
+ NSTimeInterval remaining = _tapAnimationInDuration - elapsed;
346
425
 
347
426
  RNGHUIView *target = self.animationTarget ?: self;
348
427
  [self animateTarget:target toOpacity:_activeOpacity scale:_activeScale duration:remaining];
@@ -359,10 +438,10 @@ static CATransform3D RNGHCenterScaleTransform(NSRect bounds, CGFloat scale)
359
438
  [strongSelf animateTarget:target
360
439
  toOpacity:strongSelf->_defaultOpacity
361
440
  scale:strongSelf->_defaultScale
362
- duration:strongSelf->_tapAnimationDuration];
441
+ duration:strongSelf->_tapAnimationOutDuration];
363
442
  if (strongSelf->_activeUnderlayOpacity != strongSelf->_defaultUnderlayOpacity) {
364
443
  [strongSelf animateUnderlayToOpacity:strongSelf->_defaultUnderlayOpacity
365
- duration:strongSelf->_tapAnimationDuration];
444
+ duration:strongSelf->_tapAnimationOutDuration];
366
445
  }
367
446
  }
368
447
  });
@@ -323,8 +323,10 @@ static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::Poi
323
323
  }
324
324
 
325
325
  _buttonView.userEnabled = newProps.enabled;
326
- _buttonView.pressAndHoldAnimationDuration = newProps.pressAndHoldAnimationDuration;
327
- _buttonView.tapAnimationDuration = newProps.tapAnimationDuration > 0 ? newProps.tapAnimationDuration : 0;
326
+ _buttonView.tapAnimationInDuration = newProps.tapAnimationInDuration > 0 ? newProps.tapAnimationInDuration : 0;
327
+ _buttonView.tapAnimationOutDuration = newProps.tapAnimationOutDuration > 0 ? newProps.tapAnimationOutDuration : 0;
328
+ _buttonView.longPressDuration = newProps.longPressDuration;
329
+ _buttonView.longPressAnimationOutDuration = newProps.longPressAnimationOutDuration;
328
330
  _buttonView.activeOpacity = newProps.activeOpacity;
329
331
  _buttonView.defaultOpacity = newProps.defaultOpacity;
330
332
  _buttonView.activeScale = newProps.activeScale;
@@ -358,6 +360,19 @@ static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::Poi
358
360
  }
359
361
 
360
362
  [super updateProps:props oldProps:oldProps];
363
+
364
+ #if !TARGET_OS_TV && !TARGET_OS_OSX
365
+ // super's updateProps sets self.accessibilityIdentifier from testID via the
366
+ // standard Fabric mechanism. However, setAccessibilityProps already forwards
367
+ // testID to _buttonView.accessibilityIdentifier (the actual button element).
368
+ // Having the identifier on both views causes testing frameworks (e.g. Detox)
369
+ // to report multiple matches for the same testID. Clear it from the wrapper so
370
+ // only _buttonView carries the identifier.
371
+ if (!newProps.testId.empty()) {
372
+ self.accessibilityIdentifier = nil;
373
+ }
374
+ #endif
375
+
361
376
  if (shouldApplyStartAnimationState) {
362
377
  [_buttonView applyStartAnimationState];
363
378
  }