Skip to content

Commit

Permalink
[iOS] Stylus support (#3113)
Browse files Browse the repository at this point in the history
## Description

This PR adds stylus support on `iOS`.

>[!WARNING]
> This PR lacks support for `Hover` since currently we are unable to test that.

>[!NOTE]
> You can read more about this feature in #3107

## Test plan

Tested on **_StylusData_** example
  • Loading branch information
m-bert authored Sep 24, 2024
1 parent efd5da9 commit 744d921
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 11 deletions.
64 changes: 57 additions & 7 deletions apple/Handlers/RNPanHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "RNPanHandler.h"
#import "RNGHStylusData.h"

#if TARGET_OS_OSX

Expand All @@ -31,6 +32,10 @@ @interface RNBetterPanGestureRecognizer : UIPanGestureRecognizer
@property (nonatomic) CGFloat failOffsetYEnd;
@property (nonatomic) CGFloat activateAfterLongPress;

#if !TARGET_OS_OSX && !TARGET_OS_TV
@property (atomic, readonly, strong) RNGHStylusData *stylusData;
#endif

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

@end
Expand Down Expand Up @@ -80,6 +85,28 @@ - (void)setMinimumNumberOfTouches:(NSUInteger)minimumNumberOfTouches
}
#endif

#if !TARGET_OS_OSX && !TARGET_OS_TV
- (void)tryUpdateStylusData:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];

if (touch.type != UITouchTypePencil) {
return;
} else if (_stylusData == nil) {
_stylusData = [[RNGHStylusData alloc] init];
}

_stylusData.altitudeAngle = touch.altitudeAngle;
_stylusData.azimuthAngle = [touch azimuthAngleInView:nil];
_stylusData.pressure = touch.force / touch.maximumPossibleForce;

CGPoint tilts = ghSpherical2tilt(_stylusData.altitudeAngle, _stylusData.azimuthAngle);

_stylusData.tiltX = tilts.x;
_stylusData.tiltY = tilts.y;
}
#endif

- (void)activateAfterLongPress
{
self.state = UIGestureRecognizerStateBegan;
Expand All @@ -102,6 +129,8 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event
} else {
super.minimumNumberOfTouches = _realMinimumNumberOfTouches;
}

[self tryUpdateStylusData:event];
#endif

#if TARGET_OS_OSX
Expand Down Expand Up @@ -150,17 +179,28 @@ - (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event
[self setTranslation:CGPointMake(0, 0) inView:self.view];
}
}

[self tryUpdateStylusData:event];
#endif
}

- (void)interactionsEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];

#if !TARGET_OS_TV && !TARGET_OS_OSX
[self tryUpdateStylusData:event];
#endif
}

- (void)interactionsCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];

#if !TARGET_OS_TV && !TARGET_OS_OSX
[self tryUpdateStylusData:event];
#endif

[self reset];
}

Expand Down Expand Up @@ -224,6 +264,10 @@ - (void)reset
self.enabled = YES;
[super reset];
[_gestureHandler reset];

#if !TARGET_OS_TV && !TARGET_OS_OSX
_stylusData = nil;
#endif
}

- (void)updateHasCustomActivationCriteria
Expand Down Expand Up @@ -405,17 +449,23 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(NSPanGestureRecognizer *)rec
withTranslation:[recognizer translationInView:recognizer.view.window.contentView]
withVelocity:[recognizer velocityInView:recognizer.view.window.contentView]
withNumberOfTouches:1
withPointerType:RNGestureHandlerMouse];
withPointerType:RNGestureHandlerMouse
withStylusData:nil];
}
#else
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)recognizer
{
return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:recognizer.view]
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
withTranslation:[recognizer translationInView:recognizer.view.window]
withVelocity:[recognizer velocityInView:recognizer.view.window]
withNumberOfTouches:recognizer.numberOfTouches
withPointerType:_pointerType];
RNBetterPanGestureRecognizer *panRecognizer = (RNBetterPanGestureRecognizer *)recognizer;

return [RNGestureHandlerEventExtraData
forPan:[recognizer locationInView:recognizer.view]
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
withTranslation:[recognizer translationInView:recognizer.view.window]
withVelocity:[recognizer velocityInView:recognizer.view.window]
withNumberOfTouches:recognizer.numberOfTouches
withPointerType:_pointerType
withStylusData:[panRecognizer.stylusData toDictionary]]; // In Objective-C calling method on nil returns
// nil, therefore this line does not crash.
}
#endif

Expand Down
77 changes: 77 additions & 0 deletions apple/RNGHStylusData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// RNGHStylusData.h
// Pods
//
// Created by Michał Bert on 18/09/2024.
//

#ifndef RNGHStylusData_h
#define RNGHStylusData_h

@interface RNGHStylusData : NSObject

@property (atomic, assign) double tiltX;
@property (atomic, assign) double tiltY;
@property (atomic, assign) double altitudeAngle;
@property (atomic, assign) double azimuthAngle;
@property (atomic, assign) double pressure;

- (NSDictionary *)toDictionary;

@end

static CGPoint ghSpherical2tilt(double altitudeAngle, double azimuthAngle)
{
CGPoint tilts = {.x = 0.0, .y = 0.0};

const double radToDeg = 180 / M_PI;
const double eps = 0.000000001;

if (altitudeAngle < eps) {
// the pen is in the X-Y plane
if (azimuthAngle < eps || fabs(azimuthAngle - 2 * M_PI) < eps) {
// pen is on positive X axis
tilts.x = M_PI_2;
}
if (fabs(azimuthAngle - M_PI_2) < eps) {
// pen is on positive Y axis
tilts.y = M_PI_2;
}
if (fabs(azimuthAngle - M_PI) < eps) {
// pen is on negative X axis
tilts.x = -M_PI_2;
}
if (fabs(azimuthAngle - 3 * M_PI_2) < eps) {
// pen is on negative Y axis
tilts.y = -M_PI_2;
}
if (azimuthAngle > eps && fabs(azimuthAngle - M_PI_2) < eps) {
tilts.x = M_PI_2;
tilts.y = M_PI_2;
}
if (fabs(azimuthAngle - M_PI_2) > eps && fabs(azimuthAngle - M_PI) < eps) {
tilts.x = -M_PI_2;
tilts.y = M_PI_2;
}
if (azimuthAngle - M_PI > eps && fabs(azimuthAngle - 3 * M_PI_2) < eps) {
tilts.x = -M_PI_2;
tilts.y = -M_PI_2;
}
if (fabs(azimuthAngle - 3 * M_PI_2) > eps && fabs(azimuthAngle - 2 * M_PI) < eps) {
tilts.x = M_PI_2;
tilts.y = -M_PI_2;
}
} else {
const double tanAlt = tan(altitudeAngle);

tilts.x = atan(cos(azimuthAngle) / tanAlt);
tilts.y = atan(sin(azimuthAngle) / tanAlt);
}

tilts.x = round(tilts.x * radToDeg);
tilts.y = round(tilts.y * radToDeg);

return tilts;
}

#endif /* RNGHStylusData_h */
37 changes: 37 additions & 0 deletions apple/RNGHStylusData.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// RNGHStylusData.m
// DoubleConversion
//
// Created by Michał Bert on 18/09/2024.
//

#import "RNGHStylusData.h"
#import <Foundation/Foundation.h>

@implementation RNGHStylusData

- (instancetype)init
{
if (self = [super init]) {
self.tiltX = 0;
self.tiltY = 0;
self.altitudeAngle = M_PI_2;
self.azimuthAngle = 0;
self.pressure = 0;
}

return self;
}

- (NSDictionary *)toDictionary
{
return @{
@"tiltX" : @(_tiltX),
@"tiltY" : @(_tiltY),
@"altitudeAngle" : @(_altitudeAngle),
@"azimuthAngle" : @(_azimuthAngle),
@"pressure" : @(_pressure),
};
}

@end
4 changes: 3 additions & 1 deletion apple/RNGestureHandlerEvents.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#import <Foundation/Foundation.h>

#import "RNGHStylusData.h"
#import "RNGHTouchEventType.h"
#import "RNGHUIKit.h"
#import "RNGestureHandlerState.h"
Expand Down Expand Up @@ -29,7 +30,8 @@
withTranslation:(CGPoint)translation
withVelocity:(CGPoint)velocity
withNumberOfTouches:(NSUInteger)numberOfTouches
withPointerType:(NSInteger)pointerType;
withPointerType:(NSInteger)pointerType
withStylusData:(NSDictionary *)stylusData;
+ (RNGestureHandlerEventExtraData *)forForce:(CGFloat)force
forPosition:(CGPoint)position
withAbsolutePosition:(CGPoint)absolutePosition
Expand Down
14 changes: 11 additions & 3 deletions apple/RNGestureHandlerEvents.m
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ + (RNGestureHandlerEventExtraData *)forPan:(CGPoint)position
withVelocity:(CGPoint)velocity
withNumberOfTouches:(NSUInteger)numberOfTouches
withPointerType:(NSInteger)pointerType
withStylusData:(NSDictionary *)stylusData
{
return [[RNGestureHandlerEventExtraData alloc] initWithData:@{
NSMutableDictionary *data = [@{
@"x" : @(position.x),
@"y" : @(position.y),
@"absoluteX" : @(absolutePosition.x),
Expand All @@ -74,8 +75,15 @@ + (RNGestureHandlerEventExtraData *)forPan:(CGPoint)position
@"velocityX" : SAFE_VELOCITY(velocity.x),
@"velocityY" : SAFE_VELOCITY(velocity.y),
@"numberOfPointers" : @(numberOfTouches),
@"pointerType" : @(pointerType)
}];
@"pointerType" : @(pointerType),
} mutableCopy];

// Add the stylusData to the dictionary only if necessary
if (stylusData != nil) {
data[@"stylusData"] = stylusData;
}

return [[RNGestureHandlerEventExtraData alloc] initWithData:data];
}

+ (RNGestureHandlerEventExtraData *)forForce:(CGFloat)force
Expand Down

0 comments on commit 744d921

Please sign in to comment.