Skip to content

Commit

Permalink
Add useComposedEventHandler hook (#5890)
Browse files Browse the repository at this point in the history
## Summary

There have already been some requests for a way to merge our event
handlers. Inspired by
#5854, I
created a hook that can work with any handlers made using `useEvent`. I
will happily accept any comments and suggestions regarding typing since
I am not so sure about it.


https://github.com/software-mansion/react-native-reanimated/assets/77503811/9c295325-28ce-4ec2-8490-1a7431e8fafd

The PR can be merged only ater
#5845
(it is also based on it).

## Test plan

Open `useComposedEventHandler` example from Example app and watch event
callbacks in console.
  • Loading branch information
szydlovsky committed May 14, 2024
1 parent 22927c8 commit 88542d6
Show file tree
Hide file tree
Showing 12 changed files with 619 additions and 3 deletions.
92 changes: 92 additions & 0 deletions __typetests__/common/useComposedEventHandlerTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from 'react';
import Animated, {
useAnimatedScrollHandler,
useComposedEventHandler,
useEvent,
} from '../..';
import { Text } from 'react-native';
import type { ReanimatedEvent } from '../..';

function useComposedEventHandlerTest() {
function useComposedEventHandlerTestDifferentHandlers() {
function useCustomScrollHandler(
handler: (event: ReanimatedEvent<object>) => void
) {
return useEvent(
(event) => {
'worklet';
handler(event);
},
['onScroll']
);
}

const onScrollCustomHandler = useCustomScrollHandler((e) => {
'worklet';
console.log(`custom scroll handler invoked on ${e.eventName}`);
});

const onScrollHandler = useAnimatedScrollHandler((e) => {
console.log(`scroll handler invoked on ${e.eventName}`);
});

const composedHandler = useComposedEventHandler([
onScrollHandler,
onScrollCustomHandler,
]);

return (
<>
<Animated.ScrollView onScroll={composedHandler}>
{[...new Array(20)].map((_each, index) => (
<Text>{index}</Text>
))}
</Animated.ScrollView>
</>
);
}

function useComposedEventHandlerTestDifferentProps() {
const onDragBeginHandler = useAnimatedScrollHandler({
onBeginDrag(e) {
'worklet';
console.log('Drag begin');
},
});

const onDragEndHandler = useAnimatedScrollHandler({
onEndDrag(e) {
'worklet';
console.log('Drag end');
},
});

const composedHandler1 = useComposedEventHandler([
onDragBeginHandler,
onDragEndHandler,
]);

const composedHandler2 = useComposedEventHandler([
onDragBeginHandler,
onDragEndHandler,
]);

return (
<>
<Animated.ScrollView
// This will work well thanks to the way it is filtered in `PropsFilter`.
onMomentumScrollBegin={composedHandler1}
// same as above
onMomentumScrollEnd={composedHandler2}>
{[...new Array(20)].map((_each, index) => (
<Text>{index}</Text>
))}
</Animated.ScrollView>
</>
);
}
}
147 changes: 147 additions & 0 deletions app/src/examples/ComposedHandlerConditionalExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import Animated, {
useAnimatedScrollHandler,
useComposedEventHandler,
} from 'react-native-reanimated';

export default function ComposedHandlerConditionalExample() {
const [toggleFirst, setToggleFirst] = React.useState(true);
const [toggleSecond, setToggleSecond] = React.useState(true);
const [toggleThird, setToggleThird] = React.useState(true);

const handlerFunc = React.useCallback(
(handlerName: string, eventName: string) => {
'worklet';
console.log(`${handlerName} handler: ${eventName}`);
},
[]
);

const firstHandler = useAnimatedScrollHandler({
onScroll(e) {
handlerFunc('first', e.eventName);
},
});

const secondHandler = useAnimatedScrollHandler({
onScroll(e) {
handlerFunc('second', e.eventName);
},
});

const thirdHandler = useAnimatedScrollHandler({
onScroll(e) {
handlerFunc('third', e.eventName);
},
});

const composedHandler = useComposedEventHandler([
toggleFirst ? firstHandler : null,
toggleSecond ? secondHandler : null,
toggleThird ? thirdHandler : null,
]);

return (
<View style={styles.container}>
<Text style={styles.infoText}>Check console logs!</Text>
<ToggleButton
name={'first'}
isToggled={toggleFirst}
onPressFunc={() => setToggleFirst(!toggleFirst)}
/>
<ToggleButton
name={'second'}
isToggled={toggleSecond}
onPressFunc={() => setToggleSecond(!toggleSecond)}
/>
<ToggleButton
name={'third'}
isToggled={toggleThird}
onPressFunc={() => setToggleThird(!toggleThird)}
/>
<Animated.FlatList
onScroll={composedHandler}
style={styles.list}
data={items}
renderItem={({ item }) => <Item title={item.title} />}
keyExtractor={(item) => `A:${item.title}`}
/>
</View>
);
}

type ToggleProps = {
name: string;
isToggled: boolean;
onPressFunc: () => void;
};
const ToggleButton = ({ name, isToggled, onPressFunc }: ToggleProps) => (
<View style={styles.toggleContainer}>
<View
style={[
styles.toggleIcon,
isToggled ? styles.toggleON : styles.toggleOFF,
]}
/>
<Button
title={`Toggle ${name} handler ${isToggled ? 'OFF' : 'ON'}`}
onPress={onPressFunc}
/>
</View>
);

type ItemValue = { title: string };
const items: ItemValue[] = [...new Array(101)].map((_each, index) => {
return { title: `${index}` };
});

type ItemProps = { title: string };
const Item = ({ title }: ItemProps) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
},
infoText: {
fontSize: 19,
alignSelf: 'center',
},
list: {
flex: 1,
},
item: {
backgroundColor: '#883ef0',
alignItems: 'center',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 20,
},
title: {
fontSize: 32,
},
toggleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
toggleIcon: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 3,
borderColor: 'black',
},
toggleON: {
backgroundColor: '#7CFC00',
},
toggleOFF: {
backgroundColor: 'red',
},
});
96 changes: 96 additions & 0 deletions app/src/examples/ComposedHandlerDifferentEventsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import Animated, {
useAnimatedScrollHandler,
useComposedEventHandler,
} from 'react-native-reanimated';

export default function ComposedHandlerDifferentEventsExample() {
const handlerFunc = React.useCallback(
(handlerName: string, eventName: string) => {
'worklet';
console.log(`${handlerName} handler: ${eventName}`);
},
[]
);

const onScrollHandler = useAnimatedScrollHandler({
onScroll(e) {
handlerFunc('scroll', e.eventName);
},
});

const onDragHandler = useAnimatedScrollHandler({
onBeginDrag(e) {
handlerFunc('drag', e.eventName);
},
onEndDrag(e) {
handlerFunc('drag', e.eventName);
},
});

const onMomentumHandler = useAnimatedScrollHandler({
onMomentumBegin(e) {
handlerFunc('momentum', e.eventName);
},
onMomentumEnd(e) {
handlerFunc('momentum', e.eventName);
},
});

const composedHandler = useComposedEventHandler([
onScrollHandler,
onDragHandler,
onMomentumHandler,
]);

return (
<View style={styles.container}>
<Text style={styles.infoText}>Check console logs!</Text>
<Animated.FlatList
onScroll={composedHandler}
style={styles.list}
data={items}
renderItem={({ item }) => <Item title={item.title} />}
keyExtractor={(item) => `A:${item.title}`}
/>
</View>
);
}

type ItemValue = { title: string };
const items: ItemValue[] = [...new Array(101)].map((_each, index) => {
return { title: `${index}` };
});

type ItemProps = { title: string };
const Item = ({ title }: ItemProps) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
},
infoText: {
fontSize: 19,
alignSelf: 'center',
},
list: {
flex: 1,
},
item: {
backgroundColor: '#66e3c0',
alignItems: 'center',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 20,
},
title: {
fontSize: 32,
},
});
Loading

0 comments on commit 88542d6

Please sign in to comment.