Skip to content

Commit 8254e4a

Browse files
authored
Transform entire MotionEvent instead of single pointer only (#2156)
## Description Gesture Handler on Android intercepts events via the `GestureHandlerRootView`, which means that we need to transform the touch coordinated by ourselves to check if it's inside the view. Similarly, we need to transform the event when dispatching it to one of the gesture handlers in order to correctly calculate the changes and send touch events. The current method works well until there is more than one pointer at once on the screen. When there is more than one pointer, only the first one will be correctly transformed, while others will simply be moved by the offset calculated when transforming the first pointer. This PR changes the logic when delivering events to gesture handlers - the whole event is transformed, and handlers receive both events - transformed and untransformed one. This way things like touch events and `NativeViewGestureHandler` which rely on the location in the coordinate space of the view keep working, and other gestures get to work in the coordinate space of the root view. Fixes #2138. ## Test plan Tested on the Example app.
1 parent bd85bf7 commit 8254e4a

File tree

12 files changed

+188
-108
lines changed

12 files changed

+188
-108
lines changed

android/lib/src/main/java/com/swmansion/gesturehandler/FlingGestureHandler.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,19 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
6363
}
6464
}
6565

66-
override fun onHandle(event: MotionEvent) {
66+
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
6767
val state = state
6868
if (state == STATE_UNDETERMINED) {
69-
startFling(event)
69+
startFling(sourceEvent)
7070
}
7171
if (state == STATE_BEGAN) {
72-
tryEndFling(event)
73-
if (event.pointerCount > maxNumberOfPointersSimultaneously) {
74-
maxNumberOfPointersSimultaneously = event.pointerCount
72+
tryEndFling(sourceEvent)
73+
if (sourceEvent.pointerCount > maxNumberOfPointersSimultaneously) {
74+
maxNumberOfPointersSimultaneously = sourceEvent.pointerCount
7575
}
76-
val action = event.actionMasked
76+
val action = sourceEvent.actionMasked
7777
if (action == MotionEvent.ACTION_UP) {
78-
endFling(event)
78+
endFling(sourceEvent)
7979
}
8080
}
8181
}

android/lib/src/main/java/com/swmansion/gesturehandler/GestureHandler.kt

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swmansion.gesturehandler
33
import android.app.Activity
44
import android.content.Context
55
import android.content.ContextWrapper
6+
import android.graphics.PointF
67
import android.graphics.Rect
78
import android.view.MotionEvent
89
import android.view.MotionEvent.PointerCoords
@@ -325,7 +326,7 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
325326
while handling event: $event
326327
""".trimIndent(), e) {}
327328

328-
fun handle(origEvent: MotionEvent) {
329+
fun handle(transformedEvent: MotionEvent, sourceEvent: MotionEvent) {
329330
if (!isEnabled
330331
|| state == STATE_CANCELLED
331332
|| state == STATE_FAILED
@@ -335,20 +336,20 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
335336
}
336337

337338
// a workaround for https://github.com/software-mansion/react-native-gesture-handler/issues/1188
338-
val event = if (BuildConfig.DEBUG) {
339-
adaptEvent(origEvent)
339+
val (adaptedTransformedEvent, adaptedSourceEvent) = if (BuildConfig.DEBUG) {
340+
arrayOf(adaptEvent(transformedEvent), adaptEvent(sourceEvent))
340341
} else {
341342
try {
342-
adaptEvent(origEvent)
343+
arrayOf(adaptEvent(transformedEvent), adaptEvent(sourceEvent))
343344
} catch (e: AdaptEventException) {
344345
fail()
345346
return
346347
}
347348
}
348349

349-
x = event.x
350-
y = event.y
351-
numberOfPointers = event.pointerCount
350+
x = adaptedTransformedEvent.x
351+
y = adaptedTransformedEvent.y
352+
numberOfPointers = adaptedTransformedEvent.pointerCount
352353
isWithinBounds = isWithinBounds(view, x, y)
353354
if (shouldCancelWhenOutside && !isWithinBounds) {
354355
if (state == STATE_ACTIVE) {
@@ -358,13 +359,16 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
358359
}
359360
return
360361
}
361-
lastAbsolutePositionX = GestureUtils.getLastPointerX(event, true)
362-
lastAbsolutePositionY = GestureUtils.getLastPointerY(event, true)
363-
lastEventOffsetX = event.rawX - event.x
364-
lastEventOffsetY = event.rawY - event.y
365-
onHandle(event)
366-
if (event != origEvent) {
367-
event.recycle()
362+
lastAbsolutePositionX = GestureUtils.getLastPointerX(adaptedTransformedEvent, true)
363+
lastAbsolutePositionY = GestureUtils.getLastPointerY(adaptedTransformedEvent, true)
364+
lastEventOffsetX = adaptedTransformedEvent.rawX - adaptedTransformedEvent.x
365+
lastEventOffsetY = adaptedTransformedEvent.rawY - adaptedTransformedEvent.y
366+
onHandle(adaptedTransformedEvent, adaptedSourceEvent)
367+
if (adaptedTransformedEvent != transformedEvent) {
368+
adaptedTransformedEvent.recycle()
369+
}
370+
if (adaptedSourceEvent != sourceEvent) {
371+
adaptedSourceEvent.recycle()
368372
}
369373
}
370374

@@ -659,13 +663,29 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
659663
// if the handler is waiting for failure of other one)
660664
open fun resetProgress() {}
661665

662-
protected open fun onHandle(event: MotionEvent) {
666+
protected open fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
663667
moveToState(STATE_FAILED)
664668
}
665669

666670
protected open fun onStateChange(newState: Int, previousState: Int) {}
667671
protected open fun onReset() {}
668672
protected open fun onCancel() {}
673+
674+
/**
675+
* Transforms a point in the coordinate space of the wrapperView (GestureHandlerRootView) to
676+
* coordinate space of the view the gesture is attached to.
677+
*
678+
* If the gesture handler is not currently attached to a view, it will return (NaN, NaN).
679+
*
680+
* This method modifies and transforms the received point.
681+
*/
682+
protected fun transformPoint(point: PointF): PointF {
683+
return orchestrator?.transformPointToViewCoords(this.view, point) ?: run {
684+
point.x = Float.NaN
685+
point.y = Float.NaN
686+
point
687+
}
688+
}
669689
fun reset() {
670690
view = null
671691
orchestrator = null
@@ -689,14 +709,14 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
689709
}
690710

691711
val lastRelativePositionX: Float
692-
get() = lastAbsolutePositionX - lastEventOffsetX
712+
get() = lastAbsolutePositionX
693713
val lastRelativePositionY: Float
694-
get() = lastAbsolutePositionY - lastEventOffsetY
714+
get() = lastAbsolutePositionY
695715

696716
val lastPositionInWindowX: Float
697-
get() = lastAbsolutePositionX - windowOffset[0]
717+
get() = lastAbsolutePositionX + lastEventOffsetX - windowOffset[0]
698718
val lastPositionInWindowY: Float
699-
get() = lastAbsolutePositionY - windowOffset[1]
719+
get() = lastAbsolutePositionY + lastEventOffsetY - windowOffset[1]
700720

701721
companion object {
702722
const val STATE_UNDETERMINED = 0

android/lib/src/main/java/com/swmansion/gesturehandler/GestureHandlerOrchestrator.kt

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -242,26 +242,17 @@ class GestureHandlerOrchestrator(
242242
}
243243
}
244244

245-
private fun deliverEventToGestureHandler(handler: GestureHandler<*>, event: MotionEvent) {
245+
private fun deliverEventToGestureHandler(handler: GestureHandler<*>, sourceEvent: MotionEvent) {
246246
if (!isViewAttachedUnderWrapper(handler.view)) {
247247
handler.cancel()
248248
return
249249
}
250250
if (!handler.wantEvents()) {
251251
return
252252
}
253-
val action = event.actionMasked
254-
val coords = tempCoords
255-
extractCoordsForView(handler.view, event, coords)
256-
val oldX = event.x
257-
val oldY = event.y
258-
// TODO: we may consider scaling events if necessary using MotionEvent.transform
259-
// for now the events are only offset to the top left corner of the view but if
260-
// view or any ot the parents is scaled the other pointers position will not reflect
261-
// their actual place in the view. On the other hand not scaling seems like a better
262-
// approach when we want to use pointer coordinates to calculate velocity or distance
263-
// for pinch so I don't know yet if we should transform or not...
264-
event.setLocation(coords[0], coords[1])
253+
254+
val action = sourceEvent.actionMasked
255+
val event = transformEventToViewCoords(handler.view, MotionEvent.obtain(sourceEvent))
265256

266257
// Touch events are sent before the handler itself has a chance to process them,
267258
// mainly because `onTouchesUp` shoul be send befor gesture finishes. This means that
@@ -277,7 +268,7 @@ class GestureHandlerOrchestrator(
277268

278269
if (!handler.isAwaiting || action != MotionEvent.ACTION_MOVE) {
279270
val isFirstEvent = handler.state == 0
280-
handler.handle(event)
271+
handler.handle(event, sourceEvent)
281272
if (handler.isActive) {
282273
// After handler is done waiting for other one to fail its progress should be
283274
// reset, otherwise there may be a visible jump in values sent by the handler.
@@ -305,7 +296,7 @@ class GestureHandlerOrchestrator(
305296
}
306297
}
307298

308-
event.setLocation(oldX, oldY)
299+
event.recycle()
309300
}
310301

311302
/**
@@ -329,19 +320,75 @@ class GestureHandlerOrchestrator(
329320
return parent === wrapperView
330321
}
331322

332-
private fun extractCoordsForView(view: View?, event: MotionEvent, outputCoords: FloatArray) {
333-
if (view === wrapperView) {
334-
outputCoords[0] = event.x
335-
outputCoords[1] = event.y
336-
return
323+
/**
324+
* Transforms an event in the coordinates of wrapperView into the coordinate space of the received view.
325+
*
326+
* This modifies and returns the same event as it receives
327+
*
328+
* @param view - view to which coordinate space the event should be transformed
329+
* @param event - event to transform
330+
*/
331+
fun transformEventToViewCoords(view: View?, event: MotionEvent): MotionEvent {
332+
if (view == null) {
333+
return event
337334
}
338-
require(!(view == null || view.parent !is ViewGroup)) { "Parent is null? View is no longer in the tree" }
339-
val parent = view.parent as ViewGroup
340-
extractCoordsForView(parent, event, outputCoords)
341-
val childPoint = tempPoint
342-
transformTouchPointToViewCoords(outputCoords[0], outputCoords[1], parent, view, childPoint)
343-
outputCoords[0] = childPoint.x
344-
outputCoords[1] = childPoint.y
335+
336+
val parent = view.parent as? ViewGroup
337+
// Events are passed down to the orchestrator by the wrapperView, so they are already in the
338+
// relevant coordinate space. We want to stop traversing the tree when we reach it.
339+
if (parent != wrapperView) {
340+
transformEventToViewCoords(parent, event)
341+
}
342+
343+
if (parent != null) {
344+
val localX = event.x + parent.scrollX - view.left
345+
val localY = event.y + parent.scrollY - view.top
346+
event.setLocation(localX, localY)
347+
}
348+
349+
if (!view.matrix.isIdentity) {
350+
view.matrix.invert(inverseMatrix)
351+
event.transform(inverseMatrix)
352+
}
353+
354+
return event
355+
}
356+
357+
/**
358+
* Transforms a point in the coordinates of wrapperView into the coordinate space of the received view.
359+
*
360+
* This modifies and returns the same point as it receives
361+
*
362+
* @param view - view to which coordinate space the point should be transformed
363+
* @param point - point to transform
364+
*/
365+
fun transformPointToViewCoords(view: View?, point: PointF): PointF {
366+
if (view == null) {
367+
return point
368+
}
369+
370+
val parent = view.parent as? ViewGroup
371+
// Events are passed down to the orchestrator by the wrapperView, so they are already in the
372+
// relevant coordinate space. We want to stop traversing the tree when we reach it.
373+
if (parent != wrapperView) {
374+
transformPointToViewCoords(parent, point)
375+
}
376+
377+
if (parent != null) {
378+
point.x += parent.scrollX - view.left
379+
point.y += parent.scrollY - view.top
380+
}
381+
382+
if (!view.matrix.isIdentity) {
383+
view.matrix.invert(inverseMatrix)
384+
tempCoords[0] = point.x
385+
tempCoords[1] = point.y
386+
inverseMatrix.mapPoints(tempCoords)
387+
point.x = tempCoords[0]
388+
point.y = tempCoords[1]
389+
}
390+
391+
return point
345392
}
346393

347394
private fun addAwaitingHandler(handler: GestureHandler<*>) {
@@ -451,7 +498,7 @@ class GestureHandlerOrchestrator(
451498
val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i)
452499
if (canReceiveEvents(child)) {
453500
val childPoint = tempPoint
454-
transformTouchPointToViewCoords(coords[0], coords[1], viewGroup, child, childPoint)
501+
transformPointToChildViewCoords(coords[0], coords[1], viewGroup, child, childPoint)
455502
val restoreX = coords[0]
456503
val restoreY = coords[1]
457504
coords[0] = childPoint.x
@@ -564,7 +611,7 @@ class GestureHandlerOrchestrator(
564611
return isLeafOrTransparent && isTransformedTouchPointInView(coords[0], coords[1], view)
565612
}
566613

567-
private fun transformTouchPointToViewCoords(
614+
private fun transformPointToChildViewCoords(
568615
x: Float,
569616
y: Float,
570617
parent: ViewGroup,

android/lib/src/main/java/com/swmansion/gesturehandler/GestureUtils.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import android.view.MotionEvent
44

55
object GestureUtils {
66
fun getLastPointerX(event: MotionEvent, averageTouches: Boolean): Float {
7-
val offset = event.rawX - event.x
87
val excludeIndex = if (event.actionMasked == MotionEvent.ACTION_POINTER_UP) event.actionIndex else -1
98
return if (averageTouches) {
109
var sum = 0f
1110
var count = 0
1211
for (i in 0 until event.pointerCount) {
1312
if (i != excludeIndex) {
14-
sum += event.getX(i) + offset
13+
sum += event.getX(i)
1514
count++
1615
}
1716
}
@@ -21,19 +20,18 @@ object GestureUtils {
2120
if (lastPointerIdx == excludeIndex) {
2221
lastPointerIdx--
2322
}
24-
event.getX(lastPointerIdx) + offset
23+
event.getX(lastPointerIdx)
2524
}
2625
}
2726

2827
fun getLastPointerY(event: MotionEvent, averageTouches: Boolean): Float {
29-
val offset = event.rawY - event.y
3028
val excludeIndex = if (event.actionMasked == MotionEvent.ACTION_POINTER_UP) event.actionIndex else -1
3129
return if (averageTouches) {
3230
var sum = 0f
3331
var count = 0
3432
for (i in 0 until event.pointerCount) {
3533
if (i != excludeIndex) {
36-
sum += event.getY(i) + offset
34+
sum += event.getY(i)
3735
count++
3836
}
3937
}
@@ -43,7 +41,7 @@ object GestureUtils {
4341
if (lastPointerIdx == excludeIndex) {
4442
lastPointerIdx -= 1
4543
}
46-
event.getY(lastPointerIdx) + offset
44+
event.getY(lastPointerIdx)
4745
}
4846
}
4947
}

android/lib/src/main/java/com/swmansion/gesturehandler/LongPressGestureHandler.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,21 @@ class LongPressGestureHandler(context: Context) : GestureHandler<LongPressGestur
3737
return this
3838
}
3939

40-
override fun onHandle(event: MotionEvent) {
40+
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
4141
if (state == STATE_UNDETERMINED) {
4242
previousTime = SystemClock.uptimeMillis()
4343
startTime = previousTime
4444
begin()
45-
startX = event.rawX
46-
startY = event.rawY
45+
startX = sourceEvent.rawX
46+
startY = sourceEvent.rawY
4747
handler = Handler(Looper.getMainLooper())
4848
if (minDurationMs > 0) {
4949
handler!!.postDelayed({ activate() }, minDurationMs)
5050
} else if (minDurationMs == 0L) {
5151
activate()
5252
}
5353
}
54-
if (event.actionMasked == MotionEvent.ACTION_UP) {
54+
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) {
5555
handler?.let {
5656
it.removeCallbacksAndMessages(null)
5757
handler = null
@@ -63,8 +63,8 @@ class LongPressGestureHandler(context: Context) : GestureHandler<LongPressGestur
6363
}
6464
} else {
6565
// calculate distance from start
66-
val deltaX = event.rawX - startX
67-
val deltaY = event.rawY - startY
66+
val deltaX = sourceEvent.rawX - startX
67+
val deltaY = sourceEvent.rawY - startY
6868
val distSq = deltaX * deltaX + deltaY * deltaY
6969
if (distSq > maxDistSq) {
7070
if (state == STATE_ACTIVE) {

android/lib/src/main/java/com/swmansion/gesturehandler/ManualGestureHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.swmansion.gesturehandler
33
import android.view.MotionEvent
44

55
class ManualGestureHandler : GestureHandler<ManualGestureHandler>() {
6-
override fun onHandle(event: MotionEvent) {
6+
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
77
if (state == STATE_UNDETERMINED) {
88
begin()
99
}

0 commit comments

Comments
 (0)