Skip to content

Commit e36d8d3

Browse files
authored
[apple] Handle hover in Touchable (#4283)
## Description Follow up for [Android PR](#4282) which brings hover animations to iOS, macOS and tvOS platforms. Of course docs/jsdoc will have to be unified between them, it will be done after merging either of these. <!-- Description and motivation for this PR. Include 'Fixes #<number>' if this is fixing some issue. --> ## Test plan <details> <summary>Tested on existing Touchable example and the code below: </summary> ```tsx import * as React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureHandlerRootView, Touchable as GHTouchable, } from 'react-native-gesture-handler'; const SLOW = 700; export default function App() { const [pressCount, setPressCount] = React.useState(0); const [disabled, setDisabled] = React.useState(false); // Auto-toggle `disabled` every 2s so hover mask/resume can be tested with a // mouse held still over the button — tapping a separate control would move // the pointer off first. Hover and watch: the visual masks to default while // disabled and resumes the hover look when re-enabled, pointer unmoved. React.useEffect(() => { const id = setInterval(() => setDisabled((d) => !d), 2000); return () => clearInterval(id); }, []); return ( <GestureHandlerRootView style={styles.root}> <Text style={styles.title}>Slow hover + press</Text> <Text style={styles.hint}> Use a mouse / stylus. Hover to grow & fade, press to shrink & fade more. Transitions should never flicker through the default state. </Text> <View style={styles.stage}> <GHTouchable // Press (active) visuals defaultOpacity={1} defaultScale={1} activeOpacity={0.3} activeScale={0.8} // Hover visuals hoverOpacity={0.6} hoverScale={1.2} // Underlay so the change is extra visible underlayColor="black" defaultUnderlayOpacity={0} hoverUnderlayOpacity={0.15} activeUnderlayOpacity={0.35} // Slow everything down: in/out for both tap and hover animationDuration={{ tap: { in: SLOW, out: SLOW }, hover: { in: SLOW, out: SLOW }, }} disabled={disabled} style={styles.button} onPress={() => setPressCount((c) => c + 1)}> <Text style={styles.buttonText}>Hover / Press me</Text> </GHTouchable> </View> <Text style={styles.counter}> {disabled ? 'DISABLED' : 'enabled'} · Presses: {pressCount} </Text> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#ecf0f1', alignItems: 'center', justifyContent: 'center', padding: 24, }, title: { fontSize: 22, fontWeight: 'bold', color: '#2c3e50', marginBottom: 8, }, hint: { fontSize: 14, color: '#7f8c8d', textAlign: 'center', marginBottom: 40, }, stage: { width: 260, height: 260, alignItems: 'center', justifyContent: 'center', }, button: { width: 180, height: 180, borderRadius: 24, backgroundColor: '#8e44ad', alignItems: 'center', justifyContent: 'center', }, buttonText: { color: 'white', fontSize: 18, fontWeight: 'bold', }, counter: { marginTop: 40, fontSize: 16, color: '#2c3e50', fontWeight: 'bold', }, }); ``` </details>
1 parent 52e082f commit e36d8d3

10 files changed

Lines changed: 358 additions & 131 deletions

File tree

packages/docs-gesture-handler/docs/components/touchable.mdx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,29 +237,23 @@ defaultUnderlayOpacity?: number;
237237

238238
Defines the initial opacity of underlay when the button is inactive. By default set to `0`.
239239

240-
<Badges platforms={['web', 'android']}>
241240
### hoverOpacity
242-
</Badges>
243241

244242
```ts
245243
hoverOpacity?: number;
246244
```
247245

248246
Defines the opacity of the whole component when the button is hovered. By default falls back to [`defaultOpacity`](#defaultopacity).
249247

250-
<Badges platforms={['web', 'android']}>
251248
### hoverScale
252-
</Badges>
253249

254250
```ts
255251
hoverScale?: number;
256252
```
257253

258254
Defines the scale of the whole component when the button is hovered. By default falls back to [`defaultScale`](#defaultscale).
259255

260-
<Badges platforms={['web', 'android']}>
261256
### hoverUnderlayOpacity
262-
</Badges>
263257

264258
```ts
265259
hoverUnderlayOpacity?: number;
@@ -296,7 +290,7 @@ Press and hover animation timing, in milliseconds. Defaults to 50ms for the in p
296290

297291
Each animation has two phases — `in` (running while the pointer engages the component) and `out` (running after the pointer releases) — across two categories:
298292
- `tap` — applies to presses.
299-
- `hover` — pointer hover (web and Android).
293+
- `hover` — pointer hover.
300294

301295
`longPress` is an optional override for the press-out timing once the press has been held past [`delayLongPress`](#delaylongpress). It only has an `out` field (the press-in is always the `tap` in duration). If omitted, the long-press release uses the resolved `tap` out duration.
302296

packages/react-native-gesture-handler/RNGestureHandler.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Pod::Spec.new do |s|
2020
s.source = { :git => "https://github.com/software-mansion/react-native-gesture-handler", :tag => "#{s.version}" }
2121
s.source_files = "apple/**/*.{h,m,mm}", "shared/**/*.{h,cpp}"
2222
s.requires_arc = true
23-
s.platforms = { ios: '11.0', tvos: '11.0', osx: '10.15', visionos: '1.0' }
23+
s.platforms = { ios: '15.1', tvos: '15.1', osx: '14.0', visionos: '1.0' }
2424
s.xcconfig = {
2525
"OTHER_CFLAGS" => "$(inherited) #{compilation_metadata_generation_flag} #{version_flag}"
2626
}

packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,7 @@ - (void)performFeedbackIfRequired
8989
{
9090
#if !TARGET_OS_TV && !TARGET_OS_VISION
9191
if (_feedbackOnActivation) {
92-
if (@available(iOS 10.0, *)) {
93-
[[[UIImpactFeedbackGenerator alloc] initWithStyle:(UIImpactFeedbackStyleMedium)] impactOccurred];
94-
}
92+
[[[UIImpactFeedbackGenerator alloc] initWithStyle:(UIImpactFeedbackStyleMedium)] impactOccurred];
9593
}
9694
#endif
9795
}

packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@
77

88
#import "RNGestureHandler.h"
99

10-
API_AVAILABLE(ios(13.4))
1110
@interface RNHoverGestureHandler : RNGestureHandler
1211
@end

packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,14 @@
1313
#import <React/RCTConvert.h>
1414
#import <UIKit/UIGestureRecognizerSubclass.h>
1515

16-
#define CHECK_TARGET(__VERSION__) \
17-
defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_##__VERSION__) && \
18-
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_##__VERSION__ && !TARGET_OS_TV
19-
2016
typedef NS_ENUM(NSInteger, RNGestureHandlerHoverEffect) {
2117
RNGestureHandlerHoverEffectNone = 0,
2218
RNGestureHandlerHoverEffectLift,
2319
RNGestureHandlerHoverEffectHightlight,
2420
};
2521

26-
#if CHECK_TARGET(13_4)
22+
#if !TARGET_OS_TV
2723

28-
API_AVAILABLE(ios(13.4))
2924
@interface RNBetterHoverGestureRecognizer : UIHoverGestureRecognizer <UIPointerInteractionDelegate>
3025

3126
- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler;
@@ -85,7 +80,7 @@ - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction style
8580
#endif
8681

8782
@implementation RNHoverGestureHandler {
88-
#if CHECK_TARGET(13_4)
83+
#if !TARGET_OS_TV
8984
UIPointerInteraction *_pointerInteraction;
9085
#endif
9186
}
@@ -97,67 +92,55 @@ - (instancetype)initWithTag:(NSNumber *)tag
9792
#endif
9893

9994
if ((self = [super initWithTag:tag])) {
100-
#if CHECK_TARGET(13_4)
101-
if (@available(iOS 13.4, *)) {
102-
_recognizer = [[RNBetterHoverGestureRecognizer alloc] initWithGestureHandler:self];
103-
_pointerInteraction =
104-
[[UIPointerInteraction alloc] initWithDelegate:(id<UIPointerInteractionDelegate>)_recognizer];
105-
}
95+
#if !TARGET_OS_TV
96+
_recognizer = [[RNBetterHoverGestureRecognizer alloc] initWithGestureHandler:self];
97+
_pointerInteraction = [[UIPointerInteraction alloc] initWithDelegate:(id<UIPointerInteractionDelegate>)_recognizer];
10698
#endif
10799
}
108100
return self;
109101
}
110102

111103
- (void)bindToView:(UIView *)view
112104
{
113-
#if CHECK_TARGET(13_4)
114-
if (@available(iOS 13.4, *)) {
115-
[super bindToView:view];
116-
[view addInteraction:_pointerInteraction];
117-
}
105+
#if !TARGET_OS_TV
106+
[super bindToView:view];
107+
[view addInteraction:_pointerInteraction];
118108
#endif
119109
}
120110

121111
- (void)unbindFromView
122112
{
123-
#if CHECK_TARGET(13_4)
124-
if (@available(iOS 13.4, *)) {
125-
// Remove the interaction before [super unbindFromView] detaches the recognizer and nils recognizer.view.
126-
[self.recognizer.view removeInteraction:_pointerInteraction];
127-
[super unbindFromView];
128-
}
113+
#if !TARGET_OS_TV
114+
[self.recognizer.view removeInteraction:_pointerInteraction];
115+
[super unbindFromView];
129116
#endif
130117
}
131118

132119
- (void)resetConfig
133120
{
134121
[super resetConfig];
135122

136-
#if CHECK_TARGET(13_4)
137-
if (@available(iOS 13.4, *)) {
138-
RNBetterHoverGestureRecognizer *recognizer = (RNBetterHoverGestureRecognizer *)_recognizer;
139-
recognizer.hoverEffect = RNGestureHandlerHoverEffectNone;
140-
}
123+
#if !TARGET_OS_TV
124+
RNBetterHoverGestureRecognizer *recognizer = (RNBetterHoverGestureRecognizer *)_recognizer;
125+
recognizer.hoverEffect = RNGestureHandlerHoverEffectNone;
141126
#endif
142127
}
143128

144129
- (void)updateConfig:(NSDictionary *)config
145130
{
146131
[super updateConfig:config];
147132

148-
#if CHECK_TARGET(13_4)
149-
if (@available(iOS 13.4, *)) {
150-
RNBetterHoverGestureRecognizer *recognizer = (RNBetterHoverGestureRecognizer *)_recognizer;
151-
APPLY_INT_PROP(hoverEffect);
152-
}
133+
#if !TARGET_OS_TV
134+
RNBetterHoverGestureRecognizer *recognizer = (RNBetterHoverGestureRecognizer *)_recognizer;
135+
APPLY_INT_PROP(hoverEffect);
153136
#endif
154137
}
155138

156139
- (void)setCurrentPointerType:(RNGestureHandlerPointerType)pointerType
157140
{
158141
_pointerType = pointerType;
159142

160-
#if CHECK_TARGET(16_1)
143+
#if !TARGET_OS_TV
161144
if (@available(iOS 16.1, *)) {
162145
if (((UIHoverGestureRecognizer *)self.recognizer).zOffset > 0.0) {
163146
_pointerType = RNGestureHandlerStylus;

packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,9 @@ - (void)resetConfig
380380
recognizer.activeOffsetYStart = NAN;
381381
recognizer.activeOffsetYEnd = NAN;
382382
recognizer.failOffsetYStart = NAN;
383-
recognizer.failOffsetYStart = NAN;
384383
recognizer.failOffsetYEnd = NAN;
385-
#if !TARGET_OS_OSX && !TARGET_OS_TV && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400
386-
if (@available(iOS 13.4, *)) {
387-
recognizer.allowedScrollTypesMask = 0;
388-
}
389-
#endif
390384
#if !TARGET_OS_OSX && !TARGET_OS_TV
385+
recognizer.allowedScrollTypesMask = 0;
391386
recognizer.minimumNumberOfTouches = 1;
392387
recognizer.maximumNumberOfTouches = NSUIntegerMax;
393388
#endif
@@ -412,12 +407,10 @@ - (void)updateConfig:(NSDictionary *)config
412407
APPLY_FLOAT_PROP(failOffsetYStart);
413408
APPLY_FLOAT_PROP(failOffsetYEnd);
414409

415-
#if !TARGET_OS_OSX && !TARGET_OS_TV && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400
416-
if (@available(iOS 13.4, *)) {
417-
bool enableTrackpadTwoFingerGesture = [RCTConvert BOOL:config[@"enableTrackpadTwoFingerGesture"]];
418-
if (enableTrackpadTwoFingerGesture) {
419-
recognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
420-
}
410+
#if !TARGET_OS_OSX && !TARGET_OS_TV
411+
bool enableTrackpadTwoFingerGesture = [RCTConvert BOOL:config[@"enableTrackpadTwoFingerGesture"]];
412+
if (enableTrackpadTwoFingerGesture) {
413+
recognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
421414
}
422415

423416
APPLY_NAMED_INT_PROP(minimumNumberOfTouches, @"minPointers");

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
@property (nonatomic, assign) CGFloat defaultScale;
3838
@property (nonatomic, assign) CGFloat defaultUnderlayOpacity;
3939
@property (nonatomic, assign) CGFloat activeUnderlayOpacity;
40+
@property (nonatomic, assign) NSInteger hoverAnimationInDuration;
41+
@property (nonatomic, assign) NSInteger hoverAnimationOutDuration;
42+
@property (nonatomic, assign) CGFloat hoverOpacity;
43+
@property (nonatomic, assign) CGFloat hoverScale;
44+
@property (nonatomic, assign) CGFloat hoverUnderlayOpacity;
4045
@property (nonatomic, strong, nullable) RNGHColor *underlayColor;
4146

4247
/**
@@ -54,6 +59,8 @@
5459
#if TARGET_OS_TV
5560
- (void)handleAnimatePressIn;
5661
- (void)handleAnimatePressOut;
62+
- (void)onHoverIn;
63+
- (void)onHoverOut;
5764
#endif // TARGET_OS_TV
5865

5966
/**

0 commit comments

Comments
 (0)