diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt b/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt index 75acef796a..3f136555f0 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt @@ -3,18 +3,19 @@ package com.swmansion.gesturehandler.core import android.os.Handler import android.os.Looper import android.view.MotionEvent +import android.view.VelocityTracker class FlingGestureHandler : GestureHandler() { var numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED var direction = DEFAULT_DIRECTION private val maxDurationMs = DEFAULT_MAX_DURATION_MS - private val minAcceptableDelta = DEFAULT_MIN_ACCEPTABLE_DELTA - private var startX = 0f - private var startY = 0f + private val minVelocity = DEFAULT_MIN_VELOCITY + private val minDirectionalAlignment = DEFAULT_MIN_DIRECTION_ALIGNMENT private var handler: Handler? = null private var maxNumberOfPointersSimultaneously = 0 private val failDelayed = Runnable { fail() } + private var velocityTracker: VelocityTracker? = null override fun resetConfig() { super.resetConfig() @@ -23,8 +24,7 @@ class FlingGestureHandler : GestureHandler() { } private fun startFling(event: MotionEvent) { - startX = event.rawX - startY = event.rawY + velocityTracker = VelocityTracker.obtain() begin() maxNumberOfPointersSimultaneously = 1 if (handler == null) { @@ -35,26 +35,40 @@ class FlingGestureHandler : GestureHandler() { handler!!.postDelayed(failDelayed, maxDurationMs) } - private fun tryEndFling(event: MotionEvent) = if ( - maxNumberOfPointersSimultaneously == numberOfPointersRequired && - ( - direction and DIRECTION_RIGHT != 0 && - event.rawX - startX > minAcceptableDelta || - direction and DIRECTION_LEFT != 0 && - startX - event.rawX > minAcceptableDelta || - direction and DIRECTION_UP != 0 && - startY - event.rawY > minAcceptableDelta || - direction and DIRECTION_DOWN != 0 && - event.rawY - startY > minAcceptableDelta + private fun tryEndFling(event: MotionEvent): Boolean { + addVelocityMovement(velocityTracker, event) + + val velocityVector = Vector.fromVelocity(velocityTracker!!) + + fun getVelocityAlignment( + direction: Int, + ): Boolean = ( + this.direction and direction != 0 && + velocityVector.isSimilar(Vector.fromDirection(direction), minDirectionalAlignment) ) - ) { - handler!!.removeCallbacksAndMessages(null) - activate() - true - } else { - false - } + val alignmentList = arrayOf( + DIRECTION_LEFT, + DIRECTION_RIGHT, + DIRECTION_UP, + DIRECTION_DOWN, + ).map { direction -> getVelocityAlignment(direction) } + + val isAligned = alignmentList.any { it } + val isFast = velocityVector.magnitude > this.minVelocity + + return if ( + maxNumberOfPointersSimultaneously == numberOfPointersRequired && + isAligned && + isFast + ) { + handler!!.removeCallbacksAndMessages(null) + activate() + true + } else { + false + } + } override fun activate(force: Boolean) { super.activate(force) end() @@ -92,12 +106,23 @@ class FlingGestureHandler : GestureHandler() { } override fun onReset() { + velocityTracker?.recycle() + velocityTracker = null handler?.removeCallbacksAndMessages(null) } + private fun addVelocityMovement(tracker: VelocityTracker?, event: MotionEvent) { + val offsetX = event.rawX - event.x + val offsetY = event.rawY - event.y + event.offsetLocation(offsetX, offsetY) + tracker!!.addMovement(event) + event.offsetLocation(-offsetX, -offsetY) + } + companion object { private const val DEFAULT_MAX_DURATION_MS: Long = 800 - private const val DEFAULT_MIN_ACCEPTABLE_DELTA: Long = 160 + private const val DEFAULT_MIN_VELOCITY: Long = 2000 + private const val DEFAULT_MIN_DIRECTION_ALIGNMENT: Double = 0.75 private const val DEFAULT_DIRECTION = DIRECTION_RIGHT private const val DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1 } diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt b/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt new file mode 100644 index 0000000000..f21d992498 --- /dev/null +++ b/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt @@ -0,0 +1,56 @@ +package com.swmansion.gesturehandler.core + +import android.view.VelocityTracker +import com.swmansion.gesturehandler.core.GestureHandler.Companion.DIRECTION_DOWN +import com.swmansion.gesturehandler.core.GestureHandler.Companion.DIRECTION_LEFT +import com.swmansion.gesturehandler.core.GestureHandler.Companion.DIRECTION_RIGHT +import com.swmansion.gesturehandler.core.GestureHandler.Companion.DIRECTION_UP +import kotlin.math.hypot + +class Vector(val x: Double, val y: Double) { + private val unitX: Double + private val unitY: Double + val magnitude = hypot(x, y) + + init { + val isMagnitudeSufficient = magnitude > MINIMAL_MAGNITUDE + + unitX = if (isMagnitudeSufficient) x / magnitude else 0.0 + unitY = if (isMagnitudeSufficient) y / magnitude else 0.0 + } + + private fun computeSimilarity(vector: Vector): Double { + return unitX * vector.unitX + unitY * vector.unitY + } + + fun isSimilar(vector: Vector, threshold: Double): Boolean { + return computeSimilarity(vector) > threshold + } + + companion object { + private val VECTOR_LEFT: Vector = Vector(-1.0, 0.0) + private val VECTOR_RIGHT: Vector = Vector(1.0, 0.0) + private val VECTOR_UP: Vector = Vector(0.0, -1.0) + private val VECTOR_DOWN: Vector = Vector(0.0, 1.0) + private val VECTOR_ZERO: Vector = Vector(0.0, 0.0) + const val MINIMAL_MAGNITUDE = 0.1 + + fun fromDirection(direction: Int): Vector = + when (direction) { + DIRECTION_LEFT -> VECTOR_LEFT + DIRECTION_RIGHT -> VECTOR_RIGHT + DIRECTION_UP -> VECTOR_UP + DIRECTION_DOWN -> VECTOR_DOWN + else -> VECTOR_ZERO + } + + fun fromVelocity(tracker: VelocityTracker): Vector { + tracker.computeCurrentVelocity(1000) + + val velocityX = tracker.xVelocity.toDouble() + val velocityY = tracker.yVelocity.toDouble() + + return Vector(velocityX, velocityY) + } + } +} diff --git a/src/web/constants.ts b/src/web/constants.ts index 51aba8a237..00c7413684 100644 --- a/src/web/constants.ts +++ b/src/web/constants.ts @@ -1,8 +1,2 @@ export const DEFAULT_TOUCH_SLOP = 15; - -export const Direction = { - RIGHT: 1, - LEFT: 2, - UP: 4, - DOWN: 8, -}; +export const MINIMAL_FLING_VELOCITY = 0.1; diff --git a/src/web/handlers/FlingGestureHandler.ts b/src/web/handlers/FlingGestureHandler.ts index 38a0a7aa38..850cb01346 100644 --- a/src/web/handlers/FlingGestureHandler.ts +++ b/src/web/handlers/FlingGestureHandler.ts @@ -1,25 +1,25 @@ import { State } from '../../State'; -import { Direction } from '../constants'; +import { Directions } from '../../Directions'; import { AdaptedEvent, Config } from '../interfaces'; import GestureHandler from './GestureHandler'; +import Vector from '../tools/Vector'; const DEFAULT_MAX_DURATION_MS = 800; -const DEFAULT_MIN_ACCEPTABLE_DELTA = 32; -const DEFAULT_DIRECTION = Direction.RIGHT; +const DEFAULT_MIN_VELOCITY = 700; +const DEFAULT_MIN_DIRECTION_ALIGNMENT = 0.75; +const DEFAULT_DIRECTION = Directions.RIGHT; const DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1; export default class FlingGestureHandler extends GestureHandler { private numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED; - private direction = DEFAULT_DIRECTION; + private direction: Directions = DEFAULT_DIRECTION; private maxDurationMs = DEFAULT_MAX_DURATION_MS; - private minAcceptableDelta = DEFAULT_MIN_ACCEPTABLE_DELTA; + private minVelocity = DEFAULT_MIN_VELOCITY; + private minDirectionalAlignment = DEFAULT_MIN_DIRECTION_ALIGNMENT; private delayTimeout!: number; - private startX = 0; - private startY = 0; - private maxNumberOfPointersSimultaneously = 0; private keyPointer = NaN; @@ -40,9 +40,6 @@ export default class FlingGestureHandler extends GestureHandler { } private startFling(): void { - this.startX = this.tracker.getLastX(this.keyPointer); - this.startY = this.tracker.getLastY(this.keyPointer); - this.begin(); this.maxNumberOfPointersSimultaneously = 1; @@ -51,21 +48,29 @@ export default class FlingGestureHandler extends GestureHandler { } private tryEndFling(): boolean { + const velocityVector = Vector.fromVelocity(this.tracker, this.keyPointer); + + const getAlignment = (direction: Directions) => { + return ( + direction & this.direction && + velocityVector.isSimilar( + Vector.fromDirection(direction), + this.minDirectionalAlignment + ) + ); + }; + + // list of alignments to all activated directions + const alignmentList = Object.values(Directions).map(getAlignment); + + const isAligned = alignmentList.some(Boolean); + const isFast = velocityVector.magnitude > this.minVelocity; + if ( this.maxNumberOfPointersSimultaneously === this.numberOfPointersRequired && - ((this.direction & Direction.RIGHT && - this.tracker.getLastX(this.keyPointer) - this.startX > - this.minAcceptableDelta) || - (this.direction & Direction.LEFT && - this.startX - this.tracker.getLastX(this.keyPointer) > - this.minAcceptableDelta) || - (this.direction & Direction.UP && - this.startY - this.tracker.getLastY(this.keyPointer) > - this.minAcceptableDelta) || - (this.direction & Direction.DOWN && - this.tracker.getLastY(this.keyPointer) - this.startY > - this.minAcceptableDelta)) + isAligned && + isFast ) { clearTimeout(this.delayTimeout); this.activate(); @@ -120,7 +125,7 @@ export default class FlingGestureHandler extends GestureHandler { } } - protected onPointerMove(event: AdaptedEvent): void { + private pointerMoveAction(event: AdaptedEvent): void { this.tracker.track(event); if (this.currentState !== State.BEGAN) { @@ -128,10 +133,18 @@ export default class FlingGestureHandler extends GestureHandler { } this.tryEndFling(); + } + protected onPointerMove(event: AdaptedEvent): void { + this.pointerMoveAction(event); super.onPointerMove(event); } + protected onPointerOutOfBounds(event: AdaptedEvent): void { + this.pointerMoveAction(event); + super.onPointerOutOfBounds(event); + } + protected onPointerUp(event: AdaptedEvent): void { super.onPointerUp(event); this.onUp(event); diff --git a/src/web/tools/Vector.ts b/src/web/tools/Vector.ts new file mode 100644 index 0000000000..782d73d214 --- /dev/null +++ b/src/web/tools/Vector.ts @@ -0,0 +1,52 @@ +import { Directions } from '../../Directions'; +import { MINIMAL_FLING_VELOCITY } from '../constants'; +import PointerTracker from './PointerTracker'; + +export default class Vector { + private readonly x; + private readonly y; + private readonly unitX; + private readonly unitY; + private readonly _magnitude; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + + this._magnitude = Math.hypot(this.x, this.y); + const isMagnitudeSufficient = this._magnitude > MINIMAL_FLING_VELOCITY; + + this.unitX = isMagnitudeSufficient ? this.x / this._magnitude : 0; + this.unitY = isMagnitudeSufficient ? this.y / this._magnitude : 0; + } + + static fromDirection(direction: Directions) { + return DirectionToVectorMappings.get(direction)!; + } + + static fromVelocity(tracker: PointerTracker, pointerId: number) { + return new Vector( + tracker.getVelocityX(pointerId), + tracker.getVelocityY(pointerId) + ); + } + + get magnitude() { + return this._magnitude; + } + + computeSimilarity(vector: Vector) { + return this.unitX * vector.unitX + this.unitY * vector.unitY; + } + + isSimilar(vector: Vector, threshold: number) { + return this.computeSimilarity(vector) > threshold; + } +} + +const DirectionToVectorMappings = new Map([ + [Directions.LEFT, new Vector(-1, 0)], + [Directions.RIGHT, new Vector(1, 0)], + [Directions.UP, new Vector(0, -1)], + [Directions.DOWN, new Vector(0, 1)], +]);