Skip to content

Commit 118775a

Browse files
authored
[General] Don't emit duplicate events for NativeViewGestureHandler (#4102)
## Description Updates `NativeViewGestureHandler` not to emit update event on every pointer move unless the payload would change. This significantly reduces the number of events dispatched by the native gesture handlers, which should have a positive impact on performance without causing any information loss. Supersedes #3348 ## Test plan Add `onUpdate={console.log}` to the internal `Touchable` implementation and check any example using `Touchable`.
1 parent c6b424f commit 118775a

4 files changed

Lines changed: 96 additions & 27 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class NativeViewGestureHandler : GestureHandler() {
3131

3232
private var hook: NativeViewGestureHandlerHook = defaultHook
3333

34+
private data class ActiveUpdateSnapshot(val pointerInside: Boolean, val numberOfPointers: Int, val pointerType: Int)
35+
36+
private var lastActiveUpdate: ActiveUpdateSnapshot? = null
37+
3438
init {
3539
shouldCancelWhenOutside = true
3640
}
@@ -163,6 +167,21 @@ class NativeViewGestureHandler : GestureHandler() {
163167

164168
override fun onReset() {
165169
this.hook = defaultHook
170+
lastActiveUpdate = null
171+
}
172+
173+
override fun dispatchHandlerUpdate(event: MotionEvent) {
174+
val snapshot = ActiveUpdateSnapshot(isWithinBounds, numberOfPointers, pointerType)
175+
if (snapshot == lastActiveUpdate) {
176+
return
177+
}
178+
lastActiveUpdate = snapshot
179+
super.dispatchHandlerUpdate(event)
180+
}
181+
182+
override fun dispatchStateChange(newState: Int, prevState: Int) {
183+
lastActiveUpdate = null
184+
super.dispatchStateChange(newState, prevState)
166185
}
167186

168187
override fun wantsToAttachDirectlyToView() = true

packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ - (void)updateStateIfScrollView
113113
@implementation RNNativeViewGestureHandler {
114114
BOOL _shouldActivateOnStart;
115115
BOOL _disallowInterruption;
116+
RNGestureHandlerEventExtraData *_lastActiveExtraData;
116117
}
117118

118119
- (instancetype)initWithTag:(NSNumber *)tag
@@ -180,6 +181,16 @@ - (void)unbindFromView
180181
[super unbindFromView];
181182
}
182183

184+
- (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestureHandlerEventExtraData *)extraData
185+
{
186+
if ([_lastActiveExtraData.data isEqualToDictionary:extraData.data]) {
187+
return;
188+
}
189+
190+
_lastActiveExtraData = extraData;
191+
[self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData];
192+
}
193+
183194
- (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
184195
{
185196
[self setCurrentPointerTypeForEvent:event];
@@ -202,11 +213,11 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
202213
withNumberOfTouches:event.allTouches.count
203214
withPointerType:_pointerType]];
204215

205-
[self sendEventsInState:RNGestureHandlerStateActive
206-
forViewWithTag:sender.reactTag
207-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
208-
withNumberOfTouches:event.allTouches.count
209-
withPointerType:_pointerType]];
216+
_lastActiveExtraData = nil;
217+
[self sendActiveStateEventIfChangedForView:sender
218+
extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
219+
withNumberOfTouches:event.allTouches.count
220+
withPointerType:_pointerType]];
210221
}
211222

212223
- (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event
@@ -238,30 +249,27 @@ - (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event
238249
UIControl *control = (UIControl *)sender;
239250
[control cancelTrackingWithEvent:event];
240251
} else {
241-
[self sendEventsInState:RNGestureHandlerStateActive
242-
forViewWithTag:sender.reactTag
243-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
244-
withNumberOfTouches:event.allTouches.count
245-
withPointerType:_pointerType]];
252+
[self sendActiveStateEventIfChangedForView:sender
253+
extraData:[RNGestureHandlerEventExtraData forPointerInside:NO
254+
withNumberOfTouches:event.allTouches.count
255+
withPointerType:_pointerType]];
246256
}
247257
}
248258

249259
- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
250260
{
251-
[self sendEventsInState:RNGestureHandlerStateActive
252-
forViewWithTag:sender.reactTag
253-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
254-
withNumberOfTouches:event.allTouches.count
255-
withPointerType:_pointerType]];
261+
[self sendActiveStateEventIfChangedForView:sender
262+
extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
263+
withNumberOfTouches:event.allTouches.count
264+
withPointerType:_pointerType]];
256265
}
257266

258267
- (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event
259268
{
260-
[self sendEventsInState:RNGestureHandlerStateActive
261-
forViewWithTag:sender.reactTag
262-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
263-
withNumberOfTouches:event.allTouches.count
264-
withPointerType:_pointerType]];
269+
[self sendActiveStateEventIfChangedForView:sender
270+
extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
271+
withNumberOfTouches:event.allTouches.count
272+
withPointerType:_pointerType]];
265273
}
266274

267275
- (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event
@@ -270,11 +278,10 @@ - (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event
270278
return;
271279
}
272280

273-
[self sendEventsInState:RNGestureHandlerStateActive
274-
forViewWithTag:sender.reactTag
275-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
276-
withNumberOfTouches:event.allTouches.count
277-
withPointerType:_pointerType]];
281+
[self sendActiveStateEventIfChangedForView:sender
282+
extraData:[RNGestureHandlerEventExtraData forPointerInside:NO
283+
withNumberOfTouches:event.allTouches.count
284+
withPointerType:_pointerType]];
278285
}
279286

280287
- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event

packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { tagMessage } from '../../utils';
1414
import type {
1515
GestureStateChangeEventWithHandlerData,
1616
GestureUpdateEventWithHandlerData,
17+
HandlerData,
1718
SingleGestureName,
1819
} from '../../v3/types';
1920
import type {
@@ -430,19 +431,21 @@ export default abstract class GestureHandler implements IGestureHandler {
430431
onGestureHandlerReanimatedStateChange,
431432
}: PropsRef = this.propsRef!.current;
432433

434+
const isStateChange = this.lastSentState !== newState;
435+
433436
const resultEvent: ResultEvent = !usesNativeOrVirtualDetector(
434437
this.actionType
435438
)
436439
? this.transformEventData(newState, oldState)
437-
: this.lastSentState !== newState
440+
: isStateChange
438441
? this.transformStateChangeEvent(newState, oldState)
439442
: this.transformUpdateEvent(newState);
440443

441444
// In the v2 API oldState field has to be undefined, unless we send event state changed
442445
// 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
443446
// Flipping order with setting oldState to undefined solves issue, when events were being sent twice instead of once
444447
// However, this may cause trouble in the future (but for now we don't know that)
445-
if (this.lastSentState !== newState) {
448+
if (isStateChange) {
446449
this.lastSentState = newState;
447450

448451
if (this.forReanimated) {
@@ -456,6 +459,16 @@ export default abstract class GestureHandler implements IGestureHandler {
456459
return;
457460
}
458461

462+
// Cover only V3 path due to different event shape
463+
if (!isStateChange && usesNativeOrVirtualDetector(this.actionType)) {
464+
const handlerData = (
465+
resultEvent.nativeEvent as GestureUpdateEventWithHandlerData<unknown>
466+
).handlerData;
467+
if (this.shouldSuppressActiveUpdate(handlerData)) {
468+
return;
469+
}
470+
}
471+
459472
(resultEvent.nativeEvent as GestureHandlerNativeEvent).oldState = undefined;
460473

461474
if (this.forReanimated) {
@@ -467,6 +480,12 @@ export default abstract class GestureHandler implements IGestureHandler {
467480
}
468481
};
469482

483+
protected shouldSuppressActiveUpdate(
484+
_handlerData: HandlerData<unknown>
485+
): boolean {
486+
return false;
487+
}
488+
470489
private transformEventData(
471490
newState: State,
472491
oldState: State

packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { Platform } from 'react-native';
22

33
import type { ActionType } from '../../ActionType';
44
import { State } from '../../State';
5+
import { deepEqual } from '../../utils';
6+
import type { NativeHandlerData } from '../../v3/hooks/gestures/native/NativeTypes';
7+
import type { HandlerData } from '../../v3/types';
58
import { SingleGestureName } from '../../v3/types';
69
import { DEFAULT_TOUCH_SLOP } from '../constants';
710
import type { AdaptedEvent, Config, PropsRef } from '../interfaces';
811
import type { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
912
import GestureHandler from './GestureHandler';
1013
import type IGestureHandler from './IGestureHandler';
14+
1115
export default class NativeViewGestureHandler extends GestureHandler {
1216
private buttonRole!: boolean;
1317

@@ -20,6 +24,8 @@ export default class NativeViewGestureHandler extends GestureHandler {
2024
private startY = 0;
2125
private minDistSq = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP;
2226

27+
private lastActiveHandlerData: HandlerData<NativeHandlerData> | null = null;
28+
2329
public constructor(
2430
delegate: GestureHandlerDelegate<unknown, IGestureHandler>
2531
) {
@@ -199,4 +205,22 @@ export default class NativeViewGestureHandler extends GestureHandler {
199205
),
200206
};
201207
}
208+
209+
protected override shouldSuppressActiveUpdate(
210+
handlerData: HandlerData<NativeHandlerData>
211+
): boolean {
212+
if (
213+
this.lastActiveHandlerData &&
214+
deepEqual(this.lastActiveHandlerData, handlerData)
215+
) {
216+
return true;
217+
}
218+
this.lastActiveHandlerData = handlerData;
219+
return false;
220+
}
221+
222+
public override reset(): void {
223+
super.reset();
224+
this.lastActiveHandlerData = null;
225+
}
202226
}

0 commit comments

Comments
 (0)