react-native-reanimated
react-native-reanimated copied to clipboard
Better error when passing easing from 'react-native' instead of 'reanimated'
Summary
Function withTiming should throw an error when easing is not a worklet (or a bound function run from UI thread).
This is a common bug, because it happens when user is mixing imports from reanimated and animated.
Test plan
| Before | After |
|---|---|
| The actual names of all easing functions from react-native are all "fun" | |
timing animation (example from the table) - code
import Animated, {
useSharedValue,
withTiming,
useAnimatedStyle,
// Easing, <- this should be the correct import
} from 'react-native-reanimated';
import { View, Button, StyleSheet, Easing } from 'react-native';
import React from 'react';
export default function AnimatedStyleUpdateExample() {
const randomWidth = useSharedValue(10);
const style = useAnimatedStyle(() => {
return {
width: withTiming(randomWidth.value, {
easing: Easing.linear,
}),
};
});
return (
<View style={styles.container}>
<Animated.View style={[styles.box, style]} />
<Button
title="toggle"
onPress={() => {
randomWidth.value = Math.random() * 350;
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
},
box: {
width: 100,
height: 80,
backgroundColor: 'black',
margin: 30,
},
});
Keyframe animation - code
import Animated, { Keyframe } from 'react-native-reanimated';
import { Button, Easing, View, StyleSheet } from 'react-native';
import React, { useState } from 'react';
export default function KeyframeAnimation() {
const [show, setShow] = useState(false);
const enteringAnimation = new Keyframe({
from: {
originX: 50,
transform: [{ rotate: '45deg' }, { scale: 0.5 }],
},
30: {
transform: [{ rotate: '-90deg' }, { scale: 2 }],
},
50: {
originX: 70,
},
100: {
originX: 0,
transform: [{ rotate: '0deg' }, { scale: 1 }],
easing: Easing.quad,
},
})
.duration(2000)
.withCallback((finished: boolean) => {
'worklet';
if (finished) {
console.log('callback');
}
});
const exitingAnimation = new Keyframe({
0: {
opacity: 1,
originX: 0,
},
30: {
originX: -50,
easing: Easing.exp,
},
to: {
opacity: 0,
originX: 500,
},
}).duration(2000);
return (
<View style={styles.columnReverse}>
<Button
title="animate"
onPress={() => {
setShow((last) => !last);
}}
/>
<View style={styles.blueBoxContainer}>
{show && (
<Animated.View
entering={enteringAnimation}
exiting={exitingAnimation}
style={styles.blueBox}
/>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
columnReverse: { flexDirection: 'column-reverse' },
blueBoxContainer: {
height: 400,
alignItems: 'center',
justifyContent: 'center',
},
blueBox: {
height: 100,
width: 200,
backgroundColor: 'blue',
alignItems: 'center',
justifyContent: 'center',
},
});
Curved transition animation - code
import Animated, {
BounceOut,
CurvedTransition,
LightSpeedInRight,
} from 'react-native-reanimated';
import {
Image,
LayoutChangeEvent,
Text,
View,
StyleSheet,
Easing,
} from 'react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollView, TapGestureHandler } from 'react-native-gesture-handler';
const AnimatedImage = Animated.createAnimatedComponent(Image);
type Props = {
columns: number;
pokemons: number;
};
function getRandomColor() {
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
}
type PokemonData = {
ratio: number;
address: string;
key: number;
color: string;
};
export function WaterfallGrid({ columns = 3, pokemons = 100 }: Props) {
const [poks, setPoks] = useState<Array<PokemonData>>([]);
const [dims, setDims] = useState({ width: 0, height: 0 });
const handleOnLayout = useCallback(
(e: LayoutChangeEvent) => {
const newLayout = e.nativeEvent.layout;
if (
dims.width !== +newLayout.width ||
dims.height !== +newLayout.height
) {
setDims({ width: newLayout.width, height: newLayout.height });
}
},
[dims, setDims]
);
const margin = 10;
const width = (dims.width - (columns + 1) * margin) / columns;
useEffect(() => {
if (dims.width === 0 || dims.height === 0) {
return;
}
const poks: {
ratio: number;
address: string;
key: number;
color: string;
}[] = [];
for (let i = 0; i < pokemons; i++) {
const ratio = 1 + Math.random();
poks.push({
ratio,
address: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${i}.png`,
key: i,
color: `#${getRandomColor()}`,
});
}
setPoks(poks);
}, [dims, setPoks, pokemons]);
const layoutTransition = CurvedTransition.delay(1000).easingX(Easing.linear);
const [cardsMemo, height] = useMemo<[Array<JSX.Element>, number]>(() => {
if (poks.length === 0) {
return [[], 0];
}
const cardsResult: Array<JSX.Element> = [];
const heights = new Array(columns).fill(0);
for (const pok of poks) {
const cur = Math.floor(Math.random() * (columns - 0.01));
const pokHeight = width * pok.ratio;
heights[cur] += pokHeight + margin / 2;
cardsResult.push(
<Animated.View
entering={LightSpeedInRight.delay(cur * 200 * 2).springify()}
exiting={BounceOut}
layout={layoutTransition}
key={pok.address}
style={[
{
width: width,
height: pokHeight,
backgroundColor: pok.color,
left: cur * width + (cur + 1) * margin,
top: heights[cur] - pokHeight,
},
styles.pok,
]}>
<TapGestureHandler
onHandlerStateChange={() => {
setPoks(poks.filter((it) => it.key !== pok.key));
}}>
<AnimatedImage
layout={layoutTransition}
source={{ uri: pok.address }}
style={{ width: width, height: width }}
/>
</TapGestureHandler>
</Animated.View>
);
}
return [cardsResult, Math.max(...heights) + margin / 2];
}, [poks, columns, layoutTransition, width]);
return (
<View onLayout={handleOnLayout} style={styles.flexOne}>
{cardsMemo.length === 0 && <Text> Loading </Text>}
{cardsMemo.length !== 0 && (
<ScrollView>
<View style={{ height: height }}>{cardsMemo}</View>
</ScrollView>
)}
</View>
);
}
export default function WaterfallGridExample() {
return (
<View style={styles.flexOne}>
<WaterfallGrid columns={3} pokemons={10} />
</View>
);
}
const styles = StyleSheet.create({
flexOne: {
flex: 1,
},
pok: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
},
});
@Latropos Can you check if it works on Web?
@tjzel I've tested, it works on web