Skip to content

Commit cdcd931

Browse files
committed
Add hover selector
1 parent 72208ad commit cdcd931

5 files changed

Lines changed: 121 additions & 1 deletion

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import { StyleSheet, Text, View } from 'react-native';
3+
import Animated from 'react-native-reanimated';
4+
5+
export default function PseudoHoverExample() {
6+
return (
7+
<View style={styles.container}>
8+
<Text style={styles.label}>Hover over the boxes (mouse / trackpad)</Text>
9+
<View style={styles.row}>
10+
<Animated.View
11+
style={{
12+
...styles.box,
13+
backgroundColor: { default: '#4a90e2', ':hover': '#1a5fb4' },
14+
transform: {
15+
default: [{ scale: 1 }],
16+
':hover': [{ scale: 1.1 }],
17+
},
18+
transitionDuration: '150ms',
19+
}}
20+
/>
21+
<Animated.View
22+
style={{
23+
...styles.box,
24+
backgroundColor: { default: '#e74c3c', ':hover': '#922b21' },
25+
borderRadius: { default: 16, ':hover': 40 },
26+
transitionDuration: '200ms',
27+
}}
28+
/>
29+
<Animated.View
30+
style={{
31+
...styles.box,
32+
backgroundColor: { default: '#2ecc71', ':hover': '#1a7a43' },
33+
opacity: { default: 0.6, ':hover': 1 },
34+
transitionDuration: '120ms',
35+
}}
36+
/>
37+
</View>
38+
<Text style={styles.description}>
39+
Style changes happen in C++ on UI thread.{'\n'}
40+
No JS frame should appear on the profiler.
41+
</Text>
42+
</View>
43+
);
44+
}
45+
46+
const styles = StyleSheet.create({
47+
container: {
48+
flex: 1,
49+
alignItems: 'center',
50+
justifyContent: 'center',
51+
gap: 32,
52+
padding: 20,
53+
},
54+
label: {
55+
fontSize: 16,
56+
fontWeight: '600',
57+
textAlign: 'center',
58+
},
59+
row: {
60+
flexDirection: 'row',
61+
gap: 20,
62+
},
63+
box: {
64+
width: 90,
65+
height: 90,
66+
borderRadius: 16,
67+
},
68+
description: {
69+
textAlign: 'center',
70+
color: '#666',
71+
fontSize: 14,
72+
lineHeight: 22,
73+
},
74+
});

apps/common-app/src/apps/reanimated/examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AboutExample from './AboutExample';
22
import PseudoFocusExample from './PseudoFocusExample';
33
import PseudoActiveExample from './PseudoActiveExample';
4+
import PseudoHoverExample from './PseudoHoverExample';
45
import AmountExample from './AmountExample';
56
import AndroidDrawPassExample from './AndroidDrawPassExample';
67
import AnimatableRefExample from './AnimatableRefExample';
@@ -697,6 +698,11 @@ export const EXAMPLES: Record<string, Example> = {
697698
title: 'Pseudo-selectors (:focus)',
698699
screen: PseudoFocusExample,
699700
},
701+
PseudoHoverExample: {
702+
icon: '🖱️',
703+
title: 'Pseudo-selectors (:hover)',
704+
screen: PseudoHoverExample,
705+
},
700706

701707
// Old examples
702708
AnimatedStyleUpdateExample: {

packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ public void attachPseudoSelector(int tag, String selector, PseudoSelectorCallbac
136136
(v, hasFocus) -> callback.onSelectorStateChanged(hasFocus);
137137
view.setOnFocusChangeListener(focusListener);
138138
mPseudoSelectorDetachActions.put(tag, () -> view.setOnFocusChangeListener(null));
139+
} else if (":hover".equals(selector)) {
140+
View.OnHoverListener hoverListener =
141+
(v, event) -> {
142+
int action = event.getActionMasked();
143+
if (action == MotionEvent.ACTION_HOVER_ENTER) {
144+
callback.onSelectorStateChanged(true);
145+
} else if (action == MotionEvent.ACTION_HOVER_EXIT) {
146+
callback.onSelectorStateChanged(false);
147+
}
148+
return false;
149+
};
150+
view.setOnHoverListener(hoverListener);
151+
mPseudoSelectorDetachActions.put(tag, () -> view.setOnHoverListener(null));
139152
} else {
140153
View.OnTouchListener touchListener =
141154
(v, event) -> {

packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
1515
* Supported selectors:
1616
* ":active" — UILongPressGestureRecognizer with minimumPressDuration = 0
1717
* ":focus" — NSNotificationCenter UITextFieldTextDidBeginEditing/DidEndEditing
18-
* (":hover" — UIHoverGestureRecognizer, to be added later)
18+
* ":hover" — UIHoverGestureRecognizer (iOS 13+, pointer/trackpad/mouse devices)
1919
*
2020
* Lifetime: store as an associated object on the UIView so it is
2121
* automatically released when the view is deallocated.

packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.mm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ - (void)attachSelector:(NSString *)selectorName toView:(REAUIView *)view
3636
[view addGestureRecognizer:recognizer];
3737
_gestureRecognizer = recognizer;
3838
NSLog(@"[PseudoSelector] attached UILongPressGestureRecognizer (minimumPressDuration=0) to view: %@", view);
39+
} else if ([selectorName isEqualToString:@":hover"]) {
40+
if (@available(iOS 13.0, *)) {
41+
UIHoverGestureRecognizer *recognizer =
42+
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(handleHoverGesture:)];
43+
recognizer.delegate = self;
44+
[view addGestureRecognizer:recognizer];
45+
_gestureRecognizer = recognizer;
46+
NSLog(@"[PseudoSelector] attached UIHoverGestureRecognizer to view: %@", view);
47+
}
3948
} else if ([selectorName isEqualToString:@":focus"]) {
4049
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
4150
__weak REAUIView *weakView = view;
@@ -77,6 +86,24 @@ - (void)attachSelector:(NSString *)selectorName toView:(REAUIView *)view
7786
}
7887
}
7988

89+
- (void)handleHoverGesture:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0))
90+
{
91+
switch (recognizer.state) {
92+
case UIGestureRecognizerStateBegan:
93+
NSLog(@"[PseudoSelector] :hover → true");
94+
_callback(true);
95+
break;
96+
case UIGestureRecognizerStateEnded:
97+
case UIGestureRecognizerStateCancelled:
98+
case UIGestureRecognizerStateFailed:
99+
NSLog(@"[PseudoSelector] :hover → false");
100+
_callback(false);
101+
break;
102+
default:
103+
break;
104+
}
105+
}
106+
80107
- (void)handleActiveGesture:(UILongPressGestureRecognizer *)recognizer
81108
{
82109
NSLog(@"[PseudoSelector] handleActiveGesture state: %ld", (long)recognizer.state);

0 commit comments

Comments
 (0)