Building a Gallery Carousel in React Native using Reanimated — II
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.
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 theisFirstCard
variable totrue
using thesetIsFirstCard()
. - If
scrollOffset
is equal to thescrollViewWidth-SCREEN_WIDTH
, it means we have scrolled to the end of the scroll view, so we set theisLastCard
variable totrue
using thesetIsLastCard()
.
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.
- 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, usingfindNextNearestMultiple()
, and scroll to the calculated position using thescrollTo
method.
— ThefindNextNearestMultiple()
takes in two parameters, thetargetNumber
and themultiple
. The quotient value is calculated by dividing the(targetNumber + 1)
by themultiple
, and then rounding it up usingMath.ceil()
. Multiplying this with a variablemultiple
we get the new next offset and use thescrollTo
function to move to the Next Card. - 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 thefindPreviousMultiple()
to calculate the previous nearest scroll offset.
— ThefindPreviousMultiple()
takes in two parameters, thetargetNumber
and themultiple
. The quotient value is calculated by dividing the(targetNumber -1)
by themultiple
, and then rounding it up usingMath.floor()
. Multiplying this with a variablemultiple
which is like a factor(CARD_WIDTH
). Multiplying this with a variablemultiple
we get the new previous offset and use thescrollTo
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.
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.
![](https://miro.medium.com/v2/resize:fill:388:388/1*d3pbDoFRhzcaj3MAyCuNeg.png)
![](https://miro.medium.com/v2/resize:fill:388:388/1*1qf3bUE3dZtAmyxK2BKlBQ.png)
![](https://miro.medium.com/v2/resize:fill:388:388/1*VV8gPr86lRi2wN-Zeb0gxQ.png)
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. :)