* Copyright (c) 2024 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree.
*/
import React, { useRef, useState } from 'react';
import { Animated, Pressable, Text, View } from 'react-native';
import { TestCase, TestSuite } from '@rnoh/testerino';
import { Button } from '../components';
export function AnimatedTest() {
return (
<TestSuite name="Animated">
<TestCase itShould="update the button label after a delay">
<AnimatedEndCallbackTest />
</TestCase>
<TestCase itShould="animate width">
<AnimatedRectangle />
</TestCase>
<TestCase itShould="move red square horizontally relatively to the scroll offset">
<AnimatedScrollViewTestCase />
</TestCase>
<TestCase itShould="fade in and out when clicked">
<FadeInOut />
<FadeInOut nativeDriver />
</TestCase>
<TestCase itShould="rotate grey square after red square with 0.5 second delay">
<Delay />
</TestCase>
<TestCase itShould="rotate red square in a loop">
<Loop />
</TestCase>
<TestCase itShould="rotate both squares in paralell">
<Parallel />
</TestCase>
<TestCase itShould="rotate button on press">
<AnimatedPressableView />
</TestCase>
<TestCase itShould="rotate squares with different stiffness/mass">
<Spring />
</TestCase>
<TestCase itShould="move squares with different initial velocity and deceleration values">
<Decay />
</TestCase>
<TestCase itShould="move square immediately after pressing button">
<DiffClamp />
</TestCase>
<TestCase itShould="move grey square 2x further horizontally than red">
<Multiply />
</TestCase>
<TestCase itShould="move grey twice but half the total distance of red">
<Modulo />
</TestCase>
<TestCase skip itShould="move red square closer">
<Perspective />
</TestCase>
<TestCase itShould="move square both vertically and horizontally">
<ValueXY />
</TestCase>
<TestCase
skip
itShould="(broken everywhere) move both squares, with blue square following the red with a spring">
<TrackingValue />
</TestCase>
</TestSuite>
);
}
function AnimatedRectangle() {
const animWidth = React.useRef(new Animated.Value(100)).current;
const animation = React.useMemo(() => {
const expand = Animated.timing(animWidth, {
toValue: 300,
duration: 1000,
useNativeDriver: false,
});
const contract = Animated.timing(animWidth, {
toValue: 100,
duration: 1000,
useNativeDriver: false,
});
return Animated.sequence([expand, contract]);
}, []);
return (
<Animated.View
style={{
height: 100,
width: animWidth,
backgroundColor: 'red',
}}
onTouchEnd={() => {
animation.reset();
animation.start();
}}
/>
);
}
const AnimatedScrollViewTestCase = () => {
const scrollY = new Animated.Value(0);
const translation = scrollY.interpolate({
inputRange: [0, 200],
outputRange: [0, 200],
extrapolate: 'clamp',
});
return (
<View
style={{
width: '100%',
height: 100,
position: 'relative',
overflow: 'hidden',
}}>
<Animated.ScrollView
style={{ width: '100%', height: '100%' }}
contentContainerStyle={{ alignItems: 'center', justifyContent: 'center' }}
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{
useNativeDriver: true,
},
)}>
{new Array(3).fill(0).map((_, idx) => {
return (
<View
key={idx}
style={{
width: '100%',
height: 50,
backgroundColor: 'gray',
marginBottom: 50,
}}
/>
);
})}
</Animated.ScrollView>
<Animated.View
style={[
{
position: 'absolute',
bottom: 0,
transform: [{ translateX: translation }],
width: 32,
height: 32,
backgroundColor: 'red',
},
]}
/>
</View>
);
};
function FadeInOut({ nativeDriver = false }) {
const fadeAnim = React.useRef(new Animated.Value(1)).current;
const handleFadePress = () => {
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: nativeDriver,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: nativeDriver,
}),
]).start();
};
return (
<Pressable onPress={handleFadePress}>
<Animated.View
style={{
marginTop: 24,
height: 100,
width: 100,
opacity: fadeAnim,
backgroundColor: 'red',
}}>
<Text style={{ width: 100, height: 48 }}>
Press me to fade{nativeDriver && ' using native driver'}
</Text>
</Animated.View>
</Pressable>
);
}
const Delay = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const square2Anim = useRef(new Animated.Value(0)).current;
const animation = Animated.sequence([
Animated.timing(square1Anim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.delay(500),
Animated.timing(square2Anim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<AnimatedRotatingSquaresView
handleAnimation={handleAnimation}
square1Anim={square1Anim}
square2Anim={square2Anim}
/>
);
};
const Loop = () => {
const squareAnim = useRef(new Animated.Value(0)).current;
const [isRunning, setIsRunning] = useState(false);
const animation = Animated.loop(
Animated.sequence([
Animated.timing(squareAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(squareAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
]),
);
const handleAnimation = () => {
if (isRunning) {
animation.stop();
setIsRunning(false);
} else {
animation.reset();
animation.start();
setIsRunning(true);
}
};
return (
<AnimatedRotatingSquaresView
handleAnimation={handleAnimation}
square1Anim={squareAnim}
square2Anim={new Animated.Value(0)}
/>
);
};
const Parallel = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const square2Anim = useRef(new Animated.Value(0)).current;
const animation = Animated.parallel([
Animated.timing(square1Anim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square2Anim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<AnimatedRotatingSquaresView
handleAnimation={handleAnimation}
square1Anim={square1Anim}
square2Anim={square2Anim}
/>
);
};
const AnimatedRotatingSquaresView = (props: {
square1Anim: Animated.Value;
square2Anim: Animated.Value;
handleAnimation: () => void;
}) => {
return (
<Pressable
style={{ height: 100, width: '100%' }}
onPress={props.handleAnimation}>
<View style={{ flexDirection: 'row' }}>
<Animated.View
style={{
height: 50,
width: 50,
margin: 10,
backgroundColor: 'red',
transform: [
{
rotateZ: props.square1Anim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
/>
<Animated.View
style={{
height: 50,
width: 50,
margin: 10,
backgroundColor: 'grey',
transform: [
{
rotateZ: props.square2Anim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
/>
</View>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const AnimatedPressableView = () => {
const pressableAnim = useRef(new Animated.Value(0)).current;
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const animation = Animated.timing(pressableAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
});
const onPress = () => {
animation.reset();
animation.start();
};
return (
<View style={{ height: 80, width: '100%' }}>
<AnimatedPressable
style={{
width: 100,
margin: 20,
backgroundColor: 'red',
transform: [
{
rotateZ: pressableAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
onPress={onPress}>
<Text style={{ height: 40 }}>Press me to start animation</Text>
</AnimatedPressable>
</View>
);
};
const Spring = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const square2Anim = useRef(new Animated.Value(0)).current;
const animation = Animated.parallel([
Animated.spring(square1Anim, {
toValue: 1,
stiffness: 1,
useNativeDriver: true,
}),
Animated.spring(square2Anim, {
toValue: 1,
mass: 20,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
rotateZ: square1Anim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
/>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'grey',
transform: [
{
rotateZ: square2Anim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
/>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const Decay = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const square2Anim = useRef(new Animated.Value(0)).current;
const animation = Animated.parallel([
Animated.decay(square1Anim, {
velocity: 0.5,
useNativeDriver: true,
}),
Animated.decay(square2Anim, {
velocity: 0.25,
deceleration: 0.99875,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
translateX: square1Anim,
},
],
}}
/>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'grey',
transform: [
{
translateX: square2Anim,
},
],
}}
/>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const Multiply = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const multValue = new Animated.Value(2);
const squareMultAnim = Animated.multiply(square1Anim, multValue);
const animation = Animated.timing(square1Anim, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
});
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
translateX: square1Anim,
},
],
}}
/>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'grey',
transform: [
{
translateX: squareMultAnim,
},
],
}}
/>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const DiffClamp = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const clamped = Animated.diffClamp(square1Anim, 0, 50);
const [direction, setDirection] = useState('left');
const animation = React.useRef<Animated.CompositeAnimation>();
const handleAnimation = () => {
animation.current?.stop();
animation.current = Animated.timing(square1Anim, {
toValue: direction === 'left' ? 400 : 0,
duration: 4000,
useNativeDriver: true,
});
setDirection(direction === 'left' ? 'right' : 'left');
animation.current.start();
};
return (
<Pressable style={{ width: '100%' }} onPress={handleAnimation}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
translateX: clamped,
},
],
}}
/>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const ValueXY = () => {
const square1Anim = useRef(new Animated.ValueXY({ x: 50, y: 50 })).current;
const animation = Animated.sequence([
Animated.timing(square1Anim, {
toValue: { x: 0, y: 0 },
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: { x: 0, y: 50 },
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: { x: 50, y: 0 },
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: { x: 50, y: 50 },
duration: 1000,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<View style={{ height: 100 }}>
<Animated.View
style={{
height: 40,
width: 40,
backgroundColor: 'red',
transform: [
{
translateX: square1Anim.x,
},
{
translateY: square1Anim.y,
},
],
}}
/>
</View>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const AnimatedEndCallbackTest = () => {
const [count, setCount] = React.useState(0);
const increase = () => {
Animated.timing(new Animated.Value(20), {
toValue: 0,
useNativeDriver: true,
duration: 1000,
}).start(() => {
setCount(c => c + 1);
});
};
return <Button onPress={increase} label={JSON.stringify(count)} />;
};
const TrackingValue = () => {
const square1Anim = useRef(new Animated.Value(50)).current;
const square2Anim = useRef(new Animated.Value(50)).current;
const animation = Animated.sequence([
Animated.timing(square1Anim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: 50,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(square1Anim, {
toValue: 50,
duration: 1000,
useNativeDriver: true,
}),
]);
const handleAnimation = () => {
animation.reset();
animation.start();
const tracking = Animated.spring(square2Anim, {
toValue: square1Anim,
useNativeDriver: true,
mass: 10,
});
tracking.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<View style={{ height: 100 }}>
<Animated.View
style={{
height: 40,
width: 40,
backgroundColor: 'red',
transform: [{ translateX: square1Anim }],
}}
/>
<Animated.View
style={{
height: 40,
width: 40,
backgroundColor: 'blue',
transform: [{ translateX: square2Anim }],
}}
/>
</View>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const Modulo = () => {
const square1Anim = useRef(new Animated.Value(0)).current;
const squareModuloAnim = Animated.modulo(square1Anim, 100);
const animation = Animated.timing(square1Anim, {
toValue: 199,
duration: 1000,
useNativeDriver: true,
});
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
translateX: square1Anim,
},
],
}}
/>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'grey',
transform: [
{
translateX: squareModuloAnim,
},
],
}}
/>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};
const Perspective = () => {
const square1Anim = useRef(new Animated.Value(500)).current;
const animation = Animated.timing(square1Anim, {
toValue: 50,
duration: 1000,
useNativeDriver: true,
});
const handleAnimation = () => {
animation.reset();
animation.start();
};
return (
<Pressable style={{ height: 120, width: '100%' }} onPress={handleAnimation}>
<View style={{ height: 100, justifyContent: 'center' }}>
<Animated.View
style={{
height: 50,
width: 50,
backgroundColor: 'red',
transform: [
{
rotateY: '60deg',
},
{
perspective: square1Anim,
},
],
}}
/>
</View>
<Text style={{ height: 20 }}>Press me to start animation</Text>
</Pressable>
);
};