Lesson 07-React Native Animations

React Native Animation Basics

Importance and Core Concepts of Animations

Animations enhance the interactivity of applications, for example:

  • Visual Feedback: Scaling effects on button presses.
  • Page Transitions: Sliding animations during screen changes.
  • User Guidance: Highlighting new features.

React Native’s animation system is based on a declarative API, where values are bound to animation variables and driven by time or gestures.

Supported Animation Types

  • Property Animations: Modify properties like opacity or transform.
  • Position Animations: Change a component’s position.
  • Gesture Animations: Respond to user touches or drags.

Animated API

Core Components of Animated API

Animated is React Native’s built-in animation library. Its main components include:

  • Animated.Value: Animation value that drives changes.
  • Animated.timing: Time-driven linear animations.
  • Animated.spring: Spring animations that simulate physical effects.
  • Animated.sequence / parallel: Combine multiple animations.

Basic Animation Implementation (Fade In/Out)

We’ll implement a simple fade-in effect.

Example Code

import React, { useRef, useEffect } from 'react';
import { View, Animated, Button, StyleSheet } from 'react-native';

const App = () => {
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 2000,
      useNativeDriver: true,
    }).start();
  }, [fadeAnim]);

  const resetAnimation = () => {
    fadeAnim.setValue(0);
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 2000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, { opacity: fadeAnim }]} />
      <Button title="Reset" onPress={resetAnimation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'tomato' },
});

export default App;

Analysis

  • Animated.Value: Initialized to 0, representing full transparency.
  • Animated.timing: Transitions the value to 1 (fully opaque) over 2 seconds.
  • useNativeDriver: Uses native driver for better performance.
  • Reset: Uses setValue to reset the animation value.

Composite and Sequential Animations

We can combine multiple animations, such as scaling and moving simultaneously.

Example Code

import React, { useRef } from 'react';
import { View, Animated, Button, StyleSheet } from 'react-native';

const App = () => {
  const scaleAnim = useRef(new Animated.Value(1)).current;
  const translateAnim = useRef(new Animated.Value(0)).current;

  const runAnimation = () => {
    Animated.parallel([
      Animated.timing(scaleAnim, {
        toValue: 1.5,
        duration: 1000,
        useNativeDriver: true,
      }),
      Animated.timing(translateAnim, {
        toValue: 100,
        duration: 1000,
        useNativeDriver: true,
      }),
    ]).start(() => {
      Animated.sequence([
        Animated.timing(scaleAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
        Animated.timing(translateAnim, { toValue: 0, duration: 500, useNativeDriver: true }),
      ]).start();
    });
  };

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.box,
          { transform: [{ scale: scaleAnim }, { translateY: translateAnim }] },
        ]}
      />
      <Button title="Run Animation" onPress={runAnimation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'purple' },
});

export default App;

Analysis

  • Animated.parallel: Executes scaling and translation simultaneously.
  • Animated.sequence: Sequentially executes scale-down and reset.
  • transform: Supports multiple transformations (e.g., scale, translateY).

Gesture-Driven Animations

Use PanResponder to implement a draggable animation.

Example Code

import React, { useRef } from 'react';
import { View, Animated, PanResponder, StyleSheet } from 'react-native';

const App = () => {
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {
      useNativeDriver: false,
    }),
    onPanResponderRelease: () => {
      Animated.spring(pan, {
        toValue: { x: 0, y: 0 },
        useNativeDriver: true,
      }).start();
    },
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[styles.box, { transform: [{ translateX: pan.x }, { translateY: pan.y }] }]}
        {...panResponder.panHandlers}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'teal' },
});

export default App;

Analysis

  • PanResponder: Handles touch events.
  • Animated.event: Binds gesture offsets to animation values.
  • Animated.spring: Snaps back to the original position on release.

React-Native-Reanimated

Advantages and Installation of Reanimated

react-native-reanimated (Reanimated) is a community-developed animation library with advantages including:

  • Native Performance: Animation logic runs on the UI thread.
  • Flexibility: Supports complex gestures and animations.
  • Worklets: Allows defining animation logic on the native thread.

Installation

npm install react-native-reanimated

Add the plugin to babel.config.js:

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: ['react-native-reanimated/plugin'],
};

Basic Animation Implementation

Implement a fade-in animation.

Example Code

import React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
} from 'react-native-reanimated';

const App = () => {
  const opacity = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));

  const startAnimation = () => {
    opacity.value = withTiming(1, { duration: 2000 });
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button title="Start" onPress={startAnimation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'coral' },
});

export default App;

Analysis

  • useSharedValue: Shared animation value.
  • useAnimatedStyle: Dynamically generates styles.
  • withTiming: Time-driven animation.

Optimizing Performance with Worklets

Worklets allow custom logic to run on the UI thread.

Example Code

import React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withRepeat,
  withTiming,
} from 'react-native-reanimated';

const App = () => {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const startAnimation = () => {
    scale.value = withRepeat(
      withTiming(1.5, { duration: 1000 }),
      -1, // Infinite repeat
      true // Reverse playback
    );
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button title="Pulse" onPress={startAnimation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'lime' },
});

export default App;

Analysis

  • withRepeat: Repeats the animation.
  • Worklet: Logic in useAnimatedStyle runs as a Worklet on the UI thread.

Gesture Animations with Reanimated

Combine react-native-gesture-handler for drag animations.

Installation

npm install react-native-gesture-handler

Example Code

import React from 'react';
import { View, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const App = () => {
  const offsetX = useSharedValue(0);
  const offsetY = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: offsetX.value }, { translateY: offsetY.value }],
  }));

  const panGesture = Gesture.Pan()
    .onUpdate(event => {
      offsetX.value = event.translationX;
      offsetY.value = event.translationY;
    })
    .onEnd(() => {
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    });

  return (
    <View style={styles.container}>
      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  box: { width: 100, height: 100, backgroundColor: 'indigo' },
});

export default App;

Analysis

  • Gesture.Pan: Handles drag gestures.
  • onUpdate: Updates position in real-time.
  • withSpring: Snaps back on release.

Comprehensive Case Study

Requirement Analysis

We’ll create an interactive card that:

  • Scales and rotates on tap.
  • Supports dragging, resetting on release.
  • Implements solutions using both Animated API and Reanimated.

Implementation with Animated API and Reanimated

Animated API Implementation

import React, { useRef } from 'react';
import { View, Animated, PanResponder, TouchableOpacity, StyleSheet } from 'react-native';

const App = () => {
  const scale = useRef(new Animated.Value(1)).current;
  const rotate = useRef(new Animated.Value(0)).current;
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {
      useNativeDriver: false,
    }),
    onPanResponderRelease: () => {
      Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: true }).start();
    },
  });

  const rotateInterpolate = rotate.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  });

  const animateCard = () => {
    Animated.parallel([
      Animated.timing(scale, { toValue: 1.5, duration: 500, useNativeDriver: true }),
      Animated.timing(rotate, { toValue: 1, duration: 500, useNativeDriver: true }),
    ]).start(() => {
      Animated.parallel([
        Animated.timing(scale, { toValue: 1, duration: 500, useNativeDriver: true }),
        Animated.timing(rotate, { toValue: 0, duration: 500, useNativeDriver: true }),
      ]).start();
    });
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={animateCard}>
        <Animated.View
          style={[
            styles.card,
            {
              transform: [
                { scale },
                { rotate: rotateInterpolate },
                { translateX: pan.x },
                { translateY: pan.y },
              ],
            },
          ]}
          {...panResponder.panHandlers}
        />
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  card: { width: 150, height: 150, backgroundColor: 'salmon' },
});

export default App;

Reanimated Implementation

import React from 'react';
import { View, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const App = () => {
  const scale = useSharedValue(1);
  const rotate = useSharedValue(0);
  const offsetX = useSharedValue(0);
  const offsetY = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: scale.value },
      { rotate: `${rotate.value}deg` },
      { translateX: offsetX.value },
      { translateY: offsetY.value },
    ],
  }));

  const tapGesture = Gesture.Tap().onEnd(() => {
    scale.value = withTiming(1.5, { duration: 500 }, () => {
      scale.value = withTiming(1, { duration: 500 });
    });
    rotate.value = withTiming(360, { duration: 500 }, () => {
      rotate.value = withTiming(0, { duration: 500 });
    });
  });

  const panGesture = Gesture.Pan()
    .onUpdate(event => {
      offsetX.value = event.translationX;
      offsetY.value = event.translationY;
    })
    .onEnd(() => {
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    });

  const composedGesture = Gesture.Exclusive(panGesture, tapGesture);

  return (
    <View style={styles.container}>
      <GestureDetector gesture={composedGesture}>
        <Animated.View style={[styles.card, animatedStyle]} />
      </GestureDetector>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  card: { width: 150, height: 150, backgroundColor: 'salmon' },
});

export default App;

Analysis

  • Animated API: Uses timing and PanResponder for animations and dragging.
  • Reanimated: Leverages withTiming and Gesture API for cleaner code and better performance.
  • Differences: Reanimated runs on the UI thread, avoiding JavaScript thread bottlenecks.

Advanced Techniques and Considerations

Performance Optimization Tips

  • Use useNativeDriver: Enable it in Animated API whenever possible.
  • Avoid Frequent Updates: Minimize unnecessary animation triggers.
  • Worklets: Use Worklets in Reanimated to offload complex logic to the UI thread.

Debugging Animation Techniques

  • Log Output: Print animation values (e.g., fadeAnim.addListener or console.log).
  • Slow Animations: Extend duration to inspect effects.
  • React Native Debugger: Monitor animation frame rates.

Share your love