Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make FlingHandler use velocity as the activation metric. #2796

Merged
merged 30 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
aaa4c14
fling now detects movement by velocity, first working version tested …
latekvo Mar 8, 2024
618e1e3
fix commit requirement error
latekvo Mar 8, 2024
38e736a
- applied fling changes from web to native android devices
latekvo Mar 8, 2024
a8f0ed1
add missing function,
latekvo Mar 8, 2024
17d7b22
Update android/src/main/java/com/swmansion/gesturehandler/core/FlingG…
latekvo Mar 11, 2024
fba0a25
Applied changes from review.
latekvo Mar 11, 2024
53ec2f3
resolved merge conflict
latekvo Mar 11, 2024
0e94b6f
Applied review suggestions.
latekvo Mar 11, 2024
adb8546
Fix pointer not being detected out of bounds on web.
latekvo Mar 11, 2024
555862e
Simplify code.
latekvo Mar 11, 2024
6284b70
Merge branch 'main' into @latekvo/fix_fling
latekvo Mar 11, 2024
5344a7a
Apply review suggestions.
latekvo Mar 11, 2024
92e7191
Optimization
latekvo Mar 12, 2024
dacb57f
Merge branch '@latekvo/fix_fling' of https://github.com/software-mans…
latekvo Mar 12, 2024
dd34b38
Apply changes to kotlin code
latekvo Mar 12, 2024
13138be
Move code into a separate Vector class. Decrease sensitivity.
latekvo Mar 12, 2024
63007f4
Apply changes from JS to KT: separate logic into a separate class
latekvo Mar 12, 2024
bf8693a
Minor cleanup.
latekvo Mar 12, 2024
774bdb1
- moved Vector class to separate file
latekvo Mar 13, 2024
d3cb42c
Applied cosmetic review suggestions.
latekvo Mar 13, 2024
534050e
Fling and Vector code cleanup.
latekvo Mar 13, 2024
19fc039
Merge branch 'main' into @latekvo/fix_fling
latekvo Mar 14, 2024
b4711c6
- Velocity tracker now tracks the entire user movement.
latekvo Mar 14, 2024
ec8d55f
Merge branch '@latekvo/fix_fling' of https://github.com/software-mans…
latekvo Mar 14, 2024
93a238d
Create alternative static constructors for Vector, simplified code, r…
latekvo Mar 14, 2024
4e2c455
Merge branch 'main' into @latekvo/fix_fling
latekvo Mar 15, 2024
1af78a7
Remove potential edge-case crash
latekvo Mar 15, 2024
6f70fed
Merge branch 'main' into @latekvo/fix_fling
latekvo Mar 15, 2024
b84adae
Make Vector members private readonly, make magnitude a getter of priv…
latekvo Mar 15, 2024
06f38f3
Merge branch '@latekvo/fix_fling' of https://github.com/software-mans…
latekvo Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlingGestureHandler>() {
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()
Expand All @@ -23,8 +24,7 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
}

private fun startFling(event: MotionEvent) {
startX = event.rawX
startY = event.rawY
velocityTracker = VelocityTracker.obtain()
begin()
maxNumberOfPointersSimultaneously = 1
if (handler == null) {
Expand All @@ -35,26 +35,40 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
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()
Expand Down Expand Up @@ -92,12 +106,22 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
}

override fun onReset() {
velocityTracker!!.recycle()
latekvo marked this conversation as resolved.
Show resolved Hide resolved
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
}
Expand Down
56 changes: 56 additions & 0 deletions android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
8 changes: 1 addition & 7 deletions src/web/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
61 changes: 37 additions & 24 deletions src/web/handlers/FlingGestureHandler.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -120,18 +125,26 @@ 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) {
return;
}

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);
Expand Down
48 changes: 48 additions & 0 deletions src/web/tools/Vector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Directions } from '../../Directions';
import { MINIMAL_FLING_VELOCITY } from '../constants';
import PointerTracker from './PointerTracker';

export default class Vector {
x = 0;
y = 0;
unitX = 0;
unitY = 0;
magnitude = 0;
m-bert marked this conversation as resolved.
Show resolved Hide resolved

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)
);
}

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, Vector>([
[Directions.LEFT, new Vector(-1, 0)],
[Directions.RIGHT, new Vector(1, 0)],
[Directions.UP, new Vector(0, -1)],
[Directions.DOWN, new Vector(0, 1)],
]);
Loading