diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt index a254d9ac63..73e0262c08 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt @@ -31,6 +31,10 @@ class NativeViewGestureHandler : GestureHandler() { private var hook: NativeViewGestureHandlerHook = defaultHook + private data class ActiveUpdateSnapshot(val pointerInside: Boolean, val numberOfPointers: Int, val pointerType: Int) + + private var lastActiveUpdate: ActiveUpdateSnapshot? = null + init { shouldCancelWhenOutside = true } @@ -163,6 +167,21 @@ class NativeViewGestureHandler : GestureHandler() { override fun onReset() { this.hook = defaultHook + lastActiveUpdate = null + } + + override fun dispatchHandlerUpdate(event: MotionEvent) { + val snapshot = ActiveUpdateSnapshot(isWithinBounds, numberOfPointers, pointerType) + if (snapshot == lastActiveUpdate) { + return + } + lastActiveUpdate = snapshot + super.dispatchHandlerUpdate(event) + } + + override fun dispatchStateChange(newState: Int, prevState: Int) { + lastActiveUpdate = null + super.dispatchStateChange(newState, prevState) } override fun wantsToAttachDirectlyToView() = true diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index b97925e0ee..1cf1eac1dd 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -113,6 +113,7 @@ - (void)updateStateIfScrollView @implementation RNNativeViewGestureHandler { BOOL _shouldActivateOnStart; BOOL _disallowInterruption; + RNGestureHandlerEventExtraData *_lastActiveExtraData; } - (instancetype)initWithTag:(NSNumber *)tag @@ -180,6 +181,16 @@ - (void)unbindFromView [super unbindFromView]; } +- (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestureHandlerEventExtraData *)extraData +{ + if ([_lastActiveExtraData.data isEqualToDictionary:extraData.data]) { + return; + } + + _lastActiveExtraData = extraData; + [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData]; +} + - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerTypeForEvent:event]; @@ -202,11 +213,11 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event withNumberOfTouches:event.allTouches.count withPointerType:_pointerType]]; - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + _lastActiveExtraData = nil; + [self sendActiveStateEventIfChangedForView:sender + extraData:[RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; } - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event @@ -238,30 +249,27 @@ - (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event UIControl *control = (UIControl *)sender; [control cancelTrackingWithEvent:event]; } else { - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + [self sendActiveStateEventIfChangedForView:sender + extraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; } } - (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event { - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + [self sendActiveStateEventIfChangedForView:sender + extraData:[RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; } - (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event { - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + [self sendActiveStateEventIfChangedForView:sender + extraData:[RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; } - (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event @@ -270,11 +278,10 @@ - (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event return; } - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + [self sendActiveStateEventIfChangedForView:sender + extraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; } - (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event 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 d4983d237e..1bbbd3a70d 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts @@ -14,6 +14,7 @@ import { tagMessage } from '../../utils'; import type { GestureStateChangeEventWithHandlerData, GestureUpdateEventWithHandlerData, + HandlerData, SingleGestureName, } from '../../v3/types'; import type { @@ -430,11 +431,13 @@ export default abstract class GestureHandler implements IGestureHandler { onGestureHandlerReanimatedStateChange, }: PropsRef = this.propsRef!.current; + const isStateChange = this.lastSentState !== newState; + const resultEvent: ResultEvent = !usesNativeOrVirtualDetector( this.actionType ) ? this.transformEventData(newState, oldState) - : this.lastSentState !== newState + : isStateChange ? this.transformStateChangeEvent(newState, oldState) : this.transformUpdateEvent(newState); @@ -442,7 +445,7 @@ export default abstract class GestureHandler implements IGestureHandler { // 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) { + if (isStateChange) { this.lastSentState = newState; if (this.forReanimated) { @@ -456,6 +459,16 @@ export default abstract class GestureHandler implements IGestureHandler { return; } + // Cover only V3 path due to different event shape + if (!isStateChange && usesNativeOrVirtualDetector(this.actionType)) { + const handlerData = ( + resultEvent.nativeEvent as GestureUpdateEventWithHandlerData + ).handlerData; + if (this.shouldSuppressActiveUpdate(handlerData)) { + return; + } + } + (resultEvent.nativeEvent as GestureHandlerNativeEvent).oldState = undefined; if (this.forReanimated) { @@ -467,6 +480,12 @@ export default abstract class GestureHandler implements IGestureHandler { } }; + protected shouldSuppressActiveUpdate( + _handlerData: HandlerData + ): boolean { + return false; + } + private transformEventData( newState: State, oldState: State diff --git a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts index c96a234818..d78256becc 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts @@ -2,12 +2,16 @@ import { Platform } from 'react-native'; import type { ActionType } from '../../ActionType'; import { State } from '../../State'; +import { deepEqual } from '../../utils'; +import type { NativeHandlerData } from '../../v3/hooks/gestures/native/NativeTypes'; +import type { HandlerData } from '../../v3/types'; import { SingleGestureName } from '../../v3/types'; import { DEFAULT_TOUCH_SLOP } from '../constants'; import type { AdaptedEvent, Config, PropsRef } from '../interfaces'; import type { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate'; import GestureHandler from './GestureHandler'; import type IGestureHandler from './IGestureHandler'; + export default class NativeViewGestureHandler extends GestureHandler { private buttonRole!: boolean; @@ -20,6 +24,8 @@ export default class NativeViewGestureHandler extends GestureHandler { private startY = 0; private minDistSq = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP; + private lastActiveHandlerData: HandlerData | null = null; + public constructor( delegate: GestureHandlerDelegate ) { @@ -199,4 +205,22 @@ export default class NativeViewGestureHandler extends GestureHandler { ), }; } + + protected override shouldSuppressActiveUpdate( + handlerData: HandlerData + ): boolean { + if ( + this.lastActiveHandlerData && + deepEqual(this.lastActiveHandlerData, handlerData) + ) { + return true; + } + this.lastActiveHandlerData = handlerData; + return false; + } + + public override reset(): void { + super.reset(); + this.lastActiveHandlerData = null; + } }