Skip to content

Commit

Permalink
Add corner area detection to Fling gesture. (#2807)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
latekvo authored Mar 20, 2024
1 parent 6d234d0 commit 84ba3b2
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.swmansion.gesturehandler.core

object DiagonalDirections {
const val DIRECTION_RIGHT_UP = GestureHandler.DIRECTION_RIGHT or GestureHandler.DIRECTION_UP
const val DIRECTION_RIGHT_DOWN = GestureHandler.DIRECTION_RIGHT or GestureHandler.DIRECTION_DOWN
const val DIRECTION_LEFT_UP = GestureHandler.DIRECTION_LEFT or GestureHandler.DIRECTION_UP
const val DIRECTION_LEFT_DOWN = GestureHandler.DIRECTION_LEFT or GestureHandler.DIRECTION_DOWN
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {

private val maxDurationMs = DEFAULT_MAX_DURATION_MS
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() }
Expand Down Expand Up @@ -42,19 +41,27 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {

fun getVelocityAlignment(
direction: Int,
maxDeviationCosine: Double,
): Boolean = (
this.direction and direction != 0 &&
velocityVector.isSimilar(Vector.fromDirection(direction), minDirectionalAlignment)
(this.direction and direction) == direction &&
velocityVector.isSimilar(Vector.fromDirection(direction), maxDeviationCosine)
)

val alignmentList = arrayOf(
val axialAlignmentsList = arrayOf(
DIRECTION_LEFT,
DIRECTION_RIGHT,
DIRECTION_UP,
DIRECTION_DOWN,
).map { direction -> getVelocityAlignment(direction) }
).map { direction -> getVelocityAlignment(direction, MAX_AXIAL_DEVIATION) }

val isAligned = alignmentList.any { it }
val diagonalAlignmentsList = arrayOf(
DiagonalDirections.DIRECTION_RIGHT_UP,
DiagonalDirections.DIRECTION_RIGHT_DOWN,
DiagonalDirections.DIRECTION_LEFT_UP,
DiagonalDirections.DIRECTION_LEFT_DOWN,
).map { direction -> getVelocityAlignment(direction, MAX_DIAGONAL_DEVIATION) }

val isAligned = axialAlignmentsList.any { it } or diagonalAlignmentsList.any { it }
val isFast = velocityVector.magnitude > this.minVelocity

return if (
Expand Down Expand Up @@ -122,8 +129,13 @@ class FlingGestureHandler : GestureHandler<FlingGestureHandler>() {
companion object {
private const val DEFAULT_MAX_DURATION_MS: Long = 800
private const val DEFAULT_MIN_VELOCITY: Long = 2000
private const val DEFAULT_MIN_DIRECTION_ALIGNMENT: Double = 0.75
private const val DEFAULT_ALIGNMENT_CONE: Double = 30.0
private const val DEFAULT_DIRECTION = DIRECTION_RIGHT
private const val DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1

private val MAX_AXIAL_DEVIATION: Double =
GestureUtils.coneToDeviation(DEFAULT_ALIGNMENT_CONE)
private val MAX_DIAGONAL_DEVIATION: Double =
GestureUtils.coneToDeviation(90 - DEFAULT_ALIGNMENT_CONE)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swmansion.gesturehandler.core

import android.view.MotionEvent
import kotlin.math.cos

object GestureUtils {
fun getLastPointerX(event: MotionEvent, averageTouches: Boolean): Float {
Expand Down Expand Up @@ -44,4 +45,6 @@ object GestureUtils {
event.getY(lastPointerIdx)
}
}
fun coneToDeviation(angle: Double): Double =
cos(Math.toRadians(angle / 2.0))
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,25 @@ class Vector(val x: Double, val y: Double) {
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_RIGHT_UP: Vector = Vector(1.0, -1.0)
private val VECTOR_RIGHT_DOWN: Vector = Vector(1.0, 1.0)
private val VECTOR_LEFT_UP: Vector = Vector(-1.0, -1.0)
private val VECTOR_LEFT_DOWN: Vector = Vector(-1.0, 1.0)

private val VECTOR_ZERO: Vector = Vector(0.0, 0.0)
const val MINIMAL_MAGNITUDE = 0.1
private 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
DiagonalDirections.DIRECTION_RIGHT_UP -> VECTOR_RIGHT_UP
DiagonalDirections.DIRECTION_RIGHT_DOWN -> VECTOR_RIGHT_DOWN
DiagonalDirections.DIRECTION_LEFT_UP -> VECTOR_LEFT_UP
DiagonalDirections.DIRECTION_LEFT_DOWN -> VECTOR_LEFT_DOWN
else -> VECTOR_ZERO
}

Expand Down
6 changes: 4 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import DoubleDraggable from './release_tests/doubleDraggable';
import { ComboWithGHScroll } from './release_tests/combo';
import { TouchablesIndex, TouchableExample } from './release_tests/touchables';
import Rows from './release_tests/rows';
import Fling from './release_tests/fling';
import NestedFling from './release_tests/nestedFling';
import MouseButtons from './release_tests/mouseButtons';
import ContextMenu from './release_tests/contextMenu';
import NestedTouchables from './release_tests/nestedTouchables';
Expand All @@ -34,6 +34,7 @@ import PanResponder from './basic/panResponder';
import HorizontalDrawer from './basic/horizontalDrawer';
import PagerAndDrawer from './basic/pagerAndDrawer';
import ForceTouch from './basic/forcetouch';
import Fling from './basic/fling';

import ReanimatedSimple from './new_api/reanimated';
import Camera from './new_api/camera';
Expand Down Expand Up @@ -77,6 +78,7 @@ const EXAMPLES: ExamplesSection[] = [
{ name: 'Horizontal drawer', component: HorizontalDrawer },
{ name: 'Pager & drawer', component: PagerAndDrawer },
{ name: 'Force touch', component: ForceTouch },
{ name: 'Fling', component: Fling },
],
},
{
Expand Down Expand Up @@ -116,7 +118,7 @@ const EXAMPLES: ExamplesSection[] = [
{ name: 'Double pinch & rotate', component: DoublePinchRotate },
{ name: 'Double draggable', component: DoubleDraggable },
{ name: 'Rows', component: Rows },
{ name: 'Fling', component: Fling },
{ name: 'Nested Fling', component: NestedFling },
{ name: 'Combo', component: ComboWithGHScroll },
{ name: 'Touchables', component: TouchablesIndex as React.ComponentType },
{ name: 'MouseButtons', component: MouseButtons },
Expand Down
57 changes: 57 additions & 0 deletions example/src/basic/fling/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
Directions,
Gesture,
GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';

export default function Example() {
const position = useSharedValue(0);
const beginPosition = useSharedValue(0);

const flingGesture = Gesture.Fling()
.direction(Directions.LEFT | Directions.RIGHT)

Check warning on line 20 in example/src/basic/fling/index.tsx

View workflow job for this annotation

GitHub Actions / check (example)

Unexpected use of '|'
.onBegin((e) => {
beginPosition.value = e.x;
})
.onStart((e) => {
const direction = Math.sign(e.x - beginPosition.value);
position.value = withTiming(position.value + direction * 50, {
duration: 300,
easing: Easing.bounce,
});
});

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: position.value }],
}));

return (
<View style={styles.centerView}>
<GestureDetector gesture={flingGesture}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
</View>
);
}

const styles = StyleSheet.create({
centerView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
height: 120,
width: 120,
backgroundColor: '#b58df1',
marginBottom: 30,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { USE_NATIVE_DRIVER } from '../../config';
const windowWidth = Dimensions.get('window').width;
const circleRadius = 30;

class Fling extends Component {
class NestedFling extends Component {
private touchX: Animated.Value;
private translateX: Animated.AnimatedAddition<number>;
private translateY: Animated.Value;
Expand Down Expand Up @@ -88,7 +88,7 @@ export default class Example extends Component {
render() {
return (
<View>
<Fling />
<NestedFling />
<Text>
Move up (with two fingers) or right/left (with one finger) and watch
magic happens
Expand Down
25 changes: 21 additions & 4 deletions src/Directions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
const RIGHT = 1;
const LEFT = 2;
const UP = 4;
const DOWN = 8;

// public interface
export const Directions = {
RIGHT: 1,
LEFT: 2,
UP: 4,
DOWN: 8,
RIGHT: RIGHT,
LEFT: LEFT,
UP: UP,
DOWN: DOWN,
} as const;

// internal interface
export const DiagonalDirections = {
UP_RIGHT: UP | RIGHT,
DOWN_RIGHT: DOWN | RIGHT,
UP_LEFT: UP | LEFT,
DOWN_LEFT: DOWN | LEFT,
} as const;

// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
export type Directions = typeof Directions[keyof typeof Directions];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type DiagonalDirections =
typeof DiagonalDirections[keyof typeof DiagonalDirections];
33 changes: 25 additions & 8 deletions src/web/handlers/FlingGestureHandler.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { State } from '../../State';
import { Directions } from '../../Directions';
import { DiagonalDirections, Directions } from '../../Directions';
import { AdaptedEvent, Config } from '../interfaces';

import GestureHandler from './GestureHandler';
import Vector from '../tools/Vector';
import { coneToDeviation } from '../utils';

const DEFAULT_MAX_DURATION_MS = 800;
const DEFAULT_MIN_VELOCITY = 700;
const DEFAULT_MIN_DIRECTION_ALIGNMENT = 0.75;
const DEFAULT_ALIGNMENT_CONE = 30;
const DEFAULT_DIRECTION = Directions.RIGHT;
const DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1;

const AXIAL_DEVIATION_COSINE = coneToDeviation(DEFAULT_ALIGNMENT_CONE);
const DIAGONAL_DEVIATION_COSINE = coneToDeviation(90 - DEFAULT_ALIGNMENT_CONE);

export default class FlingGestureHandler extends GestureHandler {
private numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
private direction: Directions = DEFAULT_DIRECTION;

private maxDurationMs = DEFAULT_MAX_DURATION_MS;
private minVelocity = DEFAULT_MIN_VELOCITY;
private minDirectionalAlignment = DEFAULT_MIN_DIRECTION_ALIGNMENT;
private delayTimeout!: number;

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

const getAlignment = (direction: Directions) => {
const getAlignment = (
direction: Directions | DiagonalDirections,
minimalAlignmentCosine: number
) => {
return (
direction & this.direction &&
(direction & this.direction) === direction &&
velocityVector.isSimilar(
Vector.fromDirection(direction),
this.minDirectionalAlignment
minimalAlignmentCosine
)
);
};

const axialDirectionsList = Object.values(Directions);
const diagonalDirectionsList = Object.values(DiagonalDirections);

// list of alignments to all activated directions
const alignmentList = Object.values(Directions).map(getAlignment);
const axialAlignmentList = axialDirectionsList.map((direction) =>
getAlignment(direction, AXIAL_DEVIATION_COSINE)
);

const diagonalAlignmentList = diagonalDirectionsList.map((direction) =>
getAlignment(direction, DIAGONAL_DEVIATION_COSINE)
);

const isAligned =
axialAlignmentList.some(Boolean) || diagonalAlignmentList.some(Boolean);

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

if (
Expand Down
14 changes: 11 additions & 3 deletions src/web/tools/Vector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Directions } from '../../Directions';
import { DiagonalDirections, Directions } from '../../Directions';
import { MINIMAL_FLING_VELOCITY } from '../constants';
import PointerTracker from './PointerTracker';

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

static fromDirection(direction: Directions) {
static fromDirection(direction: Directions | DiagonalDirections): Vector {
return DirectionToVectorMappings.get(direction)!;
}

Expand All @@ -44,9 +44,17 @@ export default class Vector {
}
}

const DirectionToVectorMappings = new Map<Directions, Vector>([
const DirectionToVectorMappings = new Map<
Directions | DiagonalDirections,
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)],

[DiagonalDirections.UP_RIGHT, new Vector(1, -1)],
[DiagonalDirections.DOWN_RIGHT, new Vector(1, 1)],
[DiagonalDirections.UP_LEFT, new Vector(-1, -1)],
[DiagonalDirections.DOWN_LEFT, new Vector(-1, 1)],
]);
5 changes: 5 additions & 0 deletions src/web/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ export const PointerTypeMapping = new Map<string, PointerType>([
['pen', PointerType.STYLUS],
['none', PointerType.OTHER],
]);

export const degToRad = (degrees: number) => (degrees * Math.PI) / 180;

export const coneToDeviation = (degrees: number) =>
Math.cos(degToRad(degrees / 2));

0 comments on commit 84ba3b2

Please sign in to comment.