Skip to content

Commit 5002a54

Browse files
committed
Squashed 4 commits:
- Implement 'Active' selector - Implement 'Focus' selector - Change the interface - Implement 'Hover' selector
1 parent 430b7c2 commit 5002a54

33 files changed

Lines changed: 1750 additions & 13 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 PseudoActiveExample() {
6+
return (
7+
<View style={styles.container}>
8+
<Text style={styles.label}>Press and hold the box</Text>
9+
<Animated.View
10+
style={{
11+
...styles.box,
12+
transitionDuration: '150ms',
13+
opacity: { default: 1, ':active': 0.6 },
14+
transform: {
15+
default: [{ scale: 1 }, { rotate: '0deg' }],
16+
':active': [{ scale: 0.7 }, { rotate: '45deg' }],
17+
},
18+
backgroundColor: { default: '#4a90e2', ':active': 'pink' },
19+
}}
20+
/>
21+
<Text style={styles.description}>
22+
Style changes happen in C++ on UI thread.{'\n'}
23+
No JS frame should appear on the profiler.
24+
</Text>
25+
</View>
26+
);
27+
}
28+
29+
const styles = StyleSheet.create({
30+
container: {
31+
flex: 1,
32+
alignItems: 'center',
33+
justifyContent: 'center',
34+
gap: 24,
35+
padding: 20,
36+
},
37+
label: {
38+
fontSize: 16,
39+
fontWeight: '600',
40+
},
41+
box: {
42+
width: 150,
43+
height: 150,
44+
borderRadius: 16,
45+
},
46+
description: {
47+
textAlign: 'center',
48+
color: '#666',
49+
fontSize: 14,
50+
lineHeight: 22,
51+
},
52+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { StyleSheet, Text, TextInput, View } from 'react-native';
3+
import { createAnimatedComponent } from 'react-native-reanimated';
4+
5+
const AnimatedTextInput = createAnimatedComponent(TextInput);
6+
7+
export default function PseudoFocusExample() {
8+
return (
9+
<View style={styles.container}>
10+
<Text style={styles.label}>Tap a field to focus it</Text>
11+
<AnimatedTextInput
12+
style={{
13+
...styles.input,
14+
transitionDuration: '200ms',
15+
borderColor: { default: '#ccc', ':focus': '#c70ab7' },
16+
opacity: { default: 0.6, ':focus': 1 },
17+
borderWidth: { default: 2, ':focus': 4 },
18+
}}
19+
placeholder="Username"
20+
/>
21+
<AnimatedTextInput
22+
style={{
23+
...styles.input,
24+
transitionDuration: '200ms',
25+
borderColor: { default: '#ccc', ':focus': '#4a90e2' },
26+
opacity: { default: 0.6, ':focus': 1 },
27+
borderWidth: { default: 2, ':focus': 4 },
28+
}}
29+
placeholder="Password"
30+
secureTextEntry
31+
/>
32+
<Text style={styles.description}>
33+
Border color and opacity animate in C++ on UI thread.{'\n'}
34+
No JS frame should appear on the profiler.
35+
</Text>
36+
</View>
37+
);
38+
}
39+
40+
const styles = StyleSheet.create({
41+
container: {
42+
flex: 1,
43+
alignItems: 'center',
44+
justifyContent: 'center',
45+
gap: 16,
46+
padding: 20,
47+
},
48+
label: {
49+
fontSize: 16,
50+
fontWeight: '600',
51+
},
52+
input: {
53+
width: 280,
54+
height: 48,
55+
borderRadius: 10,
56+
paddingHorizontal: 14,
57+
fontSize: 16,
58+
},
59+
description: {
60+
textAlign: 'center',
61+
color: '#666',
62+
fontSize: 14,
63+
lineHeight: 22,
64+
marginTop: 8,
65+
},
66+
});
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/RuntimeTests/ReJest/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,7 @@ type Writable<T> = {
9292
-readonly [P in keyof T]: T[P];
9393
};
9494

95-
export type OperationUpdate = Writable<
96-
StyleProps | AnimatedStyle<Writable<Record<string, unknown>>> | Writable<Record<string, unknown>>
97-
>;
95+
export type OperationUpdate = Writable<StyleProps | Writable<Record<string, unknown>>>;
9896

9997
export interface Operation {
10098
tag?: number;

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,18 @@ const ProgressTransitionExample: React.FC = () =>
351351
React.createElement(
352352
require('./SharedElementTransitions/ProgressTransition').default
353353
);
354+
const PseudoActiveExample: React.FC = () =>
355+
React.createElement(
356+
require('./PseudoActiveExample').default
357+
);
358+
const PseudoFocusExample: React.FC = () =>
359+
React.createElement(
360+
require('./PseudoFocusExample').default
361+
);
362+
const PseudoHoverExample: React.FC = () =>
363+
React.createElement(
364+
require('./PseudoHoverExample').default
365+
);
354366
const RainbowExample: React.FC = () =>
355367
React.createElement(require('./RainbowExample').default as React.FC);
356368
const ReactionsCounterExample: React.FC = () =>
@@ -999,6 +1011,21 @@ export const EXAMPLES: Record<string, Example> = {
9991011
title: 'Shadow Nodes Cloning',
10001012
screen: ShadowNodesCloningExample,
10011013
},
1014+
PseudoActiveExample: {
1015+
icon: '👆',
1016+
title: 'Pseudo-selectors (:active)',
1017+
screen: PseudoActiveExample,
1018+
},
1019+
PseudoFocusExample: {
1020+
icon: '🔤',
1021+
title: 'Pseudo-selectors (:focus)',
1022+
screen: PseudoFocusExample,
1023+
},
1024+
PseudoHoverExample: {
1025+
icon: '🖱️',
1026+
title: 'Pseudo-selectors (:hover)',
1027+
screen: PseudoHoverExample,
1028+
},
10021029

10031030
// Old examples
10041031
AnimatedStyleUpdateExample: {

packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
#include <jsi/JSIDynamic.h>
12
#include <react/renderer/scheduler/Scheduler.h>
23
#include <react/renderer/uimanager/UIManagerBinding.h>
4+
#include <react/renderer/uimanager/primitives.h>
5+
#include <reanimated/CSS/configs/CSSTransitionConfig.h>
6+
#include <reanimated/CSS/easing/EasingFunctions.h>
37
#include <reanimated/Compat/WorkletsApi.h>
48
#include <reanimated/Events/UIEventHandler.h>
59
#include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h>
@@ -9,6 +13,7 @@
913
#include <reanimated/RuntimeDecorators/UIRuntimeDecorator.h>
1014
#include <reanimated/Tools/FeatureFlags.h>
1115
#include <reanimated/Tools/ReanimatedSystraceSection.h>
16+
#include <worklets/Tools/UIScheduler.h>
1217

1318
#ifdef __ANDROID__
1419
#include <fbjni/fbjni.h>
@@ -135,6 +140,9 @@ ReanimatedModuleProxy::ReanimatedModuleProxy(
135140
cssAnimationKeyframesRegistry_(std::make_shared<CSSKeyframesRegistry>(viewStylesRepository_)),
136141
cssAnimationsRegistry_(std::make_shared<CSSAnimationsRegistry>()),
137142
cssTransitionsRegistry_(std::make_shared<CSSTransitionsRegistry>(getAnimationTimestamp_, viewStylesRepository_)),
143+
pseudoStylesRegistry_(std::make_shared<PseudoStylesRegistry>(
144+
platformDepMethodsHolder.attachPseudoSelector,
145+
platformDepMethodsHolder.detachPseudoSelector)),
138146
synchronouslyUpdateUIPropsFunction_(platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction),
139147
#ifdef ANDROID
140148
filterUnmountedTagsFunction_(platformDepMethodsHolder.filterUnmountedTagsFunction),
@@ -145,7 +153,9 @@ ReanimatedModuleProxy::ReanimatedModuleProxy(
145153
// Add registries in order of their priority (from the lowest to the
146154
// highest)
147155
// CSS transitions should be overriden by animated style animations;
148-
// animated style animations should be overriden by CSS animations
156+
// animated style animations should be overriden by CSS animations.
157+
// Pseudo-selectors override CSS transitions but are overridden by
158+
// useAnimatedStyle and CSS animations.
149159
updatesRegistryManager_->addRegistry(cssTransitionsRegistry_);
150160
updatesRegistryManager_->addRegistry(animatedPropsRegistry_);
151161
updatesRegistryManager_->addRegistry(cssAnimationsRegistry_);
@@ -247,6 +257,50 @@ void ReanimatedModuleProxy::init(const PlatformDepMethodsHolder &platformDepMeth
247257
return strongThis->obtainProp(rt, shadowNodeWrapper, propName);
248258
};
249259

260+
pseudoStylesRegistry_->setOnSelectorStateChangedFn([weakThis = weak_from_this()](
261+
const std::shared_ptr<const ShadowNode> &shadowNode,
262+
const folly::dynamic &fromStyle,
263+
const folly::dynamic &toStyle,
264+
double duration,
265+
double delay) {
266+
auto strongThis = weakThis.lock();
267+
if (!strongThis) {
268+
return;
269+
}
270+
// Schedule on the UI worklet thread where jsi::Runtime is available.
271+
strongThis->uiScheduler_->scheduleOnUI([weakThis, shadowNode, fromStyle, toStyle, duration, delay]() {
272+
auto strongThis = weakThis.lock();
273+
if (!strongThis) {
274+
return;
275+
}
276+
auto &rt = getJSIRuntimeFromWorkletRuntime(strongThis->uiRuntime_);
277+
278+
CSSTransitionConfig config;
279+
const auto easingFn = getPredefinedEasingFunction("ease");
280+
281+
for (const auto &[propKey, toVal] : toStyle.items()) {
282+
const auto propName = propKey.asString();
283+
const folly::dynamic &fromVal = fromStyle.count(propName) ? fromStyle[propName] : toVal;
284+
285+
config.changedProperties.emplace(
286+
propName,
287+
CSSTransitionPropertySettings{
288+
std::make_pair(jsi::valueFromDynamic(rt, fromVal), jsi::valueFromDynamic(rt, toVal)),
289+
duration,
290+
easingFn,
291+
delay,
292+
false,
293+
});
294+
}
295+
296+
{
297+
auto lock = strongThis->cssTransitionsRegistry_->lock();
298+
strongThis->cssTransitionsRegistry_->run(rt, shadowNode, config);
299+
}
300+
strongThis->maybeRunCSSLoop();
301+
});
302+
});
303+
250304
jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
251305
UIRuntimeDecorator::decorate(
252306
uiRuntime,
@@ -546,6 +600,41 @@ jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) {
546600
return animatedPropsRegistry_->getUpdatesOlderThanTimestamp(rt, currentTimestamp - 1000); // 1 second
547601
}
548602

603+
void ReanimatedModuleProxy::registerPseudoStyle(
604+
jsi::Runtime &rt,
605+
const jsi::Value &shadowNodeWrapper,
606+
const jsi::Value &selector,
607+
const jsi::Value &selectorStyle,
608+
const jsi::Value &defaultStyle,
609+
const jsi::Value &transitionConfig) {
610+
const auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
611+
const auto tag = shadowNode->getTag();
612+
const auto selectorStr = stringFromValue(rt, selector);
613+
const auto selectorStyleDyn = jsi::dynamicFromValue(rt, selectorStyle);
614+
const auto defaultStyleDyn = jsi::dynamicFromValue(rt, defaultStyle);
615+
616+
double duration = 0;
617+
double delay = 0;
618+
if (transitionConfig.isObject()) {
619+
auto configObj = transitionConfig.asObject(rt);
620+
auto durationProp = configObj.getProperty(rt, "duration");
621+
if (durationProp.isNumber()) {
622+
duration = durationProp.asNumber();
623+
}
624+
auto delayProp = configObj.getProperty(rt, "delay");
625+
if (delayProp.isNumber()) {
626+
delay = delayProp.asNumber();
627+
}
628+
}
629+
630+
pseudoStylesRegistry_->registerPseudoStyle(
631+
tag, shadowNode, selectorStr, selectorStyleDyn, defaultStyleDyn, duration, delay);
632+
}
633+
634+
void ReanimatedModuleProxy::unregisterPseudoStyle(jsi::Runtime &, const jsi::Value &viewTag) {
635+
pseudoStylesRegistry_->remove(static_cast<Tag>(viewTag.asNumber()));
636+
}
637+
549638
bool ReanimatedModuleProxy::handleEvent(
550639
const std::string &eventName,
551640
const int emitterReactTag,

0 commit comments

Comments
 (0)