diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/DiagonalDirections.kt b/android/src/main/java/com/swmansion/gesturehandler/core/DiagonalDirections.kt new file mode 100644 index 0000000000..4e1d3ec2d4 --- /dev/null +++ b/android/src/main/java/com/swmansion/gesturehandler/core/DiagonalDirections.kt @@ -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 +} 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 3f136555f0..e6e40c8c4b 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/FlingGestureHandler.kt @@ -11,7 +11,6 @@ class FlingGestureHandler : GestureHandler() { 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() } @@ -42,19 +41,27 @@ class FlingGestureHandler : GestureHandler() { 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 ( @@ -122,8 +129,13 @@ class FlingGestureHandler : GestureHandler() { 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) } } diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt index 5060f6b3db..46f509f039 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureUtils.kt @@ -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 { @@ -44,4 +45,6 @@ object GestureUtils { event.getY(lastPointerIdx) } } + fun coneToDeviation(angle: Double): Double = + cos(Math.toRadians(angle / 2.0)) } diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt b/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt index f21d992498..cdaea6a3cf 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/Vector.kt @@ -32,8 +32,14 @@ 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) { @@ -41,6 +47,10 @@ class Vector(val x: Double, val y: Double) { 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 } diff --git a/example/src/App.tsx b/example/src/App.tsx index 386f9e703d..255d5dc39e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -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'; @@ -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'; @@ -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 }, ], }, { @@ -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 }, diff --git a/example/src/basic/fling/index.tsx b/example/src/basic/fling/index.tsx new file mode 100644 index 0000000000..cc72438ac8 --- /dev/null +++ b/example/src/basic/fling/index.tsx @@ -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) + .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 ( + + + + + + ); +} + +const styles = StyleSheet.create({ + centerView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + box: { + height: 120, + width: 120, + backgroundColor: '#b58df1', + marginBottom: 30, + }, +}); diff --git a/example/src/release_tests/fling/index.tsx b/example/src/release_tests/nestedFling/index.tsx similarity index 97% rename from example/src/release_tests/fling/index.tsx rename to example/src/release_tests/nestedFling/index.tsx index 7ba0bc8a5a..17ecd45e7e 100644 --- a/example/src/release_tests/fling/index.tsx +++ b/example/src/release_tests/nestedFling/index.tsx @@ -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; private translateY: Animated.Value; @@ -88,7 +88,7 @@ export default class Example extends Component { render() { return ( - + Move up (with two fingers) or right/left (with one finger) and watch magic happens diff --git a/src/Directions.ts b/src/Directions.ts index 26631c64ae..afd0b707f0 100644 --- a/src/Directions.ts +++ b/src/Directions.ts @@ -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]; diff --git a/src/web/handlers/FlingGestureHandler.ts b/src/web/handlers/FlingGestureHandler.ts index 850cb01346..16b39428b3 100644 --- a/src/web/handlers/FlingGestureHandler.ts +++ b/src/web/handlers/FlingGestureHandler.ts @@ -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; @@ -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 ( diff --git a/src/web/tools/Vector.ts b/src/web/tools/Vector.ts index 782d73d214..cec7272e70 100644 --- a/src/web/tools/Vector.ts +++ b/src/web/tools/Vector.ts @@ -1,4 +1,4 @@ -import { Directions } from '../../Directions'; +import { DiagonalDirections, Directions } from '../../Directions'; import { MINIMAL_FLING_VELOCITY } from '../constants'; import PointerTracker from './PointerTracker'; @@ -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)!; } @@ -44,9 +44,17 @@ export default class Vector { } } -const DirectionToVectorMappings = new Map([ +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)], ]); diff --git a/src/web/utils.ts b/src/web/utils.ts index 88faf23db7..3a45e02013 100644 --- a/src/web/utils.ts +++ b/src/web/utils.ts @@ -15,3 +15,8 @@ export const PointerTypeMapping = new Map([ ['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));