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.
- First we have three images by default and have slid to the second image in the middle
2. When we drag the image to the right


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

- 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.

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.
-
First we need a unit
sizeto help us calculate the scroll distance. Thissizeis the page size:- If
itemWidth/itemHeightis provided,sizeuses that. - Otherwise,
sizefalls back to the container axis size (preferstyle.width/style.height, or legacywidth/height, or layout measurement).
- If
-
Then we need a value
handlerOffset, which is the current scroll distance, and it's a total value, if we scroll two,handlerOffsetis equal tosize* 2, if we scroll ten,handlerOffsetis equal tosize* 10. -
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.

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

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

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
);- 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 typesDevelopment
- Run
yarn installandyarn devin the repository root. - Run
yarn installinexample/apponce. - Start the example with
yarn ios,yarn android, oryarn webfromexample/app. - 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.
- Add a kebab-case demo route under
example/app/app/demos/<category>/<name>by following a nearby example. - Add or update the matching page under
example/website/pages/Exampleswhen the demo should appear in the documentation. - Record the demo and run
yarn giffrom the repository root when a GIF asset is needed. - Put generated media under
assetsand reference it from the example page.