Building a Gallery Carousel in React Native using Reanimated — II

Karthik Balasubramanian
Timeless
6 min readAug 31, 2023

--

Hey all!

This blog is a continuation of my previous blog. Check it out here to get more context on what Interaction we are trying to build.

Let us continue to code and piece in our Interaction.

Cover Image

Scroll Handling using useAnimatedScrollHandler()

The scroll handler manages the events and animations in our horizontal scrolling view aka Carousel, using an animated value scrollXOffset.

const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
scrollXOffset.value = withSpring(event.contentOffset.x, {
damping: 18,
stiffness: 120,
});
},
onMomentumEnd: event => {
const scrollOffset = event.contentOffset.x;
scrollXOffset.value = withSpring(scrollOffset, {
damping: 18,
stiffness: 120,
});
if (scrollOffset === 0) {
runOnJS(setIsFirstCard)(true);
} else {
runOnJS(setIsFirstCard)(false);
}
if (scrollOffset === scrollViewWidth - SCREEN_WIDTH) {
runOnJS(setIsLastCard)(true);
} else {
runOnJS(setIsLastCard)(false);
}
},
});

The onScroll callback defines what should happen when the scroll view is scrolled, and it continuously triggered when the user is scrolling the content.

The x-offset value is set to the scrollXOffset SharedValue variable, using the withSpring() from Reanimated.

This variable will be used to animate our cards.

The onMomentumEnd callback is called when the scrolling ends, we set the scrollXOffset and do the following conditional checks:

  • If the scrollOffset is 0, it means that we have scrolled to the beginning or leftmost edge of the scroll view, so we set the isFirstCard variable to true using the setIsFirstCard().
  • If scrollOffset is equal to the scrollViewWidth-SCREEN_WIDTH, it means we have scrolled to the end of the scroll view, so we set the isLastCard variable to true using the setIsLastCard().

These checks manage the state of the navigation controls, which helps us to move the carousel without trying to scroll.

Let's look at their onPress callbacks.

Navigation Controls Function Definitions

Here we have a couple of functions which scroll the view to the next/previous card. We would require some helper functions too. Let's look at the code now.

// Helper Functions

const findNextNearestMultiple = (
targetNumber: number,
multiple: number,
): number => {
const quotient = Math.ceil((targetNumber + 1) / multiple);
const nextNearestMultiple = multiple * quotient;
return nextNearestMultiple;
};

const findPreviousMultiple = (
targetNumber: number,
multiple: number,
): number => {
const quotient = Math.floor((targetNumber - 1) / multiple);
const previousMultiple = multiple * quotient;
return previousMultiple;
};


// Navigation Controls

const goToNext = () => {
if (scrollXOffset.value < scrollViewWidth - SCREEN_WIDTH) {
const nextMultiple = findNextNearestMultiple(
scrollXOffset.value,
CARD_WIDTH,
);

scrollRef.current?.scrollTo({ x: nextMultiple, animated: true });
}
};

const goToPrevious = () => {
if (scrollXOffset.value !== 0) {
const nextMultiple = findPreviousMultiple(
scrollXOffset.value,
CARD_WIDTH,
);

scrollRef.current?.scrollTo({ x: nextMultiple, animated: true });
}
};

There are two navigation control functions which use two other helper functions, let me break you down what's happening.

  1. goToNext() — This function is called to navigate to the next card in the scroll view.
    — The function first checks if the scroll offset (scrollXOffset) value is less than the (scrollViewWidth — SCREEN_WIDTH), if no, the card is already at the end of the list and there is no further scrolling possible. If yes, we calculate the next nearest scroll offset, using findNextNearestMultiple(), and scroll to the calculated position using the scrollTo method.
    — The findNextNearestMultiple() takes in two parameters, the targetNumber and the multiple. The quotient value is calculated by dividing the (targetNumber + 1) by the multiple, and then rounding it up using Math.ceil(). Multiplying this with a variable multiple we get the new next offset and use the scrollTo function to move to the Next Card.
  2. goToPrevious() — This function is called to navigate to the previous card in the scroll view.
    — This function checks if the scroll offset is equal to 0, which prevents the scrolling before the first element. It uses the findPreviousMultiple() to calculate the previous nearest scroll offset.
    — The findPreviousMultiple() takes in two parameters, the targetNumber and the multiple. The quotient value is calculated by dividing the (targetNumber -1) by the multiple, and then rounding it up using Math.floor(). Multiplying this with a variable multiple which is like a factor(CARD_WIDTH). Multiplying this with a variable multiple we get the new previous offset and use the scrollTo function to move to the previous Card.

Phew, that was a lot of same code explanation. 😪

Gallery Carousel Item Component

You would have seen this component inside the ScrollView.

<GalleryCarouselItem
key={index}
{...{
item,
index,
scrollXOffset,
handleCarouselItemPress,
scrollViewWidth,
}}
/>

It takes in a prop handleCarouselItemPress, a callback function to handle when the Card in the horizontal scroll view is pressed.

const handleCarouselItemPress = (scrollOffset: number) => {
scrollRef.current?.scrollTo({ x: scrollOffset, animated: true });
};

It accepts a scrollOffset value as a parameter, and scrolls the Carousel to the value.

The component is simple, returns an Animated.View component wrapped inside a Pressable component. And this view has only an Image component.

The Pressable component takes in one prop, the onPress which calculates the scrollOffset of the pressed item and calls the handleCarouselItemPress() with the same.

const handlePress = useCallback(() => {
handleCarouselItemPress(
index === carouselItems.length - 1
? scrollViewWidth - SCREEN_WIDTH
: index * CARD_WIDTH,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

The major part of the component is the animatedStyle applied to the Animated.View which wraps the Image component.

const inputRange = useMemo(() => {
return [
(index - 1) * CARD_WIDTH,
index === carouselItems.length - 1
? scrollViewWidth - SCREEN_WIDTH
: index * CARD_WIDTH,
(index + 1) * CARD_WIDTH,
];
}, [index, scrollViewWidth]);

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
rotateY: `${interpolate(
scrollXOffset.value,
inputRange,
[-40, 0, 40],
)}deg`,
},
{
scale: interpolate(scrollXOffset.value, inputRange, [0.9, 1, 0.9]),
},
{
translateX: interpolate(
scrollXOffset.value,
inputRange,
[-20, 0, 20],
),
},
],
};
}, [inputRange, scrollViewWidth]);

The inputRange is an array of scrollOffset values, the indexes mapping to the previous card, current card and the next card.

We apply the transformations to the previous and next card so that there is a great appear animation when we scroll either ways. We scale down and apply an X-translate, also rotate along the Y-axis with an angle of 40', with which we end up with an animation like this:

You can see there is something wrong, let's add some perspective. 😉

{ perspective: 800 }

Adding this, we would get a really cool transition animation.

Final Carousel Interaction

With this, we wrap this blog up!

See you next month with a new Animation/Interaction blog!

This blog is part of the UI Interaction List, take a look at my other blogs on various other Interactions here.

UI Interactions With React Native and Reanimated

26 stories

This is Karthik from Timeless

I hope you found this blog post helpful and informative. If you have any feedback or suggestions, please leave a comment below. I’d love to hear your thoughts and opinions on the topic.

And if you have any ideas for future blog posts, please let me know! I’m constantly looking for new and interesting topics to explore.

Thank you for reading, and I look forward to hearing from you. :)

--

--