How it works?

How it works?


About the internal implementation of RNRC(react-native-reanimated-carousel).

Working principle

This is what we expect to see in the execution of RNRC.

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

steps-1 2. When we drag the image to the right

steps-2

steps-3

  1. When the first image is partially in the viewport, we move the last image to the front

steps-4

  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.

steps-5

This is how it works in code.

I don't think I have explained this logic clearly, but this is a version that has been revised many times, it is a complicated process, if any partners have their own understanding, welcome to submit PR, we will make this explanation more clear.
In my opinion, this part is undoubtedly very important for the initial stage of project development, but it is no longer so "important" at present, because the underlying content of this part will no longer change significantly. If you are interested in it, you can understand it, otherwise I don't think it is necessary to understand it.

  1. First we need a unit size to help us calculate the scroll distance. This size is the page size:

    • If itemWidth / itemHeight is provided, size uses that.
    • Otherwise, size falls back to the container axis size (prefer style.width / style.height, or legacy width / height, or layout measurement).
  2. Then we need a value handlerOffset, which is the current scroll distance, and it's a total value, if we scroll two, handlerOffset is equal to size * 2, if we scroll ten, handlerOffset is equal to size * 10.

  3. Followed by dealing with how to get at the end of the element at the right time to move to the front, this part of logic in ./src/hooks/useOffsetX.ts. First we need to know the current window size (the total number of elements rendered on one side). The window size defaults to half the total number of elements, i.e. full render.

steps-6

From the value of the window size we can calculate the position of the end of the forward or reverse element.

steps-7

Then we need to find the boundary (see if the image moves to the front). Currently we set this position to the total length of the side plus half the size of the element. This position is affected by the windowSize and can be changed using windowSize prop. For example, if windowSize is not set and the data length is 5, then one side length is size * (5-1)/2 = size * 2, then the boundary is size * 2 + size * 0.5, if windowSize is set to 3, then the boundary is size * 2 + size * 0.5. Size * (3-1)/2 = size * 1, size * 1 + size * 0.5

steps-8

In order to control the position of the elements more intuitively, we place the elements at the origin, and they all overlap at the origin, translateX:0. In this case, we need to calculate the base position of each element startPos based on the index of the element, for example, the base position of the first image is 0, the base position of the second image is SIZE, the base position of the third image is size * 2, and so on. Then, keeping the position of each element relative to each other, we subtract each element from its base position startPos and add the minimum boundary value number.min_value, so that we have the logic to change position immediately if we cross the boundary. It will be converted automatically by outputRange.

The above logic is translated into code as follows:

    const inputRange = [
        -TOTAL_WIDTH,
        MIN - HALF_WIDTH - startPos - Number.MIN_VALUE,
        MIN - HALF_WIDTH - startPos,
        0,
        MAX + HALF_WIDTH - startPos,
        MAX + HALF_WIDTH - startPos + Number.MIN_VALUE,
        TOTAL_WIDTH,
    ];
    const outputRange = [
        startPos,
        MAX + HALF_WIDTH - Number.MIN_VALUE,
        MIN - HALF_WIDTH,
        startPos,
        MAX + HALF_WIDTH,
        MIN - HALF_WIDTH + Number.MIN_VALUE,
        startPos,
    ];
    return interpolate(
        handlerOffset.value,
        inputRange,
        outputRange,
        Extrapolate.CLAMP
    );
  1. Now we have a nice x value that behaves differently for different indexes. For example, when we want the second element to come after the first, we can do something like this
const inputRange = [-1, 0 ,1]
const outputRange = [-size, 0 ,size]
return {
    transform: [
        { translateX: interpolate(handlerOffset.value, inputRange, outputRange) },
    ],
}

So when the first one is at the origin translateX:0, the next one is at the origin translateX:size, because the first one receives an X value of 0 and the second one receives an X value of 1. And if we continue to drag to the left, then -1 approaches -1.5, which, according to our logic, exceeds -1.50000...00001, the left most image receives an X value of 1.49999... 00001, which triggers the logic to change position.

Directory

./src
├── components
│   ├── Carousel.tsx # Public component facade
│   ├── CarouselLayout.tsx # Layout, progress, controller, and autoplay wiring
│   ├── ItemLayout.tsx # Per-item animated layout
│   ├── ItemRenderer.tsx # Visible-item rendering
│   ├── LazyView.tsx # Conditional item rendering
│   ├── Pagination # Basic and custom pagination
│   └── ScrollViewGesture.tsx # Pan gesture and boundary handling
├── hooks
│   ├── useAutoPlay.ts # Autoplay timer lifecycle
│   ├── useCarouselController.tsx # prev, next, scrollTo, and current index
│   ├── useCommonVariables.ts # Shared size and offset state
│   ├── useLayoutConfig.ts # Selects normal, parallax, or stack animations
│   ├── useOffsetX.ts # Loop-aware item positioning
│   ├── useOnProgressChange.ts # Progress callbacks and shared values
│   ├── usePanGestureProxy.ts # Gesture configuration
│   └── useVisibleRanges.tsx # Render-window calculation
├── layouts
│   ├── index.tsx
│   ├── normal.ts
│   ├── parallax.ts
│   └── stack.ts
├── store
│   └── index.tsx # Context for props, shared state, and measurements
├── utils # Pure offset, gesture, animation, and data helpers
├── index.tsx # Package exports
└── types.ts # Public prop and ref types

Development

  1. Run yarn install and yarn dev in the repository root.
  2. Run yarn install in example/app once.
  3. Start the example with yarn ios, yarn android, or yarn web from example/app.
  4. Modify files under src; the root watch task rebuilds the linked package.

Tips

How to add a new animation effect?

Use the customAnimation prop for animation effects that are not covered by the built-in layouts.

  1. Add a kebab-case demo route under example/app/app/demos/<category>/<name> by following a nearby example.
  2. Add or update the matching page under example/website/pages/Examples when the demo should appear in the documentation.
  3. Record the demo and run yarn gif from the repository root when a GIF asset is needed.
  4. Put generated media under assets and reference it from the example page.