From 952bf8810abdcf9704c3099355a6bd4d44356dd8 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 12 Sep 2026 17:21:21 +0300 Subject: [PATCH 1/4] feat(core): gesture improvements and additions --- packages/core/ui/gestures/gestures-common.ts | 2 +- packages/core/ui/gestures/gestures-types.ts | 2 + packages/core/ui/gestures/index.android.ts | 312 ++++++++++--------- packages/core/ui/gestures/index.ios.ts | 35 ++- 4 files changed, 205 insertions(+), 146 deletions(-) diff --git a/packages/core/ui/gestures/gestures-common.ts b/packages/core/ui/gestures/gestures-common.ts index ac2cf9ce6a..3390789999 100644 --- a/packages/core/ui/gestures/gestures-common.ts +++ b/packages/core/ui/gestures/gestures-common.ts @@ -1,4 +1,4 @@ -import type { GesturesObserverDefinition, GestureEvents, GestureStateTypes, SwipeDirection, TouchAction, GestureEventData, TapGestureEventData, TouchGestureEventData, Pointer, GestureEventDataWithState, PinchGestureEventData, SwipeGestureEventData, PanGestureEventData, RotationGestureEventData } from './gestures-types'; +import type { GesturesObserverDefinition, GestureEventData } from './gestures-types'; import { GestureTypes } from './gestures-types'; import type { View } from '../core/view'; diff --git a/packages/core/ui/gestures/gestures-types.ts b/packages/core/ui/gestures/gestures-types.ts index 13e249d16d..f9dd4e0475 100644 --- a/packages/core/ui/gestures/gestures-types.ts +++ b/packages/core/ui/gestures/gestures-types.ts @@ -121,6 +121,8 @@ export interface SwipeGestureEventData extends GestureEventData { } export interface PanGestureEventData extends GestureEventDataWithState { + startX: number; + startY: number; deltaX: number; deltaY: number; } diff --git a/packages/core/ui/gestures/index.android.ts b/packages/core/ui/gestures/index.android.ts index 4c7c8f0ff9..d102c284f1 100644 --- a/packages/core/ui/gestures/index.android.ts +++ b/packages/core/ui/gestures/index.android.ts @@ -1,5 +1,5 @@ // Definitions. -import type { GestureEventData, TapGestureEventData, SwipeGestureEventData, PanGestureEventData, RotationGestureEventData, GestureEventDataWithState } from './gestures-types'; +import type { GestureEventData, TapGestureEventData, SwipeGestureEventData, PanGestureEventData, RotationGestureEventData, GestureEventDataWithState, PinchGestureEventData } from './gestures-types'; import type { View } from '../core/view'; import { EventData } from '../../data/observable'; @@ -15,6 +15,13 @@ export * from './gestures-common'; export * from './gestures-types'; export * from './touch-manager'; +type MotionEventCoordinates = { + rawX: number; + rawY: number; + x: number; + y: number; +}; + interface TapAndDoubleTapGestureListener { new (observer: GesturesObserver, target: View, type: number): android.view.GestureDetector.SimpleOnGestureListener; } @@ -128,8 +135,7 @@ function initializePinchGestureListener() { public onScaleBegin(detector: android.view.ScaleGestureDetector): boolean { this._scale = detector.getScaleFactor(); - const args = new PinchGestureEventData(this._target, detector, this._scale, this._target, GestureStateTypes.began); - + const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.began, detector); _executeCallback(this._observer, args); return true; @@ -138,8 +144,7 @@ function initializePinchGestureListener() { public onScale(detector: android.view.ScaleGestureDetector): boolean { this._scale *= detector.getScaleFactor(); - const args = new PinchGestureEventData(this._target, detector, this._scale, this._target, GestureStateTypes.changed); - + const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.changed, detector); _executeCallback(this._observer, args); return true; @@ -148,8 +153,7 @@ function initializePinchGestureListener() { public onScaleEnd(detector: android.view.ScaleGestureDetector): void { this._scale *= detector.getScaleFactor(); - const args = new PinchGestureEventData(this._target, detector, this._scale, this._target, GestureStateTypes.ended); - + const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.ended, detector); _executeCallback(this._observer, args); } } @@ -425,17 +429,18 @@ function _getSwipeArgs(direction: SwipeDirection, view: View, initialEvent: andr }; } -function _getPanArgs(deltaX: number, deltaY: number, view: View, state: GestureStateTypes, initialEvent: android.view.MotionEvent, currentEvent: android.view.MotionEvent): PanGestureEventData { - return { - type: GestureTypes.pan, +function _getPinchArgs(scale: number, view: View, state: GestureStateTypes, currentEvent: android.view.ScaleGestureDetector): PinchGestureEventData { + return { + type: GestureTypes.pinch, view: view, - android: { initial: initialEvent, current: currentEvent }, - deltaX: deltaX, - deltaY: deltaY, + getFocusX: () => layout.toDeviceIndependentPixels(currentEvent.getFocusX()), + getFocusY: () => layout.toDeviceIndependentPixels(currentEvent.getFocusY()), + scale, + android: currentEvent, ios: undefined, + state, object: view, - eventName: toString(GestureTypes.pan), - state: state, + eventName: toString(GestureTypes.pinch), }; } @@ -445,206 +450,228 @@ function _executeCallback(observer: GesturesObserver, args: GestureEventData) { } } -class PinchGestureEventData implements PinchGestureEventData { - public type = GestureTypes.pinch; - public eventName = toString(GestureTypes.pinch); - public ios; +abstract class CustomGestureDetector { + protected readonly _observer: GesturesObserver; + protected readonly _target: View; + protected readonly _density: number; - constructor( - public view: View, - public android: android.view.ScaleGestureDetector, - public scale: number, - public object: any, - public state: GestureStateTypes, - ) {} - - getFocusX(): number { - return this.android.getFocusX() / layout.getDisplayDensity(); - } - getFocusY(): number { - return this.android.getFocusY() / layout.getDisplayDensity(); + constructor(observer: GesturesObserver, target: View) { + this._observer = observer; + this._target = target; + this._density = layout.getDisplayDensity(); } -} - -class CustomPanGestureDetector { - private observer: GesturesObserver; - private target: View; - private density: number; - - private isTracking: boolean; - private deltaX: number; - private deltaY: number; - private initialX: number; - private initialY: number; + protected abstract get _isTrackingGesture(): boolean; + protected abstract _notifyEvent(state: GestureStateTypes, event: android.view.MotionEvent, initialEvent?: android.view.MotionEvent): void; + public abstract onTouchEvent(event: android.view.MotionEvent); +} - private lastEventCache: android.view.MotionEvent; +class CustomPanGestureDetector extends CustomGestureDetector { + private _isTracking: boolean = false; + private _deltaX: number; + private _deltaY: number; + private _startRawX: number; + private _startRawY: number; + private _startX: number; + private _startY: number; - constructor(observer: GesturesObserver, target: View) { - this.observer = observer; - this.target = target; - this.isTracking = false; + private _initialEventPosCache: MotionEventCoordinates; - this.density = layout.getDisplayDensity(); + protected override get _isTrackingGesture(): boolean { + return this._isTracking; } public onTouchEvent(event: android.view.MotionEvent) { switch (event.getActionMasked()) { case android.view.MotionEvent.ACTION_UP: case android.view.MotionEvent.ACTION_CANCEL: - this.trackStop(event, false); + if (this._isTrackingGesture) { + this._trackStop(event); + } break; - case android.view.MotionEvent.ACTION_DOWN: case android.view.MotionEvent.ACTION_POINTER_DOWN: case android.view.MotionEvent.ACTION_POINTER_UP: - this.trackStop(event, true); + this._trackInit(event); break; - case android.view.MotionEvent.ACTION_MOVE: - if (!this.isTracking) { - this.trackStart(event); + if (!this._isTrackingGesture) { + this._trackStart(event); } - this.trackChange(event); + this._trackChange(event); break; } return true; } - private trackStop(currentEvent: android.view.MotionEvent, cacheEvent: boolean) { - if (this.isTracking) { - const args = _getPanArgs(this.deltaX, this.deltaY, this.target, GestureStateTypes.ended, null, currentEvent); - _executeCallback(this.observer, args); + protected override _notifyEvent(state: GestureStateTypes, event: android.view.MotionEvent, initialEvent?: android.view.MotionEvent): void { + const args = { + type: GestureTypes.pan, + view: this._target, + android: { initial: initialEvent, current: event }, + startX: this._startX, + startY: this._startY, + deltaX: this._deltaX, + deltaY: this._deltaY, + ios: undefined, + object: this._target, + eventName: toString(GestureTypes.pan), + state, + }; - this.deltaX = undefined; - this.deltaY = undefined; - this.isTracking = false; - } + _executeCallback(this._observer, args); + } - if (cacheEvent) { - this.lastEventCache = currentEvent; - } else { - this.lastEventCache = undefined; + private _trackInit(currentEvent: android.view.MotionEvent) { + if (this._isTrackingGesture) { + this._trackStop(currentEvent); } + + this._initialEventPosCache = this._getEventCoordinates(currentEvent, true); } - private trackStart(currentEvent: android.view.MotionEvent) { - const inital = this.getEventCoordinates(this.lastEventCache ? this.lastEventCache : currentEvent); - this.initialX = inital.x; - this.initialY = inital.y; - this.isTracking = true; + private _trackStop(currentEvent: android.view.MotionEvent) { + this._notifyEvent(GestureStateTypes.ended, currentEvent); - const args = _getPanArgs(0, 0, this.target, GestureStateTypes.began, null, currentEvent); - _executeCallback(this.observer, args); + this._deltaX = undefined; + this._deltaY = undefined; + this._isTracking = false; + this._initialEventPosCache = undefined; } - private trackChange(currentEvent: android.view.MotionEvent) { - const current = this.getEventCoordinates(currentEvent); - this.deltaX = current.x - this.initialX; - this.deltaY = current.y - this.initialY; + private _trackStart(currentEvent: android.view.MotionEvent) { + const initialPos = this._initialEventPosCache ?? this._getEventCoordinates(currentEvent, true); - const args = _getPanArgs(this.deltaX, this.deltaY, this.target, GestureStateTypes.changed, null, currentEvent); - _executeCallback(this.observer, args); + this._startRawX = initialPos.rawX; + this._startRawY = initialPos.rawY; + this._startX = initialPos.x; + this._startY = initialPos.y; + this._deltaX = 0; + this._deltaY = 0; + this._isTracking = true; + + this._notifyEvent(GestureStateTypes.began, currentEvent); + } + + private _trackChange(currentEvent: android.view.MotionEvent) { + // Use screen coordinates for smoothness + const currentPos = this._getEventCoordinates(currentEvent, false); + + this._deltaX = currentPos.rawX - this._startRawX; + this._deltaY = currentPos.rawY - this._startRawY; + + this._notifyEvent(GestureStateTypes.changed, currentEvent); } - private getEventCoordinates(event: android.view.MotionEvent): { x: number; y: number } { + private _getEventCoordinates(event: android.view.MotionEvent, includeLocInView: boolean): MotionEventCoordinates { + const res = { rawX: 0, rawY: 0, x: undefined, y: undefined }; const count = event.getPointerCount(); + if (count === 1) { - return { - x: event.getRawX() / this.density, - y: event.getRawY() / this.density, - }; + res.rawX = event.getRawX() / this._density; + res.rawY = event.getRawY() / this._density; + + if (includeLocInView) { + res.x = event.getX() / this._density; + res.y = event.getY() / this._density; + } } else { const offX = event.getRawX() - event.getX(); const offY = event.getRawY() - event.getY(); - const res = { x: 0, y: 0 }; + + if (includeLocInView) { + res.x = 0; + res.y = 0; + } for (let i = 0; i < count; i++) { - res.x += event.getX(i) + offX; - res.y += event.getY(i) + offY; + const x = event.getX(i); + const y = event.getY(i); + + res.rawX += x + offX; + res.rawY += y + offY; + + if (includeLocInView) { + res.x += x; + res.y += y; + } } - res.x /= count * this.density; - res.y /= count * this.density; + res.rawX /= count * this._density; + res.rawY /= count * this._density; - return res; + if (includeLocInView) { + res.x /= count * this._density; + res.y /= count * this._density; + } } + + return res; } } -class CustomRotateGestureDetector { - private observer: GesturesObserver; - private target: View; - private trackedPtrId1: number; - private trackedPtrId2: number; +class CustomRotateGestureDetector extends CustomGestureDetector { + private _trackedPtrId1: number = INVALID_POINTER_ID; + private _trackedPtrId2: number = INVALID_POINTER_ID; - private initalPointersAngle: number; - private angle: number; + private _initalPointersAngle: number; + private _angle: number; - private get isTracking(): boolean { - return this.trackedPtrId1 !== INVALID_POINTER_ID && this.trackedPtrId2 !== INVALID_POINTER_ID; - } - - constructor(observer: GesturesObserver, target: View) { - this.observer = observer; - this.target = target; - - this.trackedPtrId1 = INVALID_POINTER_ID; - this.trackedPtrId2 = INVALID_POINTER_ID; + protected override get _isTrackingGesture(): boolean { + return this._trackedPtrId1 !== INVALID_POINTER_ID && this._trackedPtrId2 !== INVALID_POINTER_ID; } public onTouchEvent(event: android.view.MotionEvent) { const pointerID = event.getPointerId(event.getActionIndex()); - const wasTracking = this.isTracking; + const wasTracking = this._isTrackingGesture; switch (event.getActionMasked()) { case android.view.MotionEvent.ACTION_DOWN: case android.view.MotionEvent.ACTION_POINTER_DOWN: { let assigned = false; - if (this.trackedPtrId1 === INVALID_POINTER_ID && pointerID !== this.trackedPtrId2) { - this.trackedPtrId1 = pointerID; + if (this._trackedPtrId1 === INVALID_POINTER_ID && pointerID !== this._trackedPtrId2) { + this._trackedPtrId1 = pointerID; assigned = true; - } else if (this.trackedPtrId2 === INVALID_POINTER_ID && pointerID !== this.trackedPtrId1) { - this.trackedPtrId2 = pointerID; + } else if (this._trackedPtrId2 === INVALID_POINTER_ID && pointerID !== this._trackedPtrId1) { + this._trackedPtrId2 = pointerID; assigned = true; } - if (assigned && this.isTracking) { + if (assigned && this._isTrackingGesture) { // We have started tracking 2 pointers - this.angle = 0; - this.initalPointersAngle = this.getPointersAngle(event); - this.executeCallback(event, GestureStateTypes.began); + this._angle = 0; + this._initalPointersAngle = this.getPointersAngle(event); + this._notifyEvent(GestureStateTypes.began, event); } break; } case android.view.MotionEvent.ACTION_MOVE: - if (this.isTracking) { + if (this._isTrackingGesture) { this.updateAngle(event); - - this.executeCallback(event, GestureStateTypes.changed); + this._notifyEvent(GestureStateTypes.changed, event); } break; case android.view.MotionEvent.ACTION_UP: case android.view.MotionEvent.ACTION_POINTER_UP: - if (pointerID === this.trackedPtrId1) { - this.trackedPtrId1 = INVALID_POINTER_ID; - } else if (pointerID === this.trackedPtrId2) { - this.trackedPtrId2 = INVALID_POINTER_ID; + if (pointerID === this._trackedPtrId1) { + this._trackedPtrId1 = INVALID_POINTER_ID; + } else if (pointerID === this._trackedPtrId2) { + this._trackedPtrId2 = INVALID_POINTER_ID; } - if (wasTracking && !this.isTracking) { - this.executeCallback(event, GestureStateTypes.ended); + if (wasTracking && !this._isTrackingGesture) { + this._notifyEvent(GestureStateTypes.ended, event); } break; case android.view.MotionEvent.ACTION_CANCEL: - this.trackedPtrId1 = INVALID_POINTER_ID; - this.trackedPtrId2 = INVALID_POINTER_ID; + this._trackedPtrId1 = INVALID_POINTER_ID; + this._trackedPtrId2 = INVALID_POINTER_ID; if (wasTracking) { - this.executeCallback(event, GestureStateTypes.cancelled); + this._notifyEvent(GestureStateTypes.cancelled, event); } break; } @@ -652,24 +679,23 @@ class CustomRotateGestureDetector { return true; } - private executeCallback(event: android.view.MotionEvent, state: GestureStateTypes) { + protected override _notifyEvent(state: GestureStateTypes, event: android.view.MotionEvent, initialEvent?: android.view.MotionEvent): void { const args = { type: GestureTypes.rotation, - view: this.target, + view: this._target, android: event, - rotation: this.angle, + rotation: this._angle, ios: undefined, - object: this.target, + object: this._target, eventName: toString(GestureTypes.rotation), state: state, }; - - _executeCallback(this.observer, args); + _executeCallback(this._observer, args); } private updateAngle(event: android.view.MotionEvent) { const newPointersAngle = this.getPointersAngle(event); - let result = ((newPointersAngle - this.initalPointersAngle) * TO_DEGREES) % 360; + let result = ((newPointersAngle - this._initalPointersAngle) * TO_DEGREES) % 360; if (result < -180) { result += 360; @@ -678,14 +704,14 @@ class CustomRotateGestureDetector { result -= 360; } - this.angle = result; + this._angle = result; } private getPointersAngle(event: android.view.MotionEvent) { - const firstX = event.getX(event.findPointerIndex(this.trackedPtrId1)); - const firstY = event.getY(event.findPointerIndex(this.trackedPtrId1)); - const secondX = event.getX(event.findPointerIndex(this.trackedPtrId2)); - const secondY = event.getY(event.findPointerIndex(this.trackedPtrId2)); + const firstX = event.getX(event.findPointerIndex(this._trackedPtrId1)); + const firstY = event.getY(event.findPointerIndex(this._trackedPtrId1)); + const secondX = event.getX(event.findPointerIndex(this._trackedPtrId2)); + const secondY = event.getY(event.findPointerIndex(this._trackedPtrId2)); return Math.atan2(secondY - firstY, secondX - firstX); } diff --git a/packages/core/ui/gestures/index.ios.ts b/packages/core/ui/gestures/index.ios.ts index f4b26cae79..8337c99d97 100644 --- a/packages/core/ui/gestures/index.ios.ts +++ b/packages/core/ui/gestures/index.ios.ts @@ -359,7 +359,7 @@ function _getUIGestureRecognizerType(type: GestureTypes): typeof UIGestureRecogn case GestureTypes.pinch: return UIPinchGestureRecognizer; case GestureTypes.pan: - return UIPanGestureRecognizer; + return PanGestureRecognizer; case GestureTypes.swipe: return UISwipeGestureRecognizer; case GestureTypes.rotation: @@ -447,13 +447,15 @@ function _getSwipeData(args: GestureEventData): SwipeGestureEventData { } function _getPanData(args: GestureEventData, view: UIView): PanGestureEventData { - const recognizer = args.ios; + const recognizer = args.ios; return { type: args.type, view: args.view, ios: args.ios, android: undefined, + startX: recognizer.getStartX(), + startY: recognizer.getStartY(), deltaX: recognizer.translationInView(view).x, deltaY: recognizer.translationInView(view).y, object: args.view, @@ -534,6 +536,35 @@ class TouchGestureRecognizer extends UIGestureRecognizer { } } +@NativeClass +class PanGestureRecognizer extends UIPanGestureRecognizer { + private _startX: number = 0; + private _startY: number = 0; + + override touchesBeganWithEvent(touches: NSSet, event: any): void { + const viewPoint = this.locationInView(this.view); + + this._startX = viewPoint.x; + this._startY = viewPoint.y; + + super.touchesBeganWithEvent(touches, event); + } + + override reset(): void { + this._startX = 0; + this._startY = 0; + super.reset(); + } + + public getStartX(): number { + return this._startX; + } + + public getStartY(): number { + return this._startY; + } +} + class Pointer { public android: any = undefined; public ios: UITouch = undefined; From f26fbfc406a491b6d2b534397bf65603a202ae68 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 14 Sep 2026 15:16:30 +0300 Subject: [PATCH 2/4] ref: pan and pinch gesture changes --- packages/core/ui/gestures/index.android.ts | 148 +++++++++++---------- 1 file changed, 76 insertions(+), 72 deletions(-) diff --git a/packages/core/ui/gestures/index.android.ts b/packages/core/ui/gestures/index.android.ts index d102c284f1..6adeca9575 100644 --- a/packages/core/ui/gestures/index.android.ts +++ b/packages/core/ui/gestures/index.android.ts @@ -18,8 +18,9 @@ export * from './touch-manager'; type MotionEventCoordinates = { rawX: number; rawY: number; - x: number; - y: number; + // Use arrow functions to avoid unneeded native calls + getViewX(): number; + getViewY(): number; }; interface TapAndDoubleTapGestureListener { @@ -135,7 +136,7 @@ function initializePinchGestureListener() { public onScaleBegin(detector: android.view.ScaleGestureDetector): boolean { this._scale = detector.getScaleFactor(); - const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.began, detector); + const args = new PinchGestureEventDataImpl(this._target, detector, this._scale, this._target, GestureStateTypes.began); _executeCallback(this._observer, args); return true; @@ -144,7 +145,7 @@ function initializePinchGestureListener() { public onScale(detector: android.view.ScaleGestureDetector): boolean { this._scale *= detector.getScaleFactor(); - const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.changed, detector); + const args = new PinchGestureEventDataImpl(this._target, detector, this._scale, this._target, GestureStateTypes.changed); _executeCallback(this._observer, args); return true; @@ -153,7 +154,7 @@ function initializePinchGestureListener() { public onScaleEnd(detector: android.view.ScaleGestureDetector): void { this._scale *= detector.getScaleFactor(); - const args = _getPinchArgs(this._scale, this._target, GestureStateTypes.ended, detector); + const args = new PinchGestureEventDataImpl(this._target, detector, this._scale, this._target, GestureStateTypes.ended); _executeCallback(this._observer, args); } } @@ -429,21 +430,6 @@ function _getSwipeArgs(direction: SwipeDirection, view: View, initialEvent: andr }; } -function _getPinchArgs(scale: number, view: View, state: GestureStateTypes, currentEvent: android.view.ScaleGestureDetector): PinchGestureEventData { - return { - type: GestureTypes.pinch, - view: view, - getFocusX: () => layout.toDeviceIndependentPixels(currentEvent.getFocusX()), - getFocusY: () => layout.toDeviceIndependentPixels(currentEvent.getFocusY()), - scale, - android: currentEvent, - ios: undefined, - state, - object: view, - eventName: toString(GestureTypes.pinch), - }; -} - function _executeCallback(observer: GesturesObserver, args: GestureEventData) { if (observer && observer.callback) { observer.callback.call(observer.context, args); @@ -470,18 +456,16 @@ class CustomPanGestureDetector extends CustomGestureDetector { private _isTracking: boolean = false; private _deltaX: number; private _deltaY: number; - private _startRawX: number; - private _startRawY: number; - private _startX: number; - private _startY: number; - - private _initialEventPosCache: MotionEventCoordinates; + private _startRawX: number = 0; + private _startRawY: number = 0; + private _startX: number = 0; + private _startY: number = 0; protected override get _isTrackingGesture(): boolean { return this._isTracking; } - public onTouchEvent(event: android.view.MotionEvent) { + public override onTouchEvent(event: android.view.MotionEvent) { switch (event.getActionMasked()) { case android.view.MotionEvent.ACTION_UP: case android.view.MotionEvent.ACTION_CANCEL: @@ -492,6 +476,10 @@ class CustomPanGestureDetector extends CustomGestureDetector { case android.view.MotionEvent.ACTION_DOWN: case android.view.MotionEvent.ACTION_POINTER_DOWN: case android.view.MotionEvent.ACTION_POINTER_UP: + if (this._isTrackingGesture) { + this._trackStop(event); + } + this._trackInit(event); break; case android.view.MotionEvent.ACTION_MOVE: @@ -529,7 +517,12 @@ class CustomPanGestureDetector extends CustomGestureDetector { this._trackStop(currentEvent); } - this._initialEventPosCache = this._getEventCoordinates(currentEvent, true); + const initialPos = this._getEventCoordinates(currentEvent); + + this._startRawX = initialPos.rawX; + this._startRawY = initialPos.rawY; + this._startX = initialPos.getViewX(); + this._startY = initialPos.getViewY(); } private _trackStop(currentEvent: android.view.MotionEvent) { @@ -537,17 +530,14 @@ class CustomPanGestureDetector extends CustomGestureDetector { this._deltaX = undefined; this._deltaY = undefined; + this._startRawX = 0; + this._startRawY = 0; + this._startX = 0; + this._startY = 0; this._isTracking = false; - this._initialEventPosCache = undefined; } private _trackStart(currentEvent: android.view.MotionEvent) { - const initialPos = this._initialEventPosCache ?? this._getEventCoordinates(currentEvent, true); - - this._startRawX = initialPos.rawX; - this._startRawY = initialPos.rawY; - this._startX = initialPos.x; - this._startY = initialPos.y; this._deltaX = 0; this._deltaY = 0; this._isTracking = true; @@ -556,8 +546,7 @@ class CustomPanGestureDetector extends CustomGestureDetector { } private _trackChange(currentEvent: android.view.MotionEvent) { - // Use screen coordinates for smoothness - const currentPos = this._getEventCoordinates(currentEvent, false); + const currentPos = this._getEventCoordinates(currentEvent); this._deltaX = currentPos.rawX - this._startRawX; this._deltaY = currentPos.rawY - this._startRawY; @@ -565,47 +554,41 @@ class CustomPanGestureDetector extends CustomGestureDetector { this._notifyEvent(GestureStateTypes.changed, currentEvent); } - private _getEventCoordinates(event: android.view.MotionEvent, includeLocInView: boolean): MotionEventCoordinates { - const res = { rawX: 0, rawY: 0, x: undefined, y: undefined }; + private _getEventCoordinates(event: android.view.MotionEvent): MotionEventCoordinates { const count = event.getPointerCount(); + let res: MotionEventCoordinates; if (count === 1) { - res.rawX = event.getRawX() / this._density; - res.rawY = event.getRawY() / this._density; - - if (includeLocInView) { - res.x = event.getX() / this._density; - res.y = event.getY() / this._density; - } + res = { + rawX: event.getRawX() / this._density, + rawY: event.getRawY() / this._density, + getViewX: () => event.getX() / this._density, + getViewY: () => event.getY() / this._density, + }; } else { const offX = event.getRawX() - event.getX(); const offY = event.getRawY() - event.getY(); - - if (includeLocInView) { - res.x = 0; - res.y = 0; - } + let rawX: number = 0; + let rawY: number = 0; + let viewX: number = 0; + let viewY: number = 0; for (let i = 0; i < count; i++) { const x = event.getX(i); const y = event.getY(i); - res.rawX += x + offX; - res.rawY += y + offY; - - if (includeLocInView) { - res.x += x; - res.y += y; - } + rawX += x + offX; + rawY += y + offY; + viewX += x; + viewY += y; } - res.rawX /= count * this._density; - res.rawY /= count * this._density; - - if (includeLocInView) { - res.x /= count * this._density; - res.y /= count * this._density; - } + res = { + rawX: (rawX / count) * this._density, + rawY: (rawY / count) * this._density, + getViewX: () => (viewX / count) * this._density, + getViewY: () => (viewY / count) * this._density, + }; } return res; @@ -623,7 +606,7 @@ class CustomRotateGestureDetector extends CustomGestureDetector { return this._trackedPtrId1 !== INVALID_POINTER_ID && this._trackedPtrId2 !== INVALID_POINTER_ID; } - public onTouchEvent(event: android.view.MotionEvent) { + public override onTouchEvent(event: android.view.MotionEvent) { const pointerID = event.getPointerId(event.getActionIndex()); const wasTracking = this._isTrackingGesture; @@ -642,14 +625,14 @@ class CustomRotateGestureDetector extends CustomGestureDetector { if (assigned && this._isTrackingGesture) { // We have started tracking 2 pointers this._angle = 0; - this._initalPointersAngle = this.getPointersAngle(event); + this._initalPointersAngle = this._getPointersAngle(event); this._notifyEvent(GestureStateTypes.began, event); } break; } case android.view.MotionEvent.ACTION_MOVE: if (this._isTrackingGesture) { - this.updateAngle(event); + this._updateAngle(event); this._notifyEvent(GestureStateTypes.changed, event); } break; @@ -693,8 +676,8 @@ class CustomRotateGestureDetector extends CustomGestureDetector { _executeCallback(this._observer, args); } - private updateAngle(event: android.view.MotionEvent) { - const newPointersAngle = this.getPointersAngle(event); + private _updateAngle(event: android.view.MotionEvent) { + const newPointersAngle = this._getPointersAngle(event); let result = ((newPointersAngle - this._initalPointersAngle) * TO_DEGREES) % 360; if (result < -180) { @@ -707,7 +690,7 @@ class CustomRotateGestureDetector extends CustomGestureDetector { this._angle = result; } - private getPointersAngle(event: android.view.MotionEvent) { + private _getPointersAngle(event: android.view.MotionEvent) { const firstX = event.getX(event.findPointerIndex(this._trackedPtrId1)); const firstY = event.getY(event.findPointerIndex(this._trackedPtrId1)); const secondX = event.getX(event.findPointerIndex(this._trackedPtrId2)); @@ -737,7 +720,28 @@ class Pointer { } } -export class TouchGestureEventData { +class PinchGestureEventDataImpl implements PinchGestureEventData { + public type = GestureTypes.pinch; + public eventName = toString(GestureTypes.pinch); + public ios; + + constructor( + public view: View, + public android: android.view.ScaleGestureDetector, + public scale: number, + public object: any, + public state: GestureStateTypes, + ) {} + + getFocusX(): number { + return this.android.getFocusX() / layout.getDisplayDensity(); + } + getFocusY(): number { + return this.android.getFocusY() / layout.getDisplayDensity(); + } +} + +export class TouchGestureEventData implements GestureEventData { eventName: string = toString(GestureTypes.touch); type: GestureTypes = GestureTypes.touch; ios: any = undefined; From 24b275b20a82f79ea00e47f6626d6626dc396736 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 14 Sep 2026 17:01:25 +0300 Subject: [PATCH 3/4] fix: ios incorrect start location --- packages/core/ui/gestures/index.ios.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/ui/gestures/index.ios.ts b/packages/core/ui/gestures/index.ios.ts index 8337c99d97..afe7846363 100644 --- a/packages/core/ui/gestures/index.ios.ts +++ b/packages/core/ui/gestures/index.ios.ts @@ -542,18 +542,20 @@ class PanGestureRecognizer extends UIPanGestureRecognizer { private _startY: number = 0; override touchesBeganWithEvent(touches: NSSet, event: any): void { + super.touchesBeganWithEvent(touches, event); + + // At this point, the super call has finished executing and it's safe to get the view location const viewPoint = this.locationInView(this.view); this._startX = viewPoint.x; this._startY = viewPoint.y; - - super.touchesBeganWithEvent(touches, event); } override reset(): void { + super.reset(); + this._startX = 0; this._startY = 0; - super.reset(); } public getStartX(): number { From ca6f7410bf8d4ae03360ff33090ab7a0baaa7521 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Wed, 16 Sep 2026 02:13:09 +0300 Subject: [PATCH 4/4] chore: added missing tests mock for pan gesture recognizer --- packages/core/vitest.setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/vitest.setup.ts b/packages/core/vitest.setup.ts index 53f72a70f8..69f2eb5000 100644 --- a/packages/core/vitest.setup.ts +++ b/packages/core/vitest.setup.ts @@ -233,6 +233,7 @@ global.UIUserInterfaceIdiom = { Mac: 4, }; global.UIGestureRecognizer = function () {}; +global.UIPanGestureRecognizer = function () {}; global.UIGestureRecognizerDelegate = function () {}; global.UIAdaptivePresentationControllerDelegate = function () {}; global.UIPopoverPresentationControllerDelegate = function () {};