diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 1c4fb2a590..1d7175887f 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -31,13 +31,26 @@ open class GestureHandler { var tag = 0 var view: View? = null private set + + // Host detector view is a reference to a Native Detector designated to handle events from a + // Logic Detector to which the gesture is assigned. + var hostDetectorView: RNGestureHandlerDetectorView? = null + val viewForEvents: RNGestureHandlerDetectorView get() { - assert(actionType == ACTION_TYPE_NATIVE_DETECTOR) { + assert(usesNativeOrLogicDetector(actionType)) { "[react-native-gesture-handler] `viewForEvents` can only be used with NativeDetector." } - val detector = if (this is NativeViewGestureHandler) this.view?.parent else view + val detector = if (actionType == + ACTION_TYPE_LOGIC_DETECTOR + ) { + this.hostDetectorView + } else if (this is NativeViewGestureHandler) { + this.view?.parent + } else { + view + } if (detector !is RNGestureHandlerDetectorView) { throw Error( @@ -1005,6 +1018,7 @@ open class GestureHandler { const val ACTION_TYPE_JS_FUNCTION_OLD_API = 3 const val ACTION_TYPE_JS_FUNCTION_NEW_API = 4 const val ACTION_TYPE_NATIVE_DETECTOR = 5 + const val ACTION_TYPE_LOGIC_DETECTOR = 6 const val POINTER_TYPE_TOUCH = 0 const val POINTER_TYPE_STYLUS = 1 const val POINTER_TYPE_MOUSE = 2 @@ -1039,6 +1053,9 @@ open class GestureHandler { } return null } + + fun usesNativeOrLogicDetector(actionType: Int): Boolean = + actionType == ACTION_TYPE_NATIVE_DETECTOR || actionType == ACTION_TYPE_LOGIC_DETECTOR } private data class PointerData( diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt index 11d641b02d..f2f15d9711 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt @@ -15,8 +15,11 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { private var handlersToAttach: List? = null private var nativeHandlers: MutableSet = mutableSetOf() private var attachedHandlers: MutableSet = mutableSetOf() + private var attachedLogicHandlers: MutableMap> = mutableMapOf() private var moduleId: Int = -1 + data class LogicChildren(val handlerTags: List, val viewTag: Int) + fun setHandlerTags(handlerTags: ReadableArray?) { val newHandlers = handlerTags?.toArrayList()?.map { (it as Double).toInt() } ?: emptyList() if (moduleId == -1) { @@ -37,6 +40,35 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { handlersToAttach = null } + fun setLogicChildren(newLogicChildren: ReadableArray?) { + val logicChildrenToDetach = attachedLogicHandlers.keys.toMutableSet() + + val mappedChildren = newLogicChildren?.mapLogicChildren().orEmpty() + + for (child in mappedChildren) { + if (!attachedLogicHandlers.containsKey(child.viewTag)) { + attachedLogicHandlers[child.viewTag] = mutableSetOf() + } + + logicChildrenToDetach.remove(child.viewTag) + + attachHandlers( + child.handlerTags, + child.viewTag, + GestureHandler.ACTION_TYPE_LOGIC_DETECTOR, + attachedLogicHandlers[child.viewTag]!!, + ) + } + + val registry = RNGestureHandlerModule.registries[moduleId] + ?: throw Exception("Tried to access a non-existent registry") + + for (tag in logicChildrenToDetach) { + registry.detachHandler(tag) + attachedLogicHandlers.remove(tag) + } + } + private fun shouldAttachGestureToChildView(tag: Int): Boolean { val registry = RNGestureHandlerModule.registries[moduleId] ?: throw Exception("Tried to access a non-existent registry") @@ -57,39 +89,40 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { super.removeViewAt(index) } - private fun attachHandlers(newHandlers: List) { + private fun attachHandlers( + newHandlers: List, + viewTag: Int = this.id, + actionType: Int = GestureHandler.ACTION_TYPE_NATIVE_DETECTOR, + attachedHandlers: MutableSet = this.attachedHandlers, + ) { val registry = RNGestureHandlerModule.registries[moduleId] ?: throw Exception("Tried to access a non-existent registry") - val changes = mutableMapOf() - - for (tag in attachedHandlers) { - changes[tag] = GestureHandlerMutation.Detach - } + val handlersToDetach = attachedHandlers.toMutableSet() for (tag in newHandlers) { - changes[tag] = if (changes.containsKey(tag)) GestureHandlerMutation.Keep else GestureHandlerMutation.Attach - } - - for (entry in changes) { - val tag = entry.key - - if (entry.value == GestureHandlerMutation.Attach) { + handlersToDetach.remove(tag) + if (!attachedHandlers.contains(tag)) { if (shouldAttachGestureToChildView(tag)) { // It might happen that `attachHandlers` will be called before children are added into view hierarchy. In that case we cannot // attach `NativeViewGestureHandlers` here and we have to do it in `addView` method. nativeHandlers.add(tag) } else { - registry.attachHandlerToView(tag, this.id, GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) + registry.attachHandlerToView(tag, viewTag, actionType) + if (actionType == GestureHandler.ACTION_TYPE_LOGIC_DETECTOR) { + registry.getHandler(tag)?.hostDetectorView = this + } attachedHandlers.add(tag) } - } else if (entry.value == GestureHandlerMutation.Detach) { - registry.detachHandler(tag) - nativeHandlers.remove(tag) - attachedHandlers.remove(tag) } } + for (tag in handlersToDetach) { + registry.detachHandler(tag) + nativeHandlers.remove(tag) + attachedHandlers.remove(tag) + } + val child = getChildAt(0) // This covers the case where `NativeViewGestureHandlers` are attached after child views were created. @@ -132,13 +165,22 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { registry.detachHandler(tag) attachedHandlers.remove(tag) } - } - companion object { - private enum class GestureHandlerMutation { - Attach, - Detach, - Keep, + for (child in attachedLogicHandlers) { + for (tag in child.value) { + registry.detachHandler(tag) + } + child.value.clear() } } + + private fun ReadableArray.mapLogicChildren(): List = List(size()) { i -> + val child = getMap(i) ?: return@List null + val handlerTags = child.getArray("handlerTags")?.toIntList().orEmpty() + val viewTag = child.getInt("viewTag") + + LogicChildren(handlerTags, viewTag) + }.filterNotNull() + + private fun ReadableArray.toIntList(): List = List(size()) { getInt(it) } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt index 22092feebe..c8f4cb82d1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt @@ -37,6 +37,10 @@ class RNGestureHandlerDetectorViewManager : view.setModuleId(value) } + override fun setLogicChildren(view: RNGestureHandlerDetectorView, value: ReadableArray?) { + view.setLogicChildren(value) + } + override fun onDropViewInstance(view: RNGestureHandlerDetectorView) { view.onViewDrop() super.onDropViewInstance(view) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt index 2e16a0a1b8..cf1ff05cab 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt @@ -26,8 +26,8 @@ class RNGestureHandlerEvent private constructor() : Event dataBuilder: GestureHandlerEventDataBuilder, eventHandlerType: EventHandlerType, ) { - val view = if (handler.actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) { - handler.viewForEvents!! + val view = if (GestureHandler.usesNativeOrLogicDetector(handler.actionType)) { + handler.viewForEvents } else { handler.view!! } @@ -45,7 +45,7 @@ class RNGestureHandlerEvent private constructor() : Event EVENTS_POOL.release(this) } - override fun getEventName() = if (actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) { + override fun getEventName() = if (GestureHandler.usesNativeOrLogicDetector(actionType)) { if (eventHandlerType == EventHandlerType.ForAnimated) { NATIVE_DETECTOR_ANIMATED_EVENT_NAME } else if (eventHandlerType == EventHandlerType.ForReanimated) { @@ -60,11 +60,11 @@ class RNGestureHandlerEvent private constructor() : Event } // Unfortunately getCoalescingKey is not considered when sending event to C++, therefore we have to disable coalescing in v3 - override fun canCoalesce() = actionType != GestureHandler.ACTION_TYPE_NATIVE_DETECTOR + override fun canCoalesce() = !GestureHandler.usesNativeOrLogicDetector(actionType) override fun getCoalescingKey() = coalescingKey - override fun getEventData(): WritableMap = if (actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) { + override fun getEventData(): WritableMap = if (GestureHandler.usesNativeOrLogicDetector(actionType)) { createNativeEventData(dataBuilder!!) } else { createEventData(dataBuilder!!) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEventDispatcher.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEventDispatcher.kt index a3373873a9..731818578b 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEventDispatcher.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEventDispatcher.kt @@ -71,7 +71,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React RNGestureHandlerEvent.createEventData(handlerFactory.createEventBuilder(handler)) sendEventForDeviceEvent(RNGestureHandlerEvent.EVENT_NAME, data) } - GestureHandler.ACTION_TYPE_NATIVE_DETECTOR -> { + GestureHandler.ACTION_TYPE_NATIVE_DETECTOR, GestureHandler.ACTION_TYPE_LOGIC_DETECTOR -> { val eventHandlerType = if (handler.dispatchesAnimatedEvents) { EventHandlerType.ForAnimated } else if (handler.dispatchesReanimatedEvents) { @@ -87,7 +87,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React eventHandlerType, ) - handler.viewForEvents!!.dispatchEvent(event) + handler.viewForEvents.dispatchEvent(event) } } } @@ -136,7 +136,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React sendEventForDeviceEvent(RNGestureHandlerStateChangeEvent.EVENT_NAME, data) } - GestureHandler.ACTION_TYPE_NATIVE_DETECTOR -> { + GestureHandler.ACTION_TYPE_NATIVE_DETECTOR, GestureHandler.ACTION_TYPE_LOGIC_DETECTOR -> { val eventHandlerType = if (handler.dispatchesReanimatedEvents) { EventHandlerType.ForReanimated } else { @@ -152,7 +152,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React eventHandlerType, ) - handler.viewForEvents!!.dispatchEvent(event) + handler.viewForEvents.dispatchEvent(event) } } } @@ -188,7 +188,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React val data = RNGestureHandlerTouchEvent.createEventData(handler) sendEventForDeviceEvent(RNGestureHandlerEvent.EVENT_NAME, data) } - GestureHandler.ACTION_TYPE_NATIVE_DETECTOR -> { + GestureHandler.ACTION_TYPE_NATIVE_DETECTOR, GestureHandler.ACTION_TYPE_LOGIC_DETECTOR -> { val eventHandlerType = if (handler.dispatchesReanimatedEvents) { EventHandlerType.ForReanimated } else { @@ -196,7 +196,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React } val event = RNGestureHandlerTouchEvent.obtain(handler, handler.actionType, eventHandlerType) - handler.viewForEvents!!.dispatchEvent(event) + handler.viewForEvents.dispatchEvent(event) } } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerStateChangeEvent.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerStateChangeEvent.kt index aac38aadf4..a1d3313564 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerStateChangeEvent.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerStateChangeEvent.kt @@ -29,8 +29,8 @@ class RNGestureHandlerStateChangeEvent private constructor() : Event, eventHandlerType: EventHandlerType, ) { - val view = if (handler.actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) { - handler.viewForEvents!! + val view = if (GestureHandler.usesNativeOrLogicDetector(handler.actionType)) { + handler.viewForEvents } else { handler.view!! } @@ -51,7 +51,7 @@ class RNGestureHandlerStateChangeEvent private constructor() : Event init(handler: T, actionType: Int, eventHandlerType: EventHandlerType) { - val view = if (handler.actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) { - handler.viewForEvents!! + val view = if (GestureHandler.usesNativeOrLogicDetector(handler.actionType)) { + handler.viewForEvents } else { handler.view!! } @@ -33,13 +33,13 @@ class RNGestureHandlerTouchEvent private constructor() : Event @property (nonatomic, nonnull) NSMutableSet *nativeHandlers; @property (nonatomic, nonnull) NSMutableSet *attachedHandlers; +@property (nonatomic) std::unordered_map attachedLogicHandlers; @end -typedef NS_ENUM(NSInteger, RNGestureHandlerMutation) { - RNGestureHandlerMutationAttach = 1, - RNGestureHandlerMutationDetach, - RNGestureHandlerMutationKeep, -}; - @implementation RNGestureHandlerDetector { int _moduleId; } @@ -61,6 +56,12 @@ - (void)willMoveToWindow:(RNGHWindow *)newWindow NSNumber *handlerTag = [NSNumber numberWithInt:handler]; [handlerManager.registry detachHandlerWithTag:handlerTag]; } + for (const auto &child : _attachedLogicHandlers) { + for (id handlerTag : child.second) { + [handlerManager.registry detachHandlerWithTag:handlerTag]; + } + } + _attachedLogicHandlers.clear(); } } @@ -150,60 +151,94 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics [super updateLayoutMetrics:newLayoutMetrics oldLayoutMetrics:oldLayoutMetrics]; } -- (void)updateProps:(const Props::Shared &)propsBase oldProps:(const Props::Shared &)oldPropsBase +- (void)attachHandlers:(const std::vector &)handlerTags + actionType:(RNGestureHandlerActionType)actionType + viewTag:(const int)viewTag + attachedHandlers:(NSMutableSet *)attachedHandlers { - const auto &newProps = *std::static_pointer_cast(propsBase); - const auto &oldProps = *std::static_pointer_cast(oldPropsBase); - - _moduleId = newProps.moduleId; RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; react_native_assert(handlerManager != nullptr && "Tried to access a non-existent handler manager") - std::unordered_map - changes; + NSMutableSet *handlersToDetach = [attachedHandlers mutableCopy]; - if (oldPropsBase != nullptr) { - for (const auto oldHandler : oldProps.handlerTags) { - changes[oldHandler] = RNGestureHandlerMutationDetach; - } - } - - for (const auto newHandler : newProps.handlerTags) { - changes[newHandler] = changes.contains(newHandler) ? RNGestureHandlerMutationKeep : RNGestureHandlerMutationAttach; - } - - for (const auto handlerChange : changes) { - NSNumber *handlerTag = [NSNumber numberWithInt:handlerChange.first]; - - if (handlerChange.second == RNGestureHandlerMutationAttach) { - if ([self shouldAttachGestureToSubview:handlerTag]) { + for (const int tag : handlerTags) { + [handlersToDetach removeObject:@(tag)]; + if (![attachedHandlers containsObject:@(tag)]) { + if ([self shouldAttachGestureToSubview:@(tag)]) { // It might happen that `attachHandlers` will be called before children are added into view hierarchy. In that // case we cannot attach `NativeViewGestureHandlers` here and we have to do it in `didAddSubview` method. - [_nativeHandlers addObject:handlerTag]; + [_nativeHandlers addObject:@(tag)]; } else { - [handlerManager.registry attachHandlerWithTag:handlerTag - toView:self - withActionType:RNGestureHandlerActionTypeNativeDetector]; - - [_attachedHandlers addObject:handlerTag]; + if (actionType == RNGestureHandlerActionTypeLogicDetector) { + [handlerManager attachGestureHandler:@(tag) toViewWithTag:@(viewTag) withActionType:actionType]; + } else { + [handlerManager.registry attachHandlerWithTag:@(tag) toView:self withActionType:actionType]; + } + [attachedHandlers addObject:@(tag)]; } - } else if (handlerChange.second == RNGestureHandlerMutationDetach) { - [handlerManager.registry detachHandlerWithTag:handlerTag]; - [_attachedHandlers removeObject:handlerTag]; - [_nativeHandlers removeObject:handlerTag]; + [[handlerManager registry] handlerWithTag:@(tag)].hostDetectorTag = @(self.tag); } } + for (const id tag : handlersToDetach) { + [handlerManager.registry detachHandlerWithTag:tag]; + [attachedHandlers removeObject:tag]; + [_nativeHandlers removeObject:tag]; + } + // This covers the case where `NativeViewGestureHandlers` are attached after child views were created. - if (!self.subviews[0]) { + if (self.subviews[0]) { [self tryAttachNativeHandlersToChildView]; } +} + +- (void)updateProps:(const Props::Shared &)propsBase oldProps:(const Props::Shared &)oldPropsBase +{ + const auto &newProps = *std::static_pointer_cast(propsBase); + _moduleId = newProps.moduleId; + + [self attachHandlers:newProps.handlerTags + actionType:RNGestureHandlerActionTypeNativeDetector + viewTag:-1 + attachedHandlers:_attachedHandlers]; [super updateProps:propsBase oldProps:oldPropsBase]; + [self updateLogicChildren:newProps.logicChildren]; + // Override to force hittesting to work outside bounds self.clipsToBounds = NO; } +- (void)updateLogicChildren:(const std::vector &)logicChildren +{ + RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; + react_native_assert(handlerManager != nullptr && "Tried to access a non-existent handler manager") + + NSMutableSet *logicChildrenToDetach = [NSMutableSet set]; + for (const auto &child : _attachedLogicHandlers) { + [logicChildrenToDetach addObject:@(child.first)]; + } + + for (const auto &child : logicChildren) { + if (_attachedLogicHandlers.find(child.viewTag) == _attachedLogicHandlers.end()) { + _attachedLogicHandlers[child.viewTag] = [NSMutableSet set]; + } + + [logicChildrenToDetach removeObject:@(child.viewTag)]; + + [self attachHandlers:child.handlerTags + actionType:RNGestureHandlerActionTypeLogicDetector + viewTag:child.viewTag + attachedHandlers:_attachedLogicHandlers[child.viewTag]]; + } + + for (const NSNumber *tag : logicChildrenToDetach) { + for (id handlerTag : _attachedLogicHandlers[tag.intValue]) { + [handlerManager.registry detachHandlerWithTag:handlerTag]; + } + } +} + - (void)tryAttachNativeHandlersToChildView { RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h index 123d85cd54..c5870af26f 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h @@ -40,4 +40,5 @@ - (nullable RNGestureHandler *)handlerWithTag:(nonnull NSNumber *)handlerTag; +- (nullable RNGHUIView *)viewForReactTag:(nonnull NSNumber *)reactTag; @end diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm index 046b7a54e5..e9dc313905 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm @@ -309,34 +309,20 @@ - (void)sendEvent:(RNGestureHandlerStateChange *)event // but results in a compilation error. { switch (actionType) { + case RNGestureHandlerActionTypeLogicDetector: { + NSNumber *hostDetectorTag = [_registry handlerWithTag:event.handlerTag].hostDetectorTag; + detectorView = [self viewForReactTag:hostDetectorTag]; + [self sendNativeOrLogicEvent:event + withActionType:actionType + forHandlerType:eventHandlerType + forView:detectorView]; + break; + } case RNGestureHandlerActionTypeNativeDetector: { - if ([event isKindOfClass:[RNGestureHandlerEvent class]]) { - switch (eventHandlerType) { - case RNGestureHandlerEventHandlerTypeAnimated: - [self sendEventForNativeAnimatedEvent:event]; - break; - case RNGestureHandlerEventHandlerTypeReanimated: { - RNGestureHandlerEvent *gestureEvent = (RNGestureHandlerEvent *)event; - auto nativeEvent = [gestureEvent getReanimatedNativeEvent]; - [(RNGestureHandlerDetector *)detectorView dispatchReanimatedGestureEvent:nativeEvent]; - break; - } - case RNGestureHandlerEventHandlerTypeJS: { - RNGestureHandlerEvent *gestureEvent = (RNGestureHandlerEvent *)event; - auto nativeEvent = [gestureEvent getNativeEvent]; - [(RNGestureHandlerDetector *)detectorView dispatchGestureEvent:nativeEvent]; - break; - } - } - } else { - if (eventHandlerType == RNGestureHandlerEventHandlerTypeReanimated) { - auto nativeEvent = [event getReanimatedNativeEvent]; - [(RNGestureHandlerDetector *)detectorView dispatchReanimatedStateChangeEvent:nativeEvent]; - } else { - auto nativeEvent = [event getNativeEvent]; - [(RNGestureHandlerDetector *)detectorView dispatchStateChangeEvent:nativeEvent]; - } - } + [self sendNativeOrLogicEvent:event + withActionType:actionType + forHandlerType:eventHandlerType + forView:detectorView]; break; } @@ -483,4 +469,42 @@ - (void)sendEventForDeviceEvent:(RNGestureHandlerStateChange *)event [_eventDispatcher sendDeviceEventWithName:@"onGestureHandlerStateChange" body:body]; } +- (void)sendNativeOrLogicEvent:(RNGestureHandlerStateChange *)event + withActionType:(RNGestureHandlerActionType)actionType + forHandlerType:(RNGestureHandlerEventHandlerType)eventHandlerType + forView:(RNGHUIView *)detectorView +{ + if ([event isKindOfClass:[RNGestureHandlerEvent class]]) { + switch (eventHandlerType) { + case RNGestureHandlerEventHandlerTypeAnimated: + [self sendEventForNativeAnimatedEvent:event]; + break; + case RNGestureHandlerEventHandlerTypeReanimated: { + RNGestureHandlerEvent *gestureEvent = (RNGestureHandlerEvent *)event; + auto nativeEvent = [gestureEvent getReanimatedNativeEvent]; + [(RNGestureHandlerDetector *)detectorView dispatchReanimatedGestureEvent:nativeEvent]; + break; + } + case RNGestureHandlerEventHandlerTypeJS: { + RNGestureHandlerEvent *gestureEvent = (RNGestureHandlerEvent *)event; + auto nativeEvent = [gestureEvent getNativeEvent]; + [(RNGestureHandlerDetector *)detectorView dispatchGestureEvent:nativeEvent]; + break; + } + } + } else { + if (eventHandlerType == RNGestureHandlerEventHandlerTypeReanimated) { + auto nativeEvent = [event getReanimatedNativeEvent]; + [(RNGestureHandlerDetector *)detectorView dispatchReanimatedStateChangeEvent:nativeEvent]; + } else { + auto nativeEvent = [event getNativeEvent]; + [(RNGestureHandlerDetector *)detectorView dispatchStateChangeEvent:nativeEvent]; + } + } +} + +- (RNGHUIView *)viewForReactTag:(NSNumber *)reactTag +{ + return [_viewRegistry viewForReactTag:reactTag]; +} @end diff --git a/packages/react-native-gesture-handler/src/ActionType.ts b/packages/react-native-gesture-handler/src/ActionType.ts index a902e8356f..ced70acdfc 100644 --- a/packages/react-native-gesture-handler/src/ActionType.ts +++ b/packages/react-native-gesture-handler/src/ActionType.ts @@ -4,7 +4,17 @@ export const ActionType = { JS_FUNCTION_OLD_API: 3, JS_FUNCTION_NEW_API: 4, NATIVE_DETECTOR: 5, + LOGIC_DETECTOR: 6, } as const; // eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value export type ActionType = (typeof ActionType)[keyof typeof ActionType]; + +export function usesNativeOrLogicDetector( + actionType: ActionType | null +): boolean { + return ( + actionType === ActionType.NATIVE_DETECTOR || + actionType === ActionType.LOGIC_DETECTOR + ); +} diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 82392a25a7..8c9d8b0ef3 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -165,6 +165,7 @@ export { default as DrawerLayout } from './components/DrawerLayout'; export type { NativeDetectorProps } from './v3/NativeDetector/NativeDetector'; export { NativeDetector } from './v3/NativeDetector/NativeDetector'; +export { LogicDetector } from './v3/LogicDetector'; export * from './v3/hooks/useGesture'; export * from './v3/hooks/relations'; diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts index ae794a8488..b48fa90821 100644 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts @@ -42,6 +42,11 @@ type GestureHandlerTouchEvent = Readonly<{ pointerType: Int32; }>; +export interface LogicChildrenProps { + handlerTags: Int32[]; + viewTag: Int32; +} + export interface NativeProps extends ViewProps { onGestureHandlerEvent?: DirectEventHandler; onGestureHandlerStateChange?: DirectEventHandler; @@ -53,6 +58,7 @@ export interface NativeProps extends ViewProps { handlerTags: Int32[]; moduleId: Int32; + logicChildren: LogicChildrenProps[]; } export default codegenNativeComponent('RNGestureHandlerDetector', { diff --git a/packages/react-native-gesture-handler/src/v3/LogicDetector.tsx b/packages/react-native-gesture-handler/src/v3/LogicDetector.tsx new file mode 100644 index 0000000000..c24bdb9b00 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/LogicDetector.tsx @@ -0,0 +1,71 @@ +import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; +import { Wrap } from '../handlers/gestures/GestureDetector/Wrap'; +import { findNodeHandle, Platform } from 'react-native'; +import { useDetectorContext } from './NativeDetector/useDetectorContext'; +import { NativeDetectorProps } from './NativeDetector/NativeDetector'; +import { isComposedGesture } from './hooks/utils/relationUtils'; +import { GestureEvents } from './types'; + +export function LogicDetector( + props: NativeDetectorProps +) { + const { register, unregister } = useDetectorContext(); + const viewRef = useRef(null); + const [viewTag, setViewTag] = useState(-1); + const logicMethods = useRef(props.gesture.gestureEvents); + + const handleRef = useCallback((node: any) => { + viewRef.current = node; + if (!node) { + return; + } + + if (Platform.OS === 'web') { + setViewTag(node); + } else { + const tag = findNodeHandle(node); + if (tag != null) { + setViewTag(tag); + } + } + }, []); + + useEffect(() => { + logicMethods.current = props.gesture.gestureEvents; + }, [props.gesture.gestureEvents]); + + useEffect(() => { + if (viewTag === -1) { + return; + } + + // Native Detector differentiates Logic Children through a viewTag, + // thus if viewTag changes we have to reregister + unregister(viewTag); + }, [viewTag]); + + useEffect(() => { + if (viewTag === -1) { + return; + } + + const logicProps = { + viewTag, + handlerTags: isComposedGesture(props.gesture) + ? props.gesture.tags + : [props.gesture.tag], + }; + + if (Platform.OS === 'web') { + Object.assign(logicProps, { viewRef }); + } + + register(logicProps, logicMethods as RefObject>); + + return () => { + unregister(viewTag); + }; + }, [viewTag, props.gesture, register, unregister]); + + return {props.children}; +} diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx index fd096c9277..bea9959a52 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx @@ -1,14 +1,22 @@ -import React, { Ref, useEffect, useRef } from 'react'; +import React, { Ref, RefObject, useEffect, useRef } from 'react'; import RNGestureHandlerModule from '../../RNGestureHandlerModule.web'; import { ActionType } from '../../ActionType'; import { PropsRef } from '../../web/interfaces'; import { View } from 'react-native'; import { tagMessage } from '../../utils'; +import { EMPTY_SET } from './utils'; export interface GestureHandlerDetectorProps extends PropsRef { handlerTags: number[]; moduleId: number; children?: React.ReactNode; + logicChildren?: Set; +} + +export interface LogicChildrenWeb { + viewTag: number; + handlerTags: number[]; + viewRef: RefObject; } const HostGestureDetector = (props: GestureHandlerDetectorProps) => { @@ -16,25 +24,29 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => { const viewRef = useRef(null); const propsRef = useRef(props); - const attachedHandlerTags = useRef>(new Set()); - const attachedNativeHandlerTags = useRef>(new Set()); + const attachedHandlers = useRef>(new Set()); + const attachedNativeHandlers = useRef>(new Set()); + const attachedLogicHandlers = useRef>>(new Map()); - const detachHandlers = (oldHandlerTags: Set) => { + const detachHandlers = ( + currentHandlerTags: Set, + attachedHandlerTags: Set + ) => { + const oldHandlerTags = attachedHandlerTags.difference(currentHandlerTags); oldHandlerTags.forEach((tag) => { RNGestureHandlerModule.detachGestureHandler(tag); - attachedNativeHandlerTags.current.delete(tag); - attachedHandlerTags.current.delete(tag); + attachedHandlerTags.delete(tag); }); }; - const attachHandlers = (currentHandlerTags: Set) => { - const oldHandlerTags = - attachedHandlerTags.current.difference(currentHandlerTags); - const newHandlerTags = currentHandlerTags.difference( - attachedHandlerTags.current - ); - - detachHandlers(oldHandlerTags); + const attachHandlers = ( + viewRef: RefObject, + propsRef: RefObject, + currentHandlerTags: Set, + attachedHandlerTags: Set, + actionType: ActionType + ) => { + const newHandlerTags = currentHandlerTags.difference(attachedHandlerTags); newHandlerTags.forEach((tag) => { if ( @@ -45,20 +57,20 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => { RNGestureHandlerModule.attachGestureHandler( tag, viewRef.current!.firstChild, - ActionType.NATIVE_DETECTOR, + actionType, propsRef ); - attachedNativeHandlerTags.current.add(tag); + attachedNativeHandlers.current.add(tag); } else { RNGestureHandlerModule.attachGestureHandler( tag, viewRef.current, - ActionType.NATIVE_DETECTOR, + actionType, propsRef ); } + attachedHandlerTags.add(tag); }); - attachedHandlerTags.current = currentHandlerTags; }; useEffect(() => { @@ -72,14 +84,56 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => { ); } - const newHandlerTags = new Set(handlerTags); - attachHandlers(newHandlerTags); + const currentHandlerTags = new Set(handlerTags); + detachHandlers(currentHandlerTags, attachedHandlers.current); + + attachHandlers( + viewRef, + propsRef, + currentHandlerTags, + attachedHandlers.current, + ActionType.NATIVE_DETECTOR + ); return () => { - detachHandlers(newHandlerTags); + detachHandlers(EMPTY_SET, attachedHandlers.current); + attachedLogicHandlers?.current.forEach((childHandlerTags) => { + detachHandlers(EMPTY_SET, childHandlerTags); + }); }; }, [handlerTags, children]); + useEffect(() => { + const logicChildrenToDetach: Set = new Set( + attachedLogicHandlers.current.keys() + ); + + props.logicChildren?.forEach((child) => { + if (!attachedLogicHandlers.current.has(child.viewTag)) { + attachedLogicHandlers.current.set(child.viewTag, new Set()); + } + logicChildrenToDetach.delete(child.viewTag); + + const currentHandlerTags = new Set(child.handlerTags); + detachHandlers( + currentHandlerTags, + attachedLogicHandlers.current.get(child.viewTag)! + ); + + attachHandlers( + child.viewRef, + propsRef, + currentHandlerTags, + attachedLogicHandlers.current.get(child.viewTag)!, + ActionType.LOGIC_DETECTOR + ); + }); + + logicChildrenToDetach.forEach((tag) => { + detachHandlers(EMPTY_SET, attachedLogicHandlers.current.get(tag)!); + }); + }, [props.logicChildren]); + return ( }> {children} diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx b/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx index 01af531761..6bd6b3a120 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx @@ -1,9 +1,15 @@ -import React from 'react'; -import { Gesture } from '../types'; -import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; +import React, { RefObject, useCallback, useRef, useState } from 'react'; import { Animated, StyleSheet } from 'react-native'; import HostGestureDetector from './HostGestureDetector'; import { tagMessage } from '../../utils'; +import { + LogicChildren, + Gesture, + GestureEvents, + GestureHandlerEvent, +} from '../types'; +import { DetectorContext } from './useDetectorContext'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; import { configureRelations } from './utils'; import { isComposedGesture } from '../hooks/utils/relationUtils'; @@ -22,12 +28,41 @@ export function NativeDetector({ gesture, children, }: NativeDetectorProps) { + const [logicChildren, setLogicChildren] = useState([]); + const logicMethods = useRef>>>( + new Map() + ); + const NativeDetectorComponent = gesture.config.dispatchesAnimatedEvents ? AnimatedNativeDetector : gesture.config.shouldUseReanimated ? ReanimatedNativeDetector : HostGestureDetector; + const register = useCallback( + (child: LogicChildren, methods: RefObject>) => { + setLogicChildren((prev) => { + const index = prev.findIndex((c) => c.viewTag === child.viewTag); + if (index !== -1) { + const updated = [...prev]; + updated[index] = child; + return updated; + } + + return [...prev, child]; + }); + + child.handlerTags.forEach((tag) => { + logicMethods.current.set(tag, methods); + }); + }, + [] + ); + + const unregister = useCallback((childTag: number) => { + setLogicChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); + }, []); + // It might happen only with ReanimatedNativeDetector if (!NativeDetectorComponent) { throw new Error( @@ -39,38 +74,87 @@ export function NativeDetector({ configureRelations(gesture); - return ( - ) => { + return (e: GestureHandlerEvent) => { + if (gesture.gestureEvents[key]) { + gesture.gestureEvents[key](e); } - // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types - onGestureHandlerReanimatedTouchEvent={ - gesture.gestureEvents.onReanimatedTouchEvent - } - onGestureHandlerAnimatedEvent={ - gesture.gestureEvents.onGestureHandlerAnimatedEvent + + logicMethods.current.forEach((ref) => { + const method = ref.current?.[key]; + if (method) { + method(e); + } + }); + }; + }; + + const getHandlers = useCallback( + (key: keyof GestureEvents) => { + const handlers: ((e: GestureHandlerEvent) => void)[] = []; + + if (gesture.gestureEvents[key]) { + handlers.push( + gesture.gestureEvents[key] as ( + e: GestureHandlerEvent + ) => void + ); } - moduleId={globalThis._RNGH_MODULE_ID} - handlerTags={isComposedGesture(gesture) ? gesture.tags : [gesture.tag]} - style={styles.detector}> - {children} - + + logicMethods.current.forEach((ref) => { + const handler = ref.current?.[key]; + if (handler) { + handlers.push( + handler as (e: GestureHandlerEvent) => void + ); + } + }); + + return handlers; + }, + [logicChildren, gesture.gestureEvents] + ); + + const reanimatedEventHandler = Reanimated?.useComposedEventHandler( + getHandlers('onReanimatedUpdateEvent') + ); + const reanimedStateChangeHandler = Reanimated?.useComposedEventHandler( + getHandlers('onReanimatedStateChange') + ); + const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler( + getHandlers('onReanimatedTouchEvent') + ); + + return ( + + + {children} + + ); } diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/useDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/NativeDetector/useDetectorContext.ts new file mode 100644 index 0000000000..97c2c90dec --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/useDetectorContext.ts @@ -0,0 +1,20 @@ +import { createContext, RefObject, useContext } from 'react'; +import { GestureEvents, LogicChildren } from '../types'; + +type DetectorContextType = { + register: ( + child: LogicChildren, + methods: RefObject> + ) => void; + unregister: (child: number) => void; +}; + +export const DetectorContext = createContext(null); + +export function useDetectorContext() { + const ctx = useContext(DetectorContext); + if (!ctx) { + throw new Error('Logic detector must be a descendant of a Native Detector'); + } + return ctx; +} diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts b/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts index 1051c3b809..d6abd31075 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts @@ -149,3 +149,5 @@ export function configureRelations( RNGestureHandlerModule.flushOperations(); } + +export const EMPTY_SET = new Set(); diff --git a/packages/react-native-gesture-handler/src/v3/types.ts b/packages/react-native-gesture-handler/src/v3/types.ts index ffdd5f6f1f..1883b54b9a 100644 --- a/packages/react-native-gesture-handler/src/v3/types.ts +++ b/packages/react-native-gesture-handler/src/v3/types.ts @@ -63,6 +63,11 @@ export type AnimatedEvent = ((...args: any[]) => void) & { _argMapping: (Animated.Mapping | null)[]; }; +export interface LogicChildren { + viewTag: number; + handlerTags: number[]; +} + export enum SingleGestureName { Tap = 'TapGestureHandler', LongPress = 'LongPressGestureHandler', diff --git a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts index ad12231542..7ac76a9558 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts @@ -24,7 +24,7 @@ import { } from '../../handlers/gestureHandlerCommon'; import { PointerType } from '../../PointerType'; import { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate'; -import { ActionType } from '../../ActionType'; +import { ActionType, usesNativeOrLogicDetector } from '../../ActionType'; import { tagMessage } from '../../utils'; import { GestureStateChangeEvent, @@ -89,6 +89,7 @@ export default abstract class GestureHandler implements IGestureHandler { this.viewRef = viewRef; this.actionType = actionType; this.state = State.UNDETERMINED; + this.delegate.init(viewRef, this); } @@ -400,7 +401,7 @@ export default abstract class GestureHandler implements IGestureHandler { return; } - if (this.actionType === ActionType.NATIVE_DETECTOR) { + if (usesNativeOrLogicDetector(this.actionType)) { invokeNullableMethod( this.forReanimated ? onGestureHandlerReanimatedTouchEvent @@ -424,18 +425,17 @@ export default abstract class GestureHandler implements IGestureHandler { onGestureHandlerReanimatedEvent, onGestureHandlerReanimatedStateChange, }: PropsRef = this.propsRef!.current; - const resultEvent: ResultEvent = - this.actionType !== ActionType.NATIVE_DETECTOR - ? this.transformEventData(newState, oldState) - : this.lastSentState !== newState - ? this.transformStateChangeEvent(newState, oldState) - : this.transformUpdateEvent(newState); + + const resultEvent: ResultEvent = !usesNativeOrLogicDetector(this.actionType) + ? this.transformEventData(newState, oldState) + : this.lastSentState !== newState + ? this.transformStateChangeEvent(newState, oldState) + : this.transformUpdateEvent(newState); // In the v2 API oldState field has to be undefined, unless we send event state changed // Here the order is flipped to avoid workarounds such as making backup of the state and setting it to undefined first, then changing it back // Flipping order with setting oldState to undefined solves issue, when events were being sent twice instead of once // However, this may cause trouble in the future (but for now we don't know that) - if (this.lastSentState !== newState) { this.lastSentState = newState; invokeNullableMethod( @@ -446,7 +446,7 @@ export default abstract class GestureHandler implements IGestureHandler { ); } if (this.state === State.ACTIVE) { - if (this.actionType !== ActionType.NATIVE_DETECTOR) { + if (!usesNativeOrLogicDetector(this.actionType)) { (resultEvent.nativeEvent as GestureHandlerNativeEvent).oldState = undefined; }