skip to Main Content

I’m a beginner to React Native (Expo Go) and I am trying to make the following animation:

On app launch, an image starts at a random position around halfway up the screen and moves upwards until it gets out of bounds.
Once out of bounds, the image now comes up from the bottom of the screen.

What I’ve had trouble with is having the image start midway on ONLY the first animation.

I’ve played around with different Reanimated libraries such as withRepeat and withTiming with no avail.

2

Answers


  1. Try like this
    Snack Link: 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;
    
    Login or Signup to reply.
  2. Doing this in reanimated is a kind of tricky. You can use withRepeat and withSequence to get smooth looping animations; but only once the you’ve completed the first scroll up screen. And as of getting the image to start at random place in the middle of the screen you can use useSharedValue along with useWindowDimensions to get the image position near the halfway mark:

    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)
      );
    

    Now we set up the animated style using width and height to prevent the position from traveling offscreen:

    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,
        };
      });
    

    And then because you may want to use a similar looping animation on the the x value I decided to create a function for looping (distRatio is used to fine tune the animation duration):

    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
          );
        }
      );
    };
    

    And bringing it all together (demo):

    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,
      },
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search