Skip to content

Commit

Permalink
[macOS] Add FlingGestureHandler (#3028)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
m-bert authored Aug 23, 2024
1 parent fcf2655 commit 52c00e4
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 19 deletions.
2 changes: 2 additions & 0 deletions MacOSExample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Tap from './basic/tap';
import LongPressExample from './basic/longPress';
import ManualExample from './basic/manual';
import HoverExample from './basic/hover';
import FlingExample from './basic/fling';

interface Example {
name: string;
Expand All @@ -37,6 +38,7 @@ const EXAMPLES: ExamplesSection[] = [
{ name: 'LongPress', component: LongPressExample },
{ name: 'Manual', component: ManualExample },
{ name: 'Hover', component: HoverExample },
{ name: 'Fling', component: FlingExample },
],
},
];
Expand Down
90 changes: 90 additions & 0 deletions MacOSExample/src/basic/fling/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { StyleSheet, View } from 'react-native';
import {
Directions,
Gesture,
GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

const AnimationDuration = 100;

const Colors = {
Initial: '#0a2688',
Active: '#6fcef5',
};

export default function FlingExample() {
const isActive = useSharedValue(false);
const colorProgress = useSharedValue(0);
const color1 = useSharedValue(Colors.Initial);
const color2 = useSharedValue(Colors.Active);

const animatedStyles = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
colorProgress.value,
[0, 1],
[color1.value, color2.value]
);

return {
transform: [
{
scale: withTiming(isActive.value ? 1.2 : 1, {
duration: AnimationDuration,
}),
},
],
backgroundColor,
};
});

const g = Gesture.Fling()
.direction(Directions.LEFT | Directions.UP)
.onBegin(() => {
console.log('onBegin');
})
.onStart(() => {
console.log('onStart');
isActive.value = true;
colorProgress.value = withTiming(1, {
duration: AnimationDuration,
});
})
.onEnd(() => console.log('onEnd'))
.onFinalize((_, success) => {
console.log('onFinalize', success);

isActive.value = false;

colorProgress.value = withTiming(0, {
duration: AnimationDuration,
});
});

return (
<View style={styles.container}>
<GestureDetector gesture={g}>
<Animated.View style={[styles.box, animatedStyles]} />
</GestureDetector>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-around',
alignItems: 'center',
},

box: {
width: 100,
height: 100,
borderRadius: 20,
},
});
1 change: 1 addition & 0 deletions apple/Handlers/RNFlingHandler.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import "../RNGHVector.h"
#import "RNGestureHandler.h"

@interface RNFlingGestureHandler : RNGestureHandler
Expand Down
172 changes: 153 additions & 19 deletions apple/Handlers/RNFlingHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,154 @@ - (CGPoint)getLastLocation

@end

#else

@interface RNBetterSwipeGestureRecognizer : NSGestureRecognizer {
dispatch_block_t failFlingAction;
int maxDuration;
int minVelocity;
double defaultAlignmentCone;
double axialDeviationCosine;
double diagonalDeviationCosine;
}

@property (atomic, assign) RNGestureHandlerDirection direction;
@property (atomic, assign) int numberOfTouchesRequired;

- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler;

@end

@implementation RNBetterSwipeGestureRecognizer {
__weak RNGestureHandler *_gestureHandler;

NSPoint startPosition;
double startTime;
}

- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler
{
if ((self = [super initWithTarget:self action:@selector(handleGesture:)])) {
_gestureHandler = gestureHandler;

maxDuration = 1.0;
minVelocity = 700;

defaultAlignmentCone = 30;
axialDeviationCosine = [self coneToDeviation:defaultAlignmentCone];
diagonalDeviationCosine = [self coneToDeviation:(90 - defaultAlignmentCone)];
}
return self;
}

- (void)handleGesture:(NSPanGestureRecognizer *)gestureRecognizer
{
[_gestureHandler handleGesture:self];
}

- (void)mouseDown:(NSEvent *)event
{
[super mouseDown:event];

startPosition = [self locationInView:self.view];
startTime = CACurrentMediaTime();

self.state = NSGestureRecognizerStatePossible;

__weak typeof(self) weakSelf = self;

failFlingAction = dispatch_block_create(0, ^{
__strong typeof(self) strongSelf = weakSelf;

if (strongSelf) {
strongSelf.state = NSGestureRecognizerStateFailed;
}
});

dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(maxDuration * NSEC_PER_SEC)),
dispatch_get_main_queue(),
failFlingAction);
}

- (void)mouseDragged:(NSEvent *)event
{
[super mouseDragged:event];

NSPoint currentPosition = [self locationInView:self.view];
double currentTime = CACurrentMediaTime();

NSPoint distance;
distance.x = currentPosition.x - startPosition.x;
distance.y = startPosition.y - currentPosition.y;

double timeDelta = currentTime - startTime;

Vector *velocityVector = [Vector fromVelocityX:(distance.x / timeDelta) withVelocityY:(distance.y / timeDelta)];

[self tryActivate:velocityVector];
}

- (void)mouseUp:(NSEvent *)event
{
[super mouseUp:event];

dispatch_block_cancel(failFlingAction);

self.state =
self.state == NSGestureRecognizerStateChanged ? NSGestureRecognizerStateEnded : NSGestureRecognizerStateFailed;
}

- (void)tryActivate:(Vector *)velocityVector
{
bool isAligned = NO;

for (int i = 0; i < directionsSize; ++i) {
if ([self getAlignment:axialDirections[i]
withMinimalAlignmentCosine:axialDeviationCosine
withVelocityVector:velocityVector]) {
isAligned = YES;
break;
}
}

if (!isAligned) {
for (int i = 0; i < directionsSize; ++i) {
if ([self getAlignment:diagonalDirections[i]
withMinimalAlignmentCosine:diagonalDeviationCosine
withVelocityVector:velocityVector]) {
isAligned = YES;
break;
}
}
}

bool isFastEnough = velocityVector.magnitude >= minVelocity;

if (isAligned && isFastEnough) {
self.state = NSGestureRecognizerStateChanged;
}
}

- (BOOL)getAlignment:(RNGestureHandlerDirection)direction
withMinimalAlignmentCosine:(double)minimalAlignmentCosine
withVelocityVector:(Vector *)velocityVector
{
Vector *directionVector = [Vector fromDirection:direction];
return ((self.direction & direction) == direction) &&
[velocityVector isSimilar:directionVector withThreshold:minimalAlignmentCosine];
}

- (double)coneToDeviation:(double)degrees
{
double radians = (degrees * M_PI) / 180;
return cos(radians / 2);
}

@end

#endif

@implementation RNFlingGestureHandler

- (instancetype)initWithTag:(NSNumber *)tag
Expand All @@ -100,8 +248,8 @@ - (instancetype)initWithTag:(NSNumber *)tag
- (void)resetConfig
{
[super resetConfig];
UISwipeGestureRecognizer *recognizer = (UISwipeGestureRecognizer *)_recognizer;
recognizer.direction = UISwipeGestureRecognizerDirectionRight;
RNBetterSwipeGestureRecognizer *recognizer = (RNBetterSwipeGestureRecognizer *)_recognizer;
recognizer.direction = RNGestureHandlerDirectionRight;
#if !TARGET_OS_TV
recognizer.numberOfTouchesRequired = 1;
#endif
Expand All @@ -110,7 +258,7 @@ - (void)resetConfig
- (void)configure:(NSDictionary *)config
{
[super configure:config];
UISwipeGestureRecognizer *recognizer = (UISwipeGestureRecognizer *)_recognizer;
RNBetterSwipeGestureRecognizer *recognizer = (RNBetterSwipeGestureRecognizer *)_recognizer;

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

#if !TARGET_OS_OSX
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
RNGestureHandlerState savedState = _lastState;
Expand Down Expand Up @@ -154,21 +303,6 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(id)_recognizer
withNumberOfTouches:recognizer.numberOfTouches
withPointerType:_pointerType];
}
@end

#else

@implementation RNFlingGestureHandler

- (instancetype)initWithTag:(NSNumber *)tag
{
RCTLogWarn(@"FlingGestureHandler is not supported on macOS");
if ((self = [super initWithTag:tag])) {
_recognizer = [NSGestureRecognizer alloc];
}
return self;
}
#endif

@end

#endif
31 changes: 31 additions & 0 deletions apple/RNGHVector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// RNGHVector.h
// Pods
//
// Created by Michał Bert on 05/08/2024.
//

#import "RNGestureHandlerDirection.h"

#ifndef RNGHVector_h
#define RNGHVector_h

@interface Vector : NSObject

@property (atomic, readonly, assign) double x;
@property (atomic, readonly, assign) double y;
@property (atomic, readonly, assign) double unitX;
@property (atomic, readonly, assign) double unitY;
@property (atomic, readonly, assign) double magnitude;

+ (Vector *_Nonnull)fromDirection:(RNGestureHandlerDirection)direction;
+ (Vector *_Nonnull)fromVelocityX:(double)vx withVelocityY:(double)vy;
- (nonnull instancetype)initWithX:(double)x withY:(double)y;
- (double)computeSimilarity:(Vector *_Nonnull)other;
- (BOOL)isSimilar:(Vector *_Nonnull)other withThreshold:(double)threshold;

@end

static double MINIMAL_RECOGNIZABLE_MAGNITUDE = 0.1;

#endif /* RNGHVector_h */
Loading

0 comments on commit 52c00e4

Please sign in to comment.