Skip to content

Commit 84ba3b2

Browse files
authored
Add corner area detection to Fling gesture. (#2807)
## Description This PR implements a requested feature to activate fling on corners of two adjacent activated directions. ## Test plan - In project root run `yarn` - Go to `example/` and run `yarn` - Paste Fling example from Gesture Handler docs into EmptyExample.tsx - Run `yarn web` for Web or `yarn start` for IOS and Android
1 parent 6d234d0 commit 84ba3b2

File tree

11 files changed

+166
-27
lines changed

11 files changed

+166
-27
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.swmansion.gesturehandler.core
2+
3+
object DiagonalDirections {
4+
const val DIRECTION_RIGHT_UP = GestureHandler.DIRECTION_RIGHT or GestureHandler.DIRECTION_UP
5+
const val DIRECTION_RIGHT_DOWN = GestureHandler.DIRECTION_RIGHT or GestureHandler.DIRECTION_DOWN
6+
const val DIRECTION_LEFT_UP = GestureHandler.DIRECTION_LEFT or GestureHandler.DIRECTION_UP
7+
const val DIRECTION_LEFT_DOWN = GestureHandler.DIRECTION_LEFT or GestureHandler.DIRECTION_DOWN
8+
}

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
1111

1212
private val maxDurationMs = DEFAULT_MAX_DURATION_MS
1313
private val minVelocity = DEFAULT_MIN_VELOCITY
14-
private val minDirectionalAlignment = DEFAULT_MIN_DIRECTION_ALIGNMENT
1514
private var handler: Handler? = null
1615
private var maxNumberOfPointersSimultaneously = 0
1716
private val failDelayed = Runnable { fail() }
@@ -42,19 +41,27 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
4241

4342
fun getVelocityAlignment(
4443
direction: Int,
44+
maxDeviationCosine: Double,
4545
): Boolean = (
46-
this.direction and direction != 0 &&
47-
velocityVector.isSimilar(Vector.fromDirection(direction), minDirectionalAlignment)
46+
(this.direction and direction) == direction &&
47+
velocityVector.isSimilar(Vector.fromDirection(direction), maxDeviationCosine)
4848
)
4949

50-
val alignmentList = arrayOf(
50+
val axialAlignmentsList = arrayOf(
5151
DIRECTION_LEFT,
5252
DIRECTION_RIGHT,
5353
DIRECTION_UP,
5454
DIRECTION_DOWN,
55-
).map { direction -> getVelocityAlignment(direction) }
55+
).map { direction -> getVelocityAlignment(direction, MAX_AXIAL_DEVIATION) }
5656

57-
val isAligned = alignmentList.any { it }
57+
val diagonalAlignmentsList = arrayOf(
58+
DiagonalDirections.DIRECTION_RIGHT_UP,
59+
DiagonalDirections.DIRECTION_RIGHT_DOWN,
60+
DiagonalDirections.DIRECTION_LEFT_UP,
61+
DiagonalDirections.DIRECTION_LEFT_DOWN,
62+
).map { direction -> getVelocityAlignment(direction, MAX_DIAGONAL_DEVIATION) }
63+
64+
val isAligned = axialAlignmentsList.any { it } or diagonalAlignmentsList.any { it }
5865
val isFast = velocityVector.magnitude > this.minVelocity
5966

6067
return if (
@@ -122,8 +129,13 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
122129
companion object {
123130
private const val DEFAULT_MAX_DURATION_MS: Long = 800
124131
private const val DEFAULT_MIN_VELOCITY: Long = 2000
125-
private const val DEFAULT_MIN_DIRECTION_ALIGNMENT: Double = 0.75
132+
private const val DEFAULT_ALIGNMENT_CONE: Double = 30.0
126133
private const val DEFAULT_DIRECTION = DIRECTION_RIGHT
127134
private const val DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1
135+
136+
private val MAX_AXIAL_DEVIATION: Double =
137+
GestureUtils.coneToDeviation(DEFAULT_ALIGNMENT_CONE)
138+
private val MAX_DIAGONAL_DEVIATION: Double =
139+
GestureUtils.coneToDeviation(90 - DEFAULT_ALIGNMENT_CONE)
128140
}
129141
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.swmansion.gesturehandler.core
22

33
import android.view.MotionEvent
4+
import kotlin.math.cos
45

56
object GestureUtils {
67
fun getLastPointerX(event: MotionEvent, averageTouches: Boolean): Float {
@@ -44,4 +45,6 @@ object GestureUtils {
4445
event.getY(lastPointerIdx)
4546
}
4647
}
48+
fun coneToDeviation(angle: Double): Double =
49+
cos(Math.toRadians(angle / 2.0))
4750
}

android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,25 @@ class Vector(val x: Double, val y: Double) {
3232
private val VECTOR_RIGHT: Vector = Vector(1.0, 0.0)
3333
private val VECTOR_UP: Vector = Vector(0.0, -1.0)
3434
private val VECTOR_DOWN: Vector = Vector(0.0, 1.0)
35+
36+
private val VECTOR_RIGHT_UP: Vector = Vector(1.0, -1.0)
37+
private val VECTOR_RIGHT_DOWN: Vector = Vector(1.0, 1.0)
38+
private val VECTOR_LEFT_UP: Vector = Vector(-1.0, -1.0)
39+
private val VECTOR_LEFT_DOWN: Vector = Vector(-1.0, 1.0)
40+
3541
private val VECTOR_ZERO: Vector = Vector(0.0, 0.0)
36-
const val MINIMAL_MAGNITUDE = 0.1
42+
private const val MINIMAL_MAGNITUDE = 0.1
3743

3844
fun fromDirection(direction: Int): Vector =
3945
when (direction) {
4046
DIRECTION_LEFT -> VECTOR_LEFT
4147
DIRECTION_RIGHT -> VECTOR_RIGHT
4248
DIRECTION_UP -> VECTOR_UP
4349
DIRECTION_DOWN -> VECTOR_DOWN
50+
DiagonalDirections.DIRECTION_RIGHT_UP -> VECTOR_RIGHT_UP
51+
DiagonalDirections.DIRECTION_RIGHT_DOWN -> VECTOR_RIGHT_DOWN
52+
DiagonalDirections.DIRECTION_LEFT_UP -> VECTOR_LEFT_UP
53+
DiagonalDirections.DIRECTION_LEFT_DOWN -> VECTOR_LEFT_DOWN
4454
else -> VECTOR_ZERO
4555
}
4656

example/src/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import DoubleDraggable from './release_tests/doubleDraggable';
1515
import { ComboWithGHScroll } from './release_tests/combo';
1616
import { TouchablesIndex, TouchableExample } from './release_tests/touchables';
1717
import Rows from './release_tests/rows';
18-
import Fling from './release_tests/fling';
18+
import NestedFling from './release_tests/nestedFling';
1919
import MouseButtons from './release_tests/mouseButtons';
2020
import ContextMenu from './release_tests/contextMenu';
2121
import NestedTouchables from './release_tests/nestedTouchables';
@@ -34,6 +34,7 @@ import PanResponder from './basic/panResponder';
3434
import HorizontalDrawer from './basic/horizontalDrawer';
3535
import PagerAndDrawer from './basic/pagerAndDrawer';
3636
import ForceTouch from './basic/forcetouch';
37+
import Fling from './basic/fling';
3738

3839
import ReanimatedSimple from './new_api/reanimated';
3940
import Camera from './new_api/camera';
@@ -77,6 +78,7 @@ const EXAMPLES: ExamplesSection[] = [
7778
{ name: 'Horizontal drawer', component: HorizontalDrawer },
7879
{ name: 'Pager & drawer', component: PagerAndDrawer },
7980
{ name: 'Force touch', component: ForceTouch },
81+
{ name: 'Fling', component: Fling },
8082
],
8183
},
8284
{
@@ -116,7 +118,7 @@ const EXAMPLES: ExamplesSection[] = [
116118
{ name: 'Double pinch & rotate', component: DoublePinchRotate },
117119
{ name: 'Double draggable', component: DoubleDraggable },
118120
{ name: 'Rows', component: Rows },
119-
{ name: 'Fling', component: Fling },
121+
{ name: 'Nested Fling', component: NestedFling },
120122
{ name: 'Combo', component: ComboWithGHScroll },
121123
{ name: 'Touchables', component: TouchablesIndex as React.ComponentType },
122124
{ name: 'MouseButtons', component: MouseButtons },

example/src/basic/fling/index.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { StyleSheet, View } from 'react-native';
3+
import {
4+
Directions,
5+
Gesture,
6+
GestureDetector,
7+
} from 'react-native-gesture-handler';
8+
import Animated, {
9+
useSharedValue,
10+
useAnimatedStyle,
11+
withTiming,
12+
Easing,
13+
} from 'react-native-reanimated';
14+
15+
export default function Example() {
16+
const position = useSharedValue(0);
17+
const beginPosition = useSharedValue(0);
18+
19+
const flingGesture = Gesture.Fling()
20+
.direction(Directions.LEFT | Directions.RIGHT)
21+
.onBegin((e) => {
22+
beginPosition.value = e.x;
23+
})
24+
.onStart((e) => {
25+
const direction = Math.sign(e.x - beginPosition.value);
26+
position.value = withTiming(position.value + direction * 50, {
27+
duration: 300,
28+
easing: Easing.bounce,
29+
});
30+
});
31+
32+
const animatedStyle = useAnimatedStyle(() => ({
33+
transform: [{ translateX: position.value }],
34+
}));
35+
36+
return (
37+
<View style={styles.centerView}>
38+
<GestureDetector gesture={flingGesture}>
39+
<Animated.View style={[styles.box, animatedStyle]} />
40+
</GestureDetector>
41+
</View>
42+
);
43+
}
44+
45+
const styles = StyleSheet.create({
46+
centerView: {
47+
flex: 1,
48+
justifyContent: 'center',
49+
alignItems: 'center',
50+
},
51+
box: {
52+
height: 120,
53+
width: 120,
54+
backgroundColor: '#b58df1',
55+
marginBottom: 30,
56+
},
57+
});

example/src/release_tests/fling/index.tsx renamed to example/src/release_tests/nestedFling/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { USE_NATIVE_DRIVER } from '../../config';
1212
const windowWidth = Dimensions.get('window').width;
1313
const circleRadius = 30;
1414

15-
class Fling extends Component {
15+
class NestedFling extends Component {
1616
private touchX: Animated.Value;
1717
private translateX: Animated.AnimatedAddition<number>;
1818
private translateY: Animated.Value;
@@ -88,7 +88,7 @@ export default class Example extends Component {
8888
render() {
8989
return (
9090
<View>
91-
<Fling />
91+
<NestedFling />
9292
<Text>
9393
Move up (with two fingers) or right/left (with one finger) and watch
9494
magic happens

src/Directions.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
const RIGHT = 1;
2+
const LEFT = 2;
3+
const UP = 4;
4+
const DOWN = 8;
5+
6+
// public interface
17
export const Directions = {
2-
RIGHT: 1,
3-
LEFT: 2,
4-
UP: 4,
5-
DOWN: 8,
8+
RIGHT: RIGHT,
9+
LEFT: LEFT,
10+
UP: UP,
11+
DOWN: DOWN,
12+
} as const;
13+
14+
// internal interface
15+
export const DiagonalDirections = {
16+
UP_RIGHT: UP | RIGHT,
17+
DOWN_RIGHT: DOWN | RIGHT,
18+
UP_LEFT: UP | LEFT,
19+
DOWN_LEFT: DOWN | LEFT,
620
} as const;
721

822
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
923
export type Directions = typeof Directions[keyof typeof Directions];
24+
// eslint-disable-next-line @typescript-eslint/no-redeclare
25+
export type DiagonalDirections =
26+
typeof DiagonalDirections[keyof typeof DiagonalDirections];

src/web/handlers/FlingGestureHandler.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import { State } from '../../State';
2-
import { Directions } from '../../Directions';
2+
import { DiagonalDirections, Directions } from '../../Directions';
33
import { AdaptedEvent, Config } from '../interfaces';
44

55
import GestureHandler from './GestureHandler';
66
import Vector from '../tools/Vector';
7+
import { coneToDeviation } from '../utils';
78

89
const DEFAULT_MAX_DURATION_MS = 800;
910
const DEFAULT_MIN_VELOCITY = 700;
10-
const DEFAULT_MIN_DIRECTION_ALIGNMENT = 0.75;
11+
const DEFAULT_ALIGNMENT_CONE = 30;
1112
const DEFAULT_DIRECTION = Directions.RIGHT;
1213
const DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1;
1314

15+
const AXIAL_DEVIATION_COSINE = coneToDeviation(DEFAULT_ALIGNMENT_CONE);
16+
const DIAGONAL_DEVIATION_COSINE = coneToDeviation(90 - DEFAULT_ALIGNMENT_CONE);
17+
1418
export default class FlingGestureHandler extends GestureHandler {
1519
private numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
1620
private direction: Directions = DEFAULT_DIRECTION;
1721

1822
private maxDurationMs = DEFAULT_MAX_DURATION_MS;
1923
private minVelocity = DEFAULT_MIN_VELOCITY;
20-
private minDirectionalAlignment = DEFAULT_MIN_DIRECTION_ALIGNMENT;
2124
private delayTimeout!: number;
2225

2326
private maxNumberOfPointersSimultaneously = 0;
@@ -50,20 +53,34 @@ export default class FlingGestureHandler extends GestureHandler {
5053
private tryEndFling(): boolean {
5154
const velocityVector = Vector.fromVelocity(this.tracker, this.keyPointer);
5255

53-
const getAlignment = (direction: Directions) => {
56+
const getAlignment = (
57+
direction: Directions | DiagonalDirections,
58+
minimalAlignmentCosine: number
59+
) => {
5460
return (
55-
direction & this.direction &&
61+
(direction & this.direction) === direction &&
5662
velocityVector.isSimilar(
5763
Vector.fromDirection(direction),
58-
this.minDirectionalAlignment
64+
minimalAlignmentCosine
5965
)
6066
);
6167
};
6268

69+
const axialDirectionsList = Object.values(Directions);
70+
const diagonalDirectionsList = Object.values(DiagonalDirections);
71+
6372
// list of alignments to all activated directions
64-
const alignmentList = Object.values(Directions).map(getAlignment);
73+
const axialAlignmentList = axialDirectionsList.map((direction) =>
74+
getAlignment(direction, AXIAL_DEVIATION_COSINE)
75+
);
76+
77+
const diagonalAlignmentList = diagonalDirectionsList.map((direction) =>
78+
getAlignment(direction, DIAGONAL_DEVIATION_COSINE)
79+
);
80+
81+
const isAligned =
82+
axialAlignmentList.some(Boolean) || diagonalAlignmentList.some(Boolean);
6583

66-
const isAligned = alignmentList.some(Boolean);
6784
const isFast = velocityVector.magnitude > this.minVelocity;
6885

6986
if (

src/web/tools/Vector.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Directions } from '../../Directions';
1+
import { DiagonalDirections, Directions } from '../../Directions';
22
import { MINIMAL_FLING_VELOCITY } from '../constants';
33
import PointerTracker from './PointerTracker';
44

@@ -20,7 +20,7 @@ export default class Vector {
2020
this.unitY = isMagnitudeSufficient ? this.y / this._magnitude : 0;
2121
}
2222

23-
static fromDirection(direction: Directions) {
23+
static fromDirection(direction: Directions | DiagonalDirections): Vector {
2424
return DirectionToVectorMappings.get(direction)!;
2525
}
2626

@@ -44,9 +44,17 @@ export default class Vector {
4444
}
4545
}
4646

47-
const DirectionToVectorMappings = new Map<Directions, Vector>([
47+
const DirectionToVectorMappings = new Map<
48+
Directions | DiagonalDirections,
49+
Vector
50+
>([
4851
[Directions.LEFT, new Vector(-1, 0)],
4952
[Directions.RIGHT, new Vector(1, 0)],
5053
[Directions.UP, new Vector(0, -1)],
5154
[Directions.DOWN, new Vector(0, 1)],
55+
56+
[DiagonalDirections.UP_RIGHT, new Vector(1, -1)],
57+
[DiagonalDirections.DOWN_RIGHT, new Vector(1, 1)],
58+
[DiagonalDirections.UP_LEFT, new Vector(-1, -1)],
59+
[DiagonalDirections.DOWN_LEFT, new Vector(-1, 1)],
5260
]);

0 commit comments

Comments
 (0)