Skip to content

Commit 52c00e4

Browse files
authored
[macOS] Add FlingGestureHandler (#3028)
## Description This PR adds `FlingGestureHandler` on `macOS`. From now, all handlers, except`ForceTouchGestureHandler`, are available on this platform 🥳 ## Test plan Tested on newly added example.
1 parent fcf2655 commit 52c00e4

File tree

7 files changed

+369
-19
lines changed

7 files changed

+369
-19
lines changed

MacOSExample/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Tap from './basic/tap';
1616
import LongPressExample from './basic/longPress';
1717
import ManualExample from './basic/manual';
1818
import HoverExample from './basic/hover';
19+
import FlingExample from './basic/fling';
1920

2021
interface Example {
2122
name: string;
@@ -37,6 +38,7 @@ const EXAMPLES: ExamplesSection[] = [
3738
{ name: 'LongPress', component: LongPressExample },
3839
{ name: 'Manual', component: ManualExample },
3940
{ name: 'Hover', component: HoverExample },
41+
{ name: 'Fling', component: FlingExample },
4042
],
4143
},
4244
];
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { StyleSheet, View } from 'react-native';
2+
import {
3+
Directions,
4+
Gesture,
5+
GestureDetector,
6+
} from 'react-native-gesture-handler';
7+
import Animated, {
8+
interpolateColor,
9+
useAnimatedStyle,
10+
useSharedValue,
11+
withTiming,
12+
} from 'react-native-reanimated';
13+
14+
const AnimationDuration = 100;
15+
16+
const Colors = {
17+
Initial: '#0a2688',
18+
Active: '#6fcef5',
19+
};
20+
21+
export default function FlingExample() {
22+
const isActive = useSharedValue(false);
23+
const colorProgress = useSharedValue(0);
24+
const color1 = useSharedValue(Colors.Initial);
25+
const color2 = useSharedValue(Colors.Active);
26+
27+
const animatedStyles = useAnimatedStyle(() => {
28+
const backgroundColor = interpolateColor(
29+
colorProgress.value,
30+
[0, 1],
31+
[color1.value, color2.value]
32+
);
33+
34+
return {
35+
transform: [
36+
{
37+
scale: withTiming(isActive.value ? 1.2 : 1, {
38+
duration: AnimationDuration,
39+
}),
40+
},
41+
],
42+
backgroundColor,
43+
};
44+
});
45+
46+
const g = Gesture.Fling()
47+
.direction(Directions.LEFT | Directions.UP)
48+
.onBegin(() => {
49+
console.log('onBegin');
50+
})
51+
.onStart(() => {
52+
console.log('onStart');
53+
isActive.value = true;
54+
colorProgress.value = withTiming(1, {
55+
duration: AnimationDuration,
56+
});
57+
})
58+
.onEnd(() => console.log('onEnd'))
59+
.onFinalize((_, success) => {
60+
console.log('onFinalize', success);
61+
62+
isActive.value = false;
63+
64+
colorProgress.value = withTiming(0, {
65+
duration: AnimationDuration,
66+
});
67+
});
68+
69+
return (
70+
<View style={styles.container}>
71+
<GestureDetector gesture={g}>
72+
<Animated.View style={[styles.box, animatedStyles]} />
73+
</GestureDetector>
74+
</View>
75+
);
76+
}
77+
78+
const styles = StyleSheet.create({
79+
container: {
80+
flex: 1,
81+
justifyContent: 'space-around',
82+
alignItems: 'center',
83+
},
84+
85+
box: {
86+
width: 100,
87+
height: 100,
88+
borderRadius: 20,
89+
},
90+
});

apple/Handlers/RNFlingHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#import "../RNGHVector.h"
12
#import "RNGestureHandler.h"
23

34
@interface RNFlingGestureHandler : RNGestureHandler

apple/Handlers/RNFlingHandler.m

Lines changed: 153 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,154 @@ - (CGPoint)getLastLocation
8787

8888
@end
8989

90+
#else
91+
92+
@interface RNBetterSwipeGestureRecognizer : NSGestureRecognizer {
93+
dispatch_block_t failFlingAction;
94+
int maxDuration;
95+
int minVelocity;
96+
double defaultAlignmentCone;
97+
double axialDeviationCosine;
98+
double diagonalDeviationCosine;
99+
}
100+
101+
@property (atomic, assign) RNGestureHandlerDirection direction;
102+
@property (atomic, assign) int numberOfTouchesRequired;
103+
104+
- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler;
105+
106+
@end
107+
108+
@implementation RNBetterSwipeGestureRecognizer {
109+
__weak RNGestureHandler *_gestureHandler;
110+
111+
NSPoint startPosition;
112+
double startTime;
113+
}
114+
115+
- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler
116+
{
117+
if ((self = [super initWithTarget:self action:@selector(handleGesture:)])) {
118+
_gestureHandler = gestureHandler;
119+
120+
maxDuration = 1.0;
121+
minVelocity = 700;
122+
123+
defaultAlignmentCone = 30;
124+
axialDeviationCosine = [self coneToDeviation:defaultAlignmentCone];
125+
diagonalDeviationCosine = [self coneToDeviation:(90 - defaultAlignmentCone)];
126+
}
127+
return self;
128+
}
129+
130+
- (void)handleGesture:(NSPanGestureRecognizer *)gestureRecognizer
131+
{
132+
[_gestureHandler handleGesture:self];
133+
}
134+
135+
- (void)mouseDown:(NSEvent *)event
136+
{
137+
[super mouseDown:event];
138+
139+
startPosition = [self locationInView:self.view];
140+
startTime = CACurrentMediaTime();
141+
142+
self.state = NSGestureRecognizerStatePossible;
143+
144+
__weak typeof(self) weakSelf = self;
145+
146+
failFlingAction = dispatch_block_create(0, ^{
147+
__strong typeof(self) strongSelf = weakSelf;
148+
149+
if (strongSelf) {
150+
strongSelf.state = NSGestureRecognizerStateFailed;
151+
}
152+
});
153+
154+
dispatch_after(
155+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(maxDuration * NSEC_PER_SEC)),
156+
dispatch_get_main_queue(),
157+
failFlingAction);
158+
}
159+
160+
- (void)mouseDragged:(NSEvent *)event
161+
{
162+
[super mouseDragged:event];
163+
164+
NSPoint currentPosition = [self locationInView:self.view];
165+
double currentTime = CACurrentMediaTime();
166+
167+
NSPoint distance;
168+
distance.x = currentPosition.x - startPosition.x;
169+
distance.y = startPosition.y - currentPosition.y;
170+
171+
double timeDelta = currentTime - startTime;
172+
173+
Vector *velocityVector = [Vector fromVelocityX:(distance.x / timeDelta) withVelocityY:(distance.y / timeDelta)];
174+
175+
[self tryActivate:velocityVector];
176+
}
177+
178+
- (void)mouseUp:(NSEvent *)event
179+
{
180+
[super mouseUp:event];
181+
182+
dispatch_block_cancel(failFlingAction);
183+
184+
self.state =
185+
self.state == NSGestureRecognizerStateChanged ? NSGestureRecognizerStateEnded : NSGestureRecognizerStateFailed;
186+
}
187+
188+
- (void)tryActivate:(Vector *)velocityVector
189+
{
190+
bool isAligned = NO;
191+
192+
for (int i = 0; i < directionsSize; ++i) {
193+
if ([self getAlignment:axialDirections[i]
194+
withMinimalAlignmentCosine:axialDeviationCosine
195+
withVelocityVector:velocityVector]) {
196+
isAligned = YES;
197+
break;
198+
}
199+
}
200+
201+
if (!isAligned) {
202+
for (int i = 0; i < directionsSize; ++i) {
203+
if ([self getAlignment:diagonalDirections[i]
204+
withMinimalAlignmentCosine:diagonalDeviationCosine
205+
withVelocityVector:velocityVector]) {
206+
isAligned = YES;
207+
break;
208+
}
209+
}
210+
}
211+
212+
bool isFastEnough = velocityVector.magnitude >= minVelocity;
213+
214+
if (isAligned && isFastEnough) {
215+
self.state = NSGestureRecognizerStateChanged;
216+
}
217+
}
218+
219+
- (BOOL)getAlignment:(RNGestureHandlerDirection)direction
220+
withMinimalAlignmentCosine:(double)minimalAlignmentCosine
221+
withVelocityVector:(Vector *)velocityVector
222+
{
223+
Vector *directionVector = [Vector fromDirection:direction];
224+
return ((self.direction & direction) == direction) &&
225+
[velocityVector isSimilar:directionVector withThreshold:minimalAlignmentCosine];
226+
}
227+
228+
- (double)coneToDeviation:(double)degrees
229+
{
230+
double radians = (degrees * M_PI) / 180;
231+
return cos(radians / 2);
232+
}
233+
234+
@end
235+
236+
#endif
237+
90238
@implementation RNFlingGestureHandler
91239

92240
- (instancetype)initWithTag:(NSNumber *)tag
@@ -100,8 +248,8 @@ - (instancetype)initWithTag:(NSNumber *)tag
100248
- (void)resetConfig
101249
{
102250
[super resetConfig];
103-
UISwipeGestureRecognizer *recognizer = (UISwipeGestureRecognizer *)_recognizer;
104-
recognizer.direction = UISwipeGestureRecognizerDirectionRight;
251+
RNBetterSwipeGestureRecognizer *recognizer = (RNBetterSwipeGestureRecognizer *)_recognizer;
252+
recognizer.direction = RNGestureHandlerDirectionRight;
105253
#if !TARGET_OS_TV
106254
recognizer.numberOfTouchesRequired = 1;
107255
#endif
@@ -110,7 +258,7 @@ - (void)resetConfig
110258
- (void)configure:(NSDictionary *)config
111259
{
112260
[super configure:config];
113-
UISwipeGestureRecognizer *recognizer = (UISwipeGestureRecognizer *)_recognizer;
261+
RNBetterSwipeGestureRecognizer *recognizer = (RNBetterSwipeGestureRecognizer *)_recognizer;
114262

115263
id prop = config[@"direction"];
116264
if (prop != nil) {
@@ -125,6 +273,7 @@ - (void)configure:(NSDictionary *)config
125273
#endif
126274
}
127275

276+
#if !TARGET_OS_OSX
128277
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
129278
{
130279
RNGestureHandlerState savedState = _lastState;
@@ -154,21 +303,6 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(id)_recognizer
154303
withNumberOfTouches:recognizer.numberOfTouches
155304
withPointerType:_pointerType];
156305
}
157-
@end
158-
159-
#else
160-
161-
@implementation RNFlingGestureHandler
162-
163-
- (instancetype)initWithTag:(NSNumber *)tag
164-
{
165-
RCTLogWarn(@"FlingGestureHandler is not supported on macOS");
166-
if ((self = [super initWithTag:tag])) {
167-
_recognizer = [NSGestureRecognizer alloc];
168-
}
169-
return self;
170-
}
306+
#endif
171307

172308
@end
173-
174-
#endif

apple/RNGHVector.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// RNGHVector.h
3+
// Pods
4+
//
5+
// Created by Michał Bert on 05/08/2024.
6+
//
7+
8+
#import "RNGestureHandlerDirection.h"
9+
10+
#ifndef RNGHVector_h
11+
#define RNGHVector_h
12+
13+
@interface Vector : NSObject
14+
15+
@property (atomic, readonly, assign) double x;
16+
@property (atomic, readonly, assign) double y;
17+
@property (atomic, readonly, assign) double unitX;
18+
@property (atomic, readonly, assign) double unitY;
19+
@property (atomic, readonly, assign) double magnitude;
20+
21+
+ (Vector *_Nonnull)fromDirection:(RNGestureHandlerDirection)direction;
22+
+ (Vector *_Nonnull)fromVelocityX:(double)vx withVelocityY:(double)vy;
23+
- (nonnull instancetype)initWithX:(double)x withY:(double)y;
24+
- (double)computeSimilarity:(Vector *_Nonnull)other;
25+
- (BOOL)isSimilar:(Vector *_Nonnull)other withThreshold:(double)threshold;
26+
27+
@end
28+
29+
static double MINIMAL_RECOGNIZABLE_MAGNITUDE = 0.1;
30+
31+
#endif /* RNGHVector_h */

0 commit comments

Comments
 (0)