Let's reanimate

Drawing text and images is one thing but the real test of a UI framework is in how good is it with animating content.

And my test for animation is the classic MoveMe based on the Apple’s sample code.

The idea is to draw three boxes on the screen. When selected the box changes color and scales up and then can be moved around with the drag gesture and eventually restores back to original color and size when released.

Let’s build that sample using React Native’s Reanimated library.

Setup

I’m following the official docs but not using their template. So I’ve a basic project created with the blank template and installed the dependencies

npx create-expo-app moveme --template blank
npx expo install react-native-reanimated
npx expo install react-native-gesture-handler

Next, I added the plugin by editing babel.config.js to

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: ["react-native-reanimated/plugin"],
  };
};

And then draw 3 squares on screen:

import { StatusBar } from "expo-status-bar";
import { StyleSheet, View } from "react-native";
import Animated from "react-native-reanimated";

function Square() {
  return <Animated.View style={styles.square}></Animated.View>;
}

export default function App() {
  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      <Square />
      <Square />
      <Square />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "space-evenly",
  },
  square: {
    width: 100,
    height: 100,
    backgroundColor: "blue",
  },
});

set up

Add gesture handler

To add support for gesture handlers we first need to wrap the content within the GestureHandlerRootView

<GestureHandlerRootView style={styles.container}>
  <Square />
  <Square />
  <Square />
</GestureHandlerRootView>

And then wrap each Square within GestureDetector

function Square() {
  const gesture = Gesture.Pan();

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={styles.square} />
    </GestureDetector>
  );
}

Handle gesture events

To handle gesture we first need to create a SharedValue which is like State but for animation states. For example, to change the background color when selected we need to listen to onBegin and onFinalize events and update the style:

function Square() {
  const isPressed = useSharedValue(false);
  const animStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: isPressed.value ? "red" : "blue",
    };
  });

  const gesture = Gesture.Pan()
    .onBegin(() => {
      isPressed.value = true;
    })
    .onFinalize(() => {
      isPressed.value = false;
    });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.square, animStyle]} />
    </GestureDetector>
  );
}

Supporting drag is similar. We need to store start and current positions and then update the current position on onChange event. The onChange provides the delta change that we then need to add to the start position to calculate the final current position. And then, finally at the onFinalize event we can sync the start and current positions.

function Square() {
  const isPressed = useSharedValue(false);
  const startPos = useSharedValue({ x: 0, y: 0 });
  const pos = useSharedValue({ x: 0, y: 0 });
  const animStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: isPressed.value ? "red" : "blue",
      transform: [
        { translateX: pos.value.x },
        { translateY: pos.value.y },
        { scale: withSpring(isPressed.value ? 1.2 : 1) },
      ],
    };
  });

  const gesture = Gesture.Pan()
    .onBegin(() => {
      isPressed.value = true;
    })
    .onChange((e) => {
      pos.value = {
        x: startPos.value.x + e.translationX,
        y: startPos.value.y + e.translationY,
      };
    })
    .onFinalize(() => {
      isPressed.value = false;
      startPos.value = {
        x: pos.value.x,
        y: pos.value.y,
      };
    });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.square, animStyle]} />
    </GestureDetector>
  );
}

And there you have it

final

The source code is available on https://github.com/chunkyguy/MoveMe/tree/main/reactnative

References