Я новичок в React Native (Expo Go) и пытаюсь создать следующую анимацию:
При запуске приложения изображение начинается в произвольной позиции примерно на середине экрана и перемещается вверх, пока не выйдет за пределы. После выхода за пределы изображения изображение теперь появляется в нижней части экрана.
У меня возникла проблема с тем, что изображение начиналось ТОЛЬКО в середине первой анимации.
Я безуспешно экспериментировал с различными реанимированными библиотеками, такими как withRepeat и withTiming.
Попробуйте вот так Ссылка на закуску: https://snack.expo.dev/@cyberking40/7d4562
import React, { Component } from 'react';
import { View, Image, Animated, Dimensions } from 'react-native';
const { height } = Dimensions.get('window');
class AnimatedImage extends Component {
constructor(props) {
super(props);
this.state = {
position: new Animated.Value(height / 2), // Initial position halfway up the screen
};
}
componentDidMount() {
this.moveImage();
}
moveImage = () => {
Animated.timing(this.state.position, {
toValue: -height, // Move the image to the top of the screen
duration: 3000, // Adjust the duration as needed
useNativeDriver: false,
}).start(() => {
// When animation completes, reset position and start again
this.state.position.setValue(height); // Move the image to the bottom of the screen
this.moveImage();
});
};
render() {
const { position } = this.state;
return (
<Animated.View style = {{ position: 'absolute', top: position }}>
<Image source = {require('./assets/snack-icon.png')} />
</Animated.View>
);
}
}
export default AnimatedImage;
Сделать это в реанимации довольно сложно. Вы можете использовать withRepeat
и withSequence
, чтобы получить плавную зацикленную анимацию; но только после того, как вы завершили первую прокрутку экрана вверх. А чтобы изображение начиналось в случайном месте в середине экрана, вы можете использовать useSharedValue
вместе с useWindowDimensions
, чтобы получить положение изображения рядом с средней отметкой:
const { width, height } = useWindowDimensions();
const remainingWidth = width - imageSize;
const remainingHeight = height - imageSize;
const imageX = useSharedValue(
getRandomIntWithinRange(remainingWidth * 0.5, 50)
);
const imageY = useSharedValue(
getRandomIntWithinRange(remainingHeight * 0.5, 100)
);
Теперь мы настраиваем анимированный стиль, используя width
и height
, чтобы позиция не выходила за пределы экрана:
const imageStyle = useAnimatedStyle(() => {
return {
// since we will allow imageY to exceed
// the the height of the parent view
// we need to bound it
bottom: imageY.value % height,
left: imageX.value%width,
};
});
А затем, поскольку вы, возможно, захотите использовать аналогичную зацикленную анимацию для значения x, я решил создать функцию для зацикливания (distRatio
используется для точной настройки продолжительности анимации):
export const loopAnimationAtValue = (animValue, maxValue,distRatio=1.8) => {
const distToReset = maxValue - animValue.value;
// withRepeat combined withSequence will allow you to
// get indefinite animation; but to get smooth transitions
// first move image off screen the first time
animValue.value = withTiming(
animValue.value + distToReset,
{ duration: distToReset * distRatio },
(finished) => {
// once image is offscreen just continously scroll by maxValue
if (!finished) return;
animValue.value = withRepeat(
withSequence(
withTiming(animValue.value + maxValue, {
duration: maxValue * distRatio,
})
),
-1
);
}
);
};
И собираем все это вместе (демо):
import {
Text,
SafeAreaView,
StyleSheet,
Image,
Button,
View,
useWindowDimensions,
} from 'react-native';
import { useEffect, useMemo } from 'react';
import Animated, {
useAnimatedStyle,
useSharedValue,
cancelAnimation,
withTiming,
} from 'react-native-reanimated';
import { getRandomIntWithinRange, loopAnimationAtValue } from './helpers';
const imageSize = 100;
// multiple distance traveled by this value to get
// get animation duration
const distRatio = 1.8;
const AnimatedImage = Animated.createAnimatedComponent(Image);
export default function App() {
const { width, height } = useWindowDimensions();
const remainingWidth = width - imageSize;
const remainingHeight = height - imageSize;
const imageX = useSharedValue(
getRandomIntWithinRange(remainingWidth * 0.5, 50)
);
const imageY = useSharedValue(
getRandomIntWithinRange(remainingHeight * 0.5, 100)
);
const imageStyle = useAnimatedStyle(() => {
return {
// since we will allow imageY to exceed
// the the height of the parent view
// we need to bound it
bottom: imageY.value % height,
left: imageX.value % width,
};
});
const getRandomPosition = () => {
stopAnimation();
imageY.value = getRandomIntWithinRange(
remainingHeight * 0.5,
remainingHeight * 0.35
);
imageX.value = getRandomIntWithinRange(remainingWidth * 0.5, 50);
loopAnimationAtValue(imageY, height,distRatio);
// can loop x if wanted
// loopAnimationAtValue(imageX,width,distRatio)
};
const stopAnimation = () => {
cancelAnimation(imageX);
cancelAnimation(imageY);
};
useEffect(() => {
loopAnimationAtValue(imageY, height);
// cleanup animation
return () => {
cancelAnimation(imageY);
};
}, [height, imageY]);
return (
<SafeAreaView style = {styles.container}>
<AnimatedImage
source = {require('./assets/snack-icon.png')}
style = {[styles.image, imageStyle]}
/>
<View style = {styles.buttonRow}>
<Button title = "Set Random Position" onPress = {getRandomPosition} />
<Button title = "Stop" onPress = {stopAnimation} />
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
// justifyContent: 'center',
backgroundColor: '#ecf0f1',
padding: 8,
},
image: {
position: 'absolute',
width: imageSize,
height: imageSize,
},
buttonRow: {
flexDirection: 'row',
width: '100%',
justifyContent: 'space-between',
paddingHorizontal: 10,
},
});