-
-
Notifications
You must be signed in to change notification settings - Fork 955
/
GestureHandlerOrchestrator.kt
614 lines (559 loc) · 24 KB
/
GestureHandlerOrchestrator.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
package com.swmansion.gesturehandler
import android.graphics.Matrix
import android.graphics.PointF
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import java.util.*
class GestureHandlerOrchestrator(
private val wrapperView: ViewGroup,
private val handlerRegistry: GestureHandlerRegistry,
private val viewConfigHelper: ViewConfigurationHelper,
) {
/**
* Minimum alpha (value from 0 to 1) that should be set to a view so that it can be treated as a
* gesture target. E.g. if set to 0.1 then views that less than 10% opaque will be ignored when
* traversing view hierarchy and looking for gesture handlers.
*/
var minimumAlphaForTraversal = DEFAULT_MIN_ALPHA_FOR_TRAVERSAL
private val gestureHandlers = arrayOfNulls<GestureHandler<*>?>(SIMULTANEOUS_GESTURE_HANDLER_LIMIT)
private val awaitingHandlers = arrayOfNulls<GestureHandler<*>?>(SIMULTANEOUS_GESTURE_HANDLER_LIMIT)
private val preparedHandlers = arrayOfNulls<GestureHandler<*>?>(SIMULTANEOUS_GESTURE_HANDLER_LIMIT)
private val handlersToCancel = arrayOfNulls<GestureHandler<*>?>(SIMULTANEOUS_GESTURE_HANDLER_LIMIT)
private var gestureHandlersCount = 0
private var awaitingHandlersCount = 0
private var isHandlingTouch = false
private var handlingChangeSemaphore = 0
private var finishedHandlersCleanupScheduled = false
private var activationIndex = 0
/**
* Should be called from the view wrapper
*/
fun onTouchEvent(event: MotionEvent): Boolean {
isHandlingTouch = true
val action = event.actionMasked
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
extractGestureHandlers(event)
} else if (action == MotionEvent.ACTION_CANCEL) {
cancelAll()
}
deliverEventToGestureHandlers(event)
isHandlingTouch = false
if (finishedHandlersCleanupScheduled && handlingChangeSemaphore == 0) {
cleanupFinishedHandlers()
}
return true
}
private fun scheduleFinishedHandlersCleanup() {
if (isHandlingTouch || handlingChangeSemaphore != 0) {
finishedHandlersCleanupScheduled = true
} else {
cleanupFinishedHandlers()
}
}
private inline fun compactHandlersIf(handlers: Array<GestureHandler<*>?>, count: Int, predicate: (handler: GestureHandler<*>?) -> Boolean): Int {
var out = 0
for (i in 0 until count) {
if (predicate(handlers[i])) {
handlers[out++] = handlers[i]
}
}
return out
}
private fun cleanupFinishedHandlers() {
var shouldCleanEmptyCells = false
for (i in gestureHandlersCount - 1 downTo 0) {
val handler = gestureHandlers[i]!!
if (isFinished(handler.state) && !handler.isAwaiting) {
gestureHandlers[i] = null
shouldCleanEmptyCells = true
handler.reset()
handler.apply {
isActive = false
isAwaiting = false
activationIndex = Int.MAX_VALUE
}
}
}
if (shouldCleanEmptyCells) {
gestureHandlersCount = compactHandlersIf(gestureHandlers, gestureHandlersCount) { handler ->
handler != null
}
}
finishedHandlersCleanupScheduled = false
}
private fun hasOtherHandlerToWaitFor(handler: GestureHandler<*>): Boolean {
for (i in 0 until gestureHandlersCount) {
val otherHandler = gestureHandlers[i]!!
if (!isFinished(otherHandler.state) && shouldHandlerWaitForOther(handler, otherHandler)) {
return true
}
}
return false
}
private fun tryActivate(handler: GestureHandler<*>) {
// see if there is anyone else who we need to wait for
if (hasOtherHandlerToWaitFor(handler)) {
addAwaitingHandler(handler)
} else {
// we can activate handler right away
makeActive(handler)
handler.isAwaiting = false
}
}
private fun cleanupAwaitingHandlers() {
awaitingHandlersCount = compactHandlersIf(awaitingHandlers, awaitingHandlersCount) { handler ->
handler!!.isAwaiting
}
}
/*package*/
fun onHandlerStateChange(handler: GestureHandler<*>, newState: Int, prevState: Int) {
handlingChangeSemaphore += 1
if (isFinished(newState)) {
// if there were handlers awaiting completion of this handler, we can trigger active state
for (i in 0 until awaitingHandlersCount) {
val otherHandler = awaitingHandlers[i]
if (shouldHandlerWaitForOther(otherHandler!!, handler)) {
if (newState == GestureHandler.STATE_END) {
// gesture has ended, we need to kill the awaiting handler
otherHandler.cancel()
otherHandler.isAwaiting = false
} else {
// gesture has failed recognition, we may try activating
tryActivate(otherHandler)
}
}
}
cleanupAwaitingHandlers()
}
if (newState == GestureHandler.STATE_ACTIVE) {
tryActivate(handler)
} else if (prevState == GestureHandler.STATE_ACTIVE || prevState == GestureHandler.STATE_END) {
if (handler.isActive) {
handler.dispatchStateChange(newState, prevState)
} else if (prevState == GestureHandler.STATE_ACTIVE) {
// handle edge case where handler awaiting for another one tries to activate but finishes
// before the other would not send state change event upon ending
handler.dispatchStateChange(newState, GestureHandler.STATE_BEGAN)
}
} else if (prevState != GestureHandler.STATE_UNDETERMINED || newState != GestureHandler.STATE_CANCELLED) {
// If handler is changing state from UNDETERMINED to CANCELLED, the state change event shouldn't
// be sent. Handler hasn't yet began so it may not be initialized which results in crashes.
// If it doesn't crash, there may be some weird behavior on JS side, as `onFinalize` will be
// called without calling `onBegin` first.
handler.dispatchStateChange(newState, prevState)
}
handlingChangeSemaphore -= 1
scheduleFinishedHandlersCleanup()
}
private fun makeActive(handler: GestureHandler<*>) {
val currentState = handler.state
with(handler) {
isAwaiting = false
isActive = true
shouldResetProgress = true
activationIndex = this@GestureHandlerOrchestrator.activationIndex++
}
var toCancelCount = 0
// Cancel all handlers that are required to be cancel upon current handler's activation
for (i in 0 until gestureHandlersCount) {
val otherHandler = gestureHandlers[i]!!
if (shouldHandlerBeCancelledBy(otherHandler, handler)) {
handlersToCancel[toCancelCount++] = otherHandler
}
}
for (i in toCancelCount - 1 downTo 0) {
handlersToCancel[i]!!.cancel()
}
// Clear all awaiting handlers waiting for the current handler to fail
for (i in awaitingHandlersCount - 1 downTo 0) {
val otherHandler = awaitingHandlers[i]!!
if (shouldHandlerBeCancelledBy(otherHandler, handler)) {
otherHandler.cancel()
otherHandler.isAwaiting = false
}
}
cleanupAwaitingHandlers()
// Dispatch state change event if handler is no longer in the active state we should also
// trigger END state change and UNDETERMINED state change if necessary
handler.dispatchStateChange(GestureHandler.STATE_ACTIVE, GestureHandler.STATE_BEGAN)
if (currentState != GestureHandler.STATE_ACTIVE) {
handler.dispatchStateChange(GestureHandler.STATE_END, GestureHandler.STATE_ACTIVE)
if (currentState != GestureHandler.STATE_END) {
handler.dispatchStateChange(GestureHandler.STATE_UNDETERMINED, GestureHandler.STATE_END)
}
}
}
private fun deliverEventToGestureHandlers(event: MotionEvent) {
// Copy handlers to "prepared handlers" array, because the list of active handlers can change
// as a result of state updates
val handlersCount = gestureHandlersCount
gestureHandlers.copyInto(preparedHandlers, 0, 0, handlersCount)
// We want to deliver events to active handlers first in order of their activation (handlers
// that activated first will first get event delivered). Otherwise we deliver events in the
// order in which handlers has been added ("most direct" children goes first). Therefore we rely
// on Arrays.sort providing a stable sort (as children are registered in order in which they
// should be tested)
preparedHandlers.sortWith(handlersComparator, 0, handlersCount)
for (i in 0 until handlersCount) {
deliverEventToGestureHandler(preparedHandlers[i]!!, event)
}
}
private fun cancelAll() {
for (i in awaitingHandlersCount - 1 downTo 0) {
awaitingHandlers[i]!!.cancel()
}
// Copy handlers to "prepared handlers" array, because the list of active handlers can change
// as a result of state updates
val handlersCount = gestureHandlersCount
for (i in 0 until handlersCount) {
preparedHandlers[i] = gestureHandlers[i]
}
for (i in handlersCount - 1 downTo 0) {
preparedHandlers[i]!!.cancel()
}
}
private fun deliverEventToGestureHandler(handler: GestureHandler<*>, event: MotionEvent) {
if (!isViewAttachedUnderWrapper(handler.view)) {
handler.cancel()
return
}
if (!handler.wantEvents()) {
return
}
val action = event.actionMasked
val coords = tempCoords
extractCoordsForView(handler.view, event, coords)
val oldX = event.x
val oldY = event.y
// TODO: we may consider scaling events if necessary using MotionEvent.transform
// for now the events are only offset to the top left corner of the view but if
// view or any ot the parents is scaled the other pointers position will not reflect
// their actual place in the view. On the other hand not scaling seems like a better
// approach when we want to use pointer coordinates to calculate velocity or distance
// for pinch so I don't know yet if we should transform or not...
event.setLocation(coords[0], coords[1])
// Touch events are sent before the handler itself has a chance to process them,
// mainly because `onTouchesUp` shoul be send befor gesture finishes. This means that
// the first `onTouchesDown` event is sent before a gesture begins, activation in
// callback for this event causes problems because the handler doesn't have a chance
// to initialize itself with starting values of pointer (in pan this causes translation
// to be equal to the coordinates of the pointer). The simplest solution is to send
// the first `onTouchesDown` event after the handler processes it and changes state
// to `BEGAN`.
if (handler.needsPointerData && handler.state != 0) {
handler.updatePointerData(event)
}
if (!handler.isAwaiting || action != MotionEvent.ACTION_MOVE) {
val isFirstEvent = handler.state == 0
handler.handle(event)
if (handler.isActive) {
// After handler is done waiting for other one to fail its progress should be
// reset, otherwise there may be a visible jump in values sent by the handler.
// When handler is waiting it's already activated but the `isAwaiting` flag
// prevents it from receiving touch stream. When the flag is changed, the
// difference between this event and the last one may be large enough to be
// visible in interactions based on this gesture. This makes it consistent with
// the behavior on iOS.
if (handler.shouldResetProgress) {
handler.shouldResetProgress = false
handler.resetProgress()
}
handler.dispatchHandlerUpdate(event)
}
if (handler.needsPointerData && isFirstEvent) {
handler.updatePointerData(event)
}
// if event was of type UP or POINTER_UP we request handler to stop tracking now that
// the event has been dispatched
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
val pointerId = event.getPointerId(event.actionIndex)
handler.stopTrackingPointer(pointerId)
}
}
event.setLocation(oldX, oldY)
}
/**
* isViewAttachedUnderWrapper checks whether all of parents for view related to handler
* view are attached. Since there might be an issue rarely observed when view
* has been detached and handler's state hasn't been change to canceled, failed or
* ended yet. Probably it's a result of some race condition and stopping delivering
* for this handler and changing its state to failed of end appear to be good enough solution.
*/
private fun isViewAttachedUnderWrapper(view: View?): Boolean {
if (view == null) {
return false
}
if (view === wrapperView) {
return true
}
var parent = view.parent
while (parent != null && parent !== wrapperView) {
parent = parent.parent
}
return parent === wrapperView
}
private fun extractCoordsForView(view: View?, event: MotionEvent, outputCoords: FloatArray) {
if (view === wrapperView) {
outputCoords[0] = event.x
outputCoords[1] = event.y
return
}
require(!(view == null || view.parent !is ViewGroup)) { "Parent is null? View is no longer in the tree" }
val parent = view.parent as ViewGroup
extractCoordsForView(parent, event, outputCoords)
val childPoint = tempPoint
transformTouchPointToViewCoords(outputCoords[0], outputCoords[1], parent, view, childPoint)
outputCoords[0] = childPoint.x
outputCoords[1] = childPoint.y
}
private fun addAwaitingHandler(handler: GestureHandler<*>) {
for (i in 0 until awaitingHandlersCount) {
if (awaitingHandlers[i] === handler) {
return
}
}
check(awaitingHandlersCount < awaitingHandlers.size) { "Too many recognizers" }
awaitingHandlers[awaitingHandlersCount++] = handler
with(handler) {
isAwaiting = true
activationIndex = this@GestureHandlerOrchestrator.activationIndex++
}
}
private fun recordHandlerIfNotPresent(handler: GestureHandler<*>, view: View) {
for (i in 0 until gestureHandlersCount) {
if (gestureHandlers[i] === handler) {
return
}
}
check(gestureHandlersCount < gestureHandlers.size) { "Too many recognizers" }
gestureHandlers[gestureHandlersCount++] = handler
handler.isActive = false
handler.isAwaiting = false
handler.activationIndex = Int.MAX_VALUE
handler.prepare(view, this)
}
private fun isViewOverflowingParent(view: View): Boolean {
val parent = view.parent as? ViewGroup ?: return false
val matrix = view.matrix
val localXY = matrixTransformCoords
localXY[0] = 0f
localXY[1] = 0f
matrix.mapPoints(localXY)
val left = localXY[0] + view.left
val top = localXY[1] + view.top
return left < 0f || left + view.width > parent.width || top < 0f || top + view.height > parent.height
}
private fun extractAncestorHandlers(view: View, coords: FloatArray, pointerId: Int): Boolean {
var found = false
var parent = view.parent
while (parent != null) {
if (parent is ViewGroup) {
val parentViewGroup: ViewGroup = parent
handlerRegistry.getHandlersForView(parent)?.let {
synchronized(it) {
for (handler in it) {
if (handler.isEnabled && handler.isWithinBounds(view, coords[0], coords[1])) {
found = true
recordHandlerIfNotPresent(handler, parentViewGroup)
handler.startTrackingPointer(pointerId)
}
}
}
}
}
parent = parent.parent
}
return found
}
private fun recordViewHandlersForPointer(view: View, coords: FloatArray, pointerId: Int): Boolean {
var found = false
handlerRegistry.getHandlersForView(view)?.let {
synchronized(it) {
for (handler in it) {
if (handler.isEnabled && handler.isWithinBounds(view, coords[0], coords[1])) {
recordHandlerIfNotPresent(handler, view)
handler.startTrackingPointer(pointerId)
found = true
}
}
}
}
// if the pointer is inside the view but it overflows its parent, handlers attached to the parent
// might not have been extracted (pointer might be in a child, but may be outside parent)
if (coords[0] in 0f..view.width.toFloat() && coords[1] in 0f..view.height.toFloat()
&& isViewOverflowingParent(view) && extractAncestorHandlers(view, coords, pointerId)) {
found = true
}
return found
}
private fun extractGestureHandlers(event: MotionEvent) {
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
tempCoords[0] = event.getX(actionIndex)
tempCoords[1] = event.getY(actionIndex)
traverseWithPointerEvents(wrapperView, tempCoords, pointerId)
extractGestureHandlers(wrapperView, tempCoords, pointerId)
}
private fun extractGestureHandlers(viewGroup: ViewGroup, coords: FloatArray, pointerId: Int): Boolean {
val childrenCount = viewGroup.childCount
for (i in childrenCount - 1 downTo 0) {
val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i)
if (canReceiveEvents(child)) {
val childPoint = tempPoint
transformTouchPointToViewCoords(coords[0], coords[1], viewGroup, child, childPoint)
val restoreX = coords[0]
val restoreY = coords[1]
coords[0] = childPoint.x
coords[1] = childPoint.y
var found = false
if (!isClipping(child) || isTransformedTouchPointInView(coords[0], coords[1], child)) {
// we only consider the view if touch is inside the view bounds or if the view's children
// can render outside of the view bounds (overflow visible)
found = traverseWithPointerEvents(child, coords, pointerId)
}
coords[0] = restoreX
coords[1] = restoreY
if (found) {
return true
}
}
}
return false
}
private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int): Boolean =
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
PointerEventsConfig.NONE -> {
// This view and its children can't be the target
false
}
PointerEventsConfig.BOX_ONLY -> {
// This view is the target, its children don't matter
(recordViewHandlersForPointer(view, coords, pointerId)
|| shouldHandlerlessViewBecomeTouchTarget(view, coords))
}
PointerEventsConfig.BOX_NONE -> {
// This view can't be the target, but its children might
when (view) {
is ViewGroup -> {
extractGestureHandlers(view, coords, pointerId).also { found ->
// A child view is handling touch, also extract handlers attached to this view
if (found) {
recordViewHandlersForPointer(view, coords, pointerId)
}
}
}
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
// handlers attached to the text input, as it makes sense that gestures would work on a
// non-editable TextInput.
is EditText -> {
recordViewHandlersForPointer(view, coords, pointerId)
}
else -> false
}
}
PointerEventsConfig.AUTO -> {
// Either this view or one of its children is the target
val found = if (view is ViewGroup) {
extractGestureHandlers(view, coords, pointerId)
} else false
(recordViewHandlersForPointer(view, coords, pointerId)
|| found || shouldHandlerlessViewBecomeTouchTarget(view, coords))
}
}
private fun canReceiveEvents(view: View) =
view.visibility == View.VISIBLE && view.alpha >= minimumAlphaForTraversal
// if view is not a view group it is clipping, otherwise we check for `getClipChildren` flag to
// be turned on and also confirm with the ViewConfigHelper implementation
private fun isClipping(view: View) =
view !is ViewGroup || viewConfigHelper.isViewClippingChildren(view)
companion object {
// The limit doesn't necessarily need to exists, it was just simpler to implement it that way
// it is also more allocation-wise efficient to have a fixed limit
private const val SIMULTANEOUS_GESTURE_HANDLER_LIMIT = 20
// Be default fully transparent views can receive touch
private const val DEFAULT_MIN_ALPHA_FOR_TRAVERSAL = 0f
private val tempPoint = PointF()
private val matrixTransformCoords = FloatArray(2)
private val inverseMatrix = Matrix()
private val tempCoords = FloatArray(2)
private val handlersComparator = Comparator<GestureHandler<*>?> { a, b ->
return@Comparator if (a.isActive && b.isActive || a.isAwaiting && b.isAwaiting) {
// both A and B are either active or awaiting activation, in which case we prefer one that
// has activated (or turned into "awaiting" state) earlier
Integer.signum(b.activationIndex - a.activationIndex)
} else if (a.isActive) {
-1 // only A is active
} else if (b.isActive) {
1 // only B is active
} else if (a.isAwaiting) {
-1 // only A is awaiting, B is inactive
} else if (b.isAwaiting) {
1 // only B is awaiting, A is inactive
} else {
0 // both A and B are inactive, stable order matters
}
}
private fun shouldHandlerlessViewBecomeTouchTarget(view: View, coords: FloatArray): Boolean {
// The following code is to match the iOS behavior where transparent parts of the views can
// pass touch events through them allowing sibling nodes to handle them.
// TODO: this is not an ideal solution as we only consider ViewGroups that has no background set
// TODO: ideally we should determine the pixel color under the given coordinates and return
// false if the color is transparent
val isLeafOrTransparent = view !is ViewGroup || view.getBackground() != null
return isLeafOrTransparent && isTransformedTouchPointInView(coords[0], coords[1], view)
}
private fun transformTouchPointToViewCoords(
x: Float,
y: Float,
parent: ViewGroup,
child: View,
outLocalPoint: PointF,
) {
var localX = x + parent.scrollX - child.left
var localY = y + parent.scrollY - child.top
val matrix = child.matrix
if (!matrix.isIdentity) {
val localXY = matrixTransformCoords
localXY[0] = localX
localXY[1] = localY
matrix.invert(inverseMatrix)
inverseMatrix.mapPoints(localXY)
localX = localXY[0]
localY = localXY[1]
}
outLocalPoint[localX] = localY
}
private fun isTransformedTouchPointInView(x: Float, y: Float, child: View) =
x in 0f..child.width.toFloat() && y in 0f..child.height.toFloat()
private fun shouldHandlerWaitForOther(handler: GestureHandler<*>, other: GestureHandler<*>): Boolean {
return handler !== other && (handler.shouldWaitForHandlerFailure(other)
|| other.shouldRequireToWaitForFailure(handler))
}
private fun canRunSimultaneously(a: GestureHandler<*>, b: GestureHandler<*>) =
a === b || a.shouldRecognizeSimultaneously(b) || b.shouldRecognizeSimultaneously(a)
private fun shouldHandlerBeCancelledBy(handler: GestureHandler<*>, other: GestureHandler<*>): Boolean {
if (!handler.hasCommonPointers(other)) {
// if two handlers share no common pointer one can never trigger cancel for the other
return false
}
if (canRunSimultaneously(handler, other)) {
// if handlers are allowed to run simultaneously, when first activates second can still remain
// in began state
return false
}
return if (handler !== other &&
(handler.isAwaiting || handler.state == GestureHandler.STATE_ACTIVE)) {
// in every other case as long as the handler is about to be activated or already in active
// state, we delegate the decision to the implementation of GestureHandler#shouldBeCancelledBy
handler.shouldBeCancelledBy(other)
} else true
}
private fun isFinished(state: Int) =
state == GestureHandler.STATE_CANCELLED
|| state == GestureHandler.STATE_FAILED
|| state == GestureHandler.STATE_END
}
}