Hi

Hey, you can call me Doho. I am a front-end developer and currently in charge of an App developed by RN in our company.

I’m excited to share some of the lessons I learned using react-native Reanimated and making react-native reanimated carousel, Because there are few Chinese materials and videos about react-native Reanimated, MOST of my learning channels come from official documents and videos of the bald man on YouTube.

Most of the official documents are in English, so there are some obstacles for those who are not very good at English.

Most of the videos on YouTube are good, but some of the explanations are a bit jumpy so there are some areas that are hard to follow.

Therefore, I plan to try to realize the videos I have learned again and produce them in the way of Chinese articles. In fact, I want to exercise my summarizing ability and writing skills. The follow-up will be in the way of article or video series push! ~

My GitHub homepage

My Carousel open source library

Snack demo

Reanimated 2

Why Reanimated? Because in React Native, by default, all updates are delayed by at least one frame, because communication between the UI and the JS thread is asynchronous, and the UI thread never waits for the JS thread to process all events.

And besides JS doing Diff, updates, executing application business logic, handling network requests… In addition, events are often not handled immediately, resulting in more severe delays.

Reanimated’s approach is to move the logic that handles animations and events from the JSt thread to the UI thread.

More details are not the focus of this article, you can Google ~

The cause of

Business needs to add a rotation map to support cyclic scrolling on the home page, so I searched github for some useful components in the community. Excluding some libraries that were updated only four or five years ago, there is still a component library called React-Native Snap-Carousel.

However, I found a problem in using it. When I quickly swiped through the loop, I got stuck, which looked like I had to wait for elements to append to the head or end of the loop.

Because of the time problem, so did not go deep to see the implementation of the source code, the middle of the community we mentioned a variety of ways to try is also unable to solve, mostly by increasing the number of pre-rendering before and after, but in fact, or the way to treat the symptoms.

The library also has a low maintenance frequency and a lot of unresolved issues, although the README says there will be a completely react-native Gesture-handler +react-native Reanimated implementation in the future, and it will be very useful. But it’s been a long time since they announced it and they haven’t released the official version yet, so let’s just do it ourselves, but accidentally use the same library they planned, which is the gesture and animation library above.

What are we trying to solve

You don’t get stuck if you slide sideways quickly while rolling in a loop

Implementation approach

  1. First we have three images by default and have slid to the second image in the middle

  1. We drag the image and slide it to the right


  1. After half of the first image is in the wheel view, we move the last image to the front

  1. So we’ve done a circular scroll to one side and the other way around. It works by sliding and appending, and relies on Reanimated, so the whole processing logic is still done in the UI thread, and moving images doesn’t cause animation to freeze.

Pre –

Since there may be partners who do not use either library, this section will cover some basic API usage, and refer to the respective documentation for details.

The react – native – gesture – handler, the react – native – reanimated

  • PanGestureHandler

After wrapping the view container, you can call back to get parameters for different gesture movements.

  • useAnimatedGestureHandler

A simple hook, use on PanGestureHandler onHandlerStateChangeprops, onStart, onActive, can be set in the hook onEnd… Response events for various events.

  • useSharedValue

The animation value generated by Reanimated, whose changes affect the behavior of the animation.

  • useAnimatedStyle

Reanimated needs to use useAnimatedStyle to generate the style, because it controls the generated style when the SharedValue changes, and also allows the generated style to be associated with Reanimated.View.

  • The View element in Reanimated

Using the basic condition of the Reanimated value, after placing the SharedValue in useAnimatedStyle, the returned style can be passed to the Styles property to animate the element.

  • useDerivedValue

Responds to a change in a SharedValue value and produces a read-only value.

const number_a = useSharedValue<number> (1);
const number_b = useDerivedValue(() = >{
	return number_a.value*10}, [])// number_a = 1
// number_b = 10
Copy the code
  • interpolate

Make SharedValue create a map. This is very useful when modifying an animation effect. For example, if we have an avatar, it is 100 in width and enlarged to 200 after login.

Types: interpolate(SharedValue,inputRange,outputRange,? ExtrapolateParameter)

SharedValue: animation value

InputRange: indicates the inputRange

OutputRange: indicates the outputRange

ExtrapolateParameter? : After the input range overflows, whether to change according to the output range (optional)

/ / pseudo code
const loginStatusAnim = useSharedValue<number> (0); 

const style = useAnimatedStyle(() = >{
	return {
		transform: [{scale:interpolate(
				loginStatusAnim.value,
				// Here our loginStatusAnim will only change between 0 and 1
				[0.1].// But we want the output value to map to 100-200, of course we can directly change 0 and 1 to 100 and 200, so here is just a demonstration
				[100.200]}]}},[])return <Reanimated.View style={style}></Reanimated.View>

Copy the code

On the whole

The code here is not unpastable pseudo-code, you can paste step by step into the editor, you can see the effect.

  1. First of all, we can use Expo to initiate a project. Using Expo here can avoid some loose ends and focus more on this attempt.

So we have an initial project that we can use that will smooth out any dependency differences that we might introduce later on.

expo init my-project
cd ./my-project
Copy the code
  1. Install the library we need to use, in this case to avoid late library upgrades, the API may change, so the version is specified.
Yarn add [email protected] [email protected]Copy the code
  1. First we need a container to handle the gesture logic, and the container will arrange the elements horizontally. We will use gestures to create a container similar to ScrollView, with more flexible control of sliding left and right. The main logic is inanimatedListScrollHandlerIn the.

Then to get the element moving, we need two more steps to generate the element with the offset X and the element with the offset X.

Carousel.tsx

import React from 'react';
import { Dimensions, Text, View } from 'react-native';
import Animated, {
  useAnimatedGestureHandler,
  useSharedValue,
  useDerivedValue
} from 'react-native-reanimated';
import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import { useComputedAnim } from './useComputedAnim';
import { Layouts } from './Layouts';

const data = [1.2.3];
const { width } = Dimensions.get('window');
const height = 300;

const Carousel: React.FC = () = > {
  // 1. Get the 'base value' to use in the calculation. (Just a wrapper logic)
  const computedAnimResult = useComputedAnim(width, data.length);

  const animatedListScrollHandler = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
    onStart:... .onActive:... ,} [])return <PanGestureHandler onHandlerStateChange={animatedListScrollHandler}>{/* // 2. The gesture container specifies the layout elements that need to be nested within Reanimated */}<Animated.View
      style={{
        // 3.To specify the width and height of the caroute container, most of our calculations need to rely on the knownwidth.width.height.flexDirection: 'row',
        position: 'relative'}} >{data.map((_, I) => {return (// 3<Layouts width={width} index={i} key={i} offsetX={offsetX} computedAnimResult={computedAnimResult}>
            <View style={{ flex: 1.backgroundColor: "red", justifyContent: "center", alignItems: "center", borderWidth: 1.borderColor: "black}} ">
              <Text style={{ fontSize: 100}} >{i}</Text>
            </View>
          </Layouts>
        );
      })}
    </Animated.View>
  </PanGestureHandler>
}

export default Carousel;
Copy the code

useComputedAnim.ts

export interface IComputedAnimResult {
    MAX: number;
    MIN: number;
    WL: number;
    LENGTH: number;
}

export function useComputedAnim(
    width: number,
    LENGTH: number
) :IComputedAnimResult {
	  /* * 1. After removing the width of the header and tail elements, the distance between them can be slid * because the position of the header and tail elements should be moved to the other side */
    const MAX = (LENGTH - 2) * width;
	  // 2
    const MIN = -MAX;
	  // 3. The total length of the elements
    const WL = width * LENGTH;

    return {
        MAX,
        MIN,
        WL,
        LENGTH,
    };
}
Copy the code
  1. Now we’re going to perfectanimatedListScrollHandlerThe logic that allows the gesture to slide can make the container internal offsetXChange occurs.

Here we are done generating the offset X.

Carousel.tsx

const Carousel:React.FC = () = > {
	/ /...

	// 1. Offset of position
  const handlerOffsetX = useSharedValue<number> (0);

	// 2. The offset value needs to be converted to return to 0 after a cycle, which is the actual value we use
  const offsetX = useDerivedValue(() = > {
    const x = handlerOffsetX.value % computedAnimResult.WL;
    return isNaN(x) ? 0 : x;
  }, [computedAnimResult]);

	// This Hook calls the set method when the gesture occurs and returns parameters that inform the gesture
	const animatedListScrollHandler = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
		/** * 3. CTX is the temporary context provided by method execution, we can record some temporary variables ** here we drag the current offset to the context */ 
		onStart: (_, ctx: any) = > {
     		ctx.startContentOffsetX = handlerOffsetX.value;
     },
		/** * 4. We take the initial position from the context and add it to the X offset of the slide returned by onActive to move the element left and right */ 	
     onActive: (e, ctx: any) = >{ handlerOffsetX.value = ctx.startContentOffsetX + e.translationX; }}, [])/ /...
}
Copy the code
  1. So we have the most important valueoffsetXBecause each of them is going to be moved independentlyAt the end oforThe headOf the elements to the other side, so to accurately control their position, you need to apply this value to eachCarouselItem elements.

Layouts.tsx

import React from 'react';
import { FlexStyle, View } from 'react-native';
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
import { IComputedAnimResult } from './useComputedAnim';
import { useOffsetX } from './useOffsetX';

export const Layouts: React.FC<{
  index: number;
  width: number; height? : FlexStyle['height'];
  offsetX: Animated.SharedValue<number>; computedAnimResult: IComputedAnimResult; } > =(props) = > {
  const {
    index,
    width,
    children,
    height = '100%',
    offsetX,
    computedAnimResult,
  } = props;

  /* * 1. This is the core logic that makes the CarouselItem element move correctly, which we'll cover below */
  const x = useOffsetX({
    offsetX,
    index,
    width,
    computedAnimResult,
  });

  /* * 2. Reanimated needs to use useAnimatedStyle to generate style * because it will control the generated style when sharedValue changes * and the generated style is also allowed to be associated with Reanimated
  const offsetXStyle = useAnimatedStyle(() = > {
    return {
      /* * 3. Here we need to use 'index * width' to get the elements back to the origin (i.e. they are all stacked together) * and then use the exact value 'x.value' we calculated with 'useOffsetX' to control their position */
      transform: [{ translateX: x.value - index * width }], }; } []);return (
    // 4. Set the style
    <Animated.View style={offsetXStyle}>
      <View style={{ width.height}} >{children}</View>
    </Animated.View>
  );
}

export default Layouts;
Copy the code

Here we have the element with the offset X, and now we have a basic look for the wheel map, which can slide sideways, but we need to complete the logic in useOffsetX if we want it to loop.

  1. And then finally we need to letuseOffsetXA hook produces the correct offset value for an element so that it can be converted to the other side near the end or head.

The calculation part of this method is a little tricky, but the general idea is that when the offset value X changes, we make sure that it exceeds the boundary we set, and if it exceeds the boundary, we put it on the other side. It doesn’t matter if you don’t, because maybe you can figure it out and implement your own logic

useOffsetX.ts

import Animated, {
    Extrapolate,
    interpolate,
    useDerivedValue,
} from 'react-native-reanimated';
import type { IComputedAnimResult } from './useComputedAnim';

interface IOpts {
    index: number;
    width: number;
    computedAnimResult: IComputedAnimResult;
    offsetX: Animated.SharedValue<number>;
}

export const useOffsetX = (opts: IOpts) = > {
    const { offsetX, index, width, computedAnimResult } = opts;
    const { MAX, WL, MIN, LENGTH } = computedAnimResult;
    const x = useDerivedValue(() = > {
            // The offset from the origin of each element
            const Wi = width * index;

            // The starting value of each element should be rotated to the other side if the boundary is crossed
            const startPos = Wi > MAX ? MAX - Wi : Wi < MIN ? MIN - Wi : Wi;

            const inputRange = [
                // WL is the movable area where the tail and the head are removed
                -WL,
                // Here is the position condition before crossing the border
                -((LENGTH - 2) * width + width / 2) - startPos - 1.// Here is the position condition after crossing the boundary
                -((LENGTH - 2) * width + width / 2) - startPos,
                / / the origin
                0./ / in the opposite direction
                (LENGTH - 2) * width + width / 2 - startPos,
                / / in the opposite direction
                (LENGTH - 2) * width + width / 2 - startPos + 1./ / in the opposite direction
                WL,
            ];

            const outputRange = [
               // the corresponding WL loops once, so it returns to the starting position
                startPos,
                1.5 * width - 1.// Turn over to the other side
                -((LENGTH - 2) * width + width / 2),
               // Return to the starting position
                startPos,
                // Turn over to the other side
                (LENGTH - 2) * width + width / 2, -1.5 * width - 1),
               // the corresponding WL loops once, so it returns to the starting position
                startPos,
            ];

            // Return the calculated X value, which is an absolute position relative to the origin, but our elements are arranged in order, so subtract index*width and put them at the origin
            returninterpolate( offsetX.value, inputRange, outputRange, Extrapolate.CLAMP ); } []);return x;
};
Copy the code
  1. At this point you should have a wheel map that slides sideways without stalling, but this is a very simple version. In fact, IN my library, I also made some hack fixes for the bugs of the react-native Gesture-handler and react-native Reanimated libraries, as well as some interactive optimisations, such as the inertia effect after dragging, and the paging effect. , these are not the scope of this article, so you can go to my warehouse to have a look! react-native-reanimated-carousel

React-native – Reanimated – Carousel-example

More functions

I will improve more APIS in my react-Native Reanimated Carousel project to make this component easier to use, but it may not increase the complex UI effects of react-Native Snap-Carousel. The goal is to make this component simpler and more flexible.

Hope more partners can participate in the maintenance together, or to make more suggestions, come, come! To project

At the end of

Thank you for reading and I look forward to receiving suggestions, questions or corrections.

Star 🌟, react-native-reanimated- Carousel, thank you!

I will write more articles on react-Native reanimated V2 in the future. I hope it will be helpful for students and families! My GitHub homepage

Ps: Reprint please indicate the source