Implement Carousel using a React-Native ScrollView Component

March 15, 2020

20 min read

In this article, we will implement a carousel using React-Native ScrollView Component. Implementation will not be including any 3rd party carousel library. The basic purpose of the article to achieve pretty simple implementation. We are looking at the kind of following stuff.

What will we do through this article

  1. We will have a carousel which has 2 items on each page. A Carousel can be swipe left or right to see other item in carousel.
  2. Adjust images as per aspect ratio.
  3. In below-attached image, you can see only 2 images per carousel page. We will try to make it dynamic no of the item on the page and they will adjust based on no of the column you want to show on a carousel page.
  4. A Carousel will be also able to perform swipe next and previous using the external component as well, like as you can see next and previous button in the following image. Next button will swipe a carousel left and show you next page of the item and Prev button will swipe carousel right and show you the previous page of item.
  5. Handling next and prev possibilities for e.g if you are on the first page then you can't go to the prev page and if you are on last page of carousel then you cannot go to the next page.
Sample we will try to implement it.


Using React-Native ScrollView is not only the option. You will find lots of React-Native Carousel Component on internet. Here are couple of them:
1. react-native-snap-carousel
2. react-native-swiper

Configure ScrollView Component to work as a Carousel

From the above-attached image, you can identify that a carousel will be swiping horizontally. Before configuring scrollview let us review some of the property which will help us to do that. Those properties belong to scrollview.

  • horizontal: ScrollView default scroll direction is vertical that is up and down. For our carousel requirement, we need scroll direction horizontal that is left and right. For that, we need to add property horizontal as true which enables scrollview to scroll horizontally. Default: false
  • pagingEnabled : After adding horizontal property you will see that scrollview starts scrolling horizontally but that scroll is continued scroll. To provide a carousel kind of effect we need to pause scroll after screen size page. Scrollview stops on multiples of the scroll view's size when scrolling. this property can be used for horizontal pagination. we need to add this property as true. Default : false
  • showsHorizontalScrollIndicator : This property helps us to show/hide the scroll indicator. We don't want to show scroll indicator in a carousel so we will add this property with the false value. Default: true

Above mention 3 property will make you scroll view work like a carousel. Here is how the code looks like.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Dimensions } from 'react-native'
3import styles from './style'
4const { width } = Dimensions.get('window')
5
6export default class MovieHome extends Component {
7 render() {
8 return (
9 <View style={styles.container}>
10 <View style={styles.navBar}>
11 <Text style={styles.navBarTitle}>MOVIES</Text>
12 </View>
13 <ScrollView
14 horizontal
15 pagingEnabled
16 showsHorizontalScrollIndicator={false}
17 >
18 <View style={{ backgroundColor: "blue", flex: 1, width: width }}></View>
19 <View style={{ backgroundColor: "red", flex: 1, width: width }}></View>
20 <View style={{ backgroundColor: "yellow", flex: 1, width: width }}></View>
21 </ScrollView>
22 </View>
23 );
24 }
25}

Now let's start with things that we mention in starting of article.

Arrange two cards in one slide

In the above code snippet, you can see how we had configured ScrollView to work as a carousel. You can see 3 view components wrap inside scrollview means three slides are there.

Now our goal is two have 2 cards or tile on single slide means on three slider total 6 cards will be there. So let do some math to arrange two cards on a single slide and adjust them as per different screen size as well. I had attached an image in which I had mention what we want to achieve on a single slide.
1. Two cards.
2. The card has an in-between space of 15pt.
3. Also, 1st card has 15pt space on left and 2nd card had 15pt space on right.

So we need to calculate total space or padding that we need to leave for proper card arrangement. it's 15+15+15 = 45 pt

Now get the device total width so we can equally divide the width between two cards. We can get ScreenWidth from Dimensions as shown in below code snippet:

1import { Dimensions } from 'react-native'
2
3const { width: screenWidth, height: screenHeight } = Dimensions.get('window')

Now we have screen width so based on that we can identify width of each card on screen. Here we are planning to show 2 card in slide. So below code snippet for calculation.

1// screenWidth is import from Dimensions
2
3const imageWidth = screenWidth / 2
4
5// Right now we are considering image height static without considering aspect ratio. Don't worry I we will also explain how to set dynamic height based aspect ratio.
6
7const imageHeight = 250

Now we will be adding addition some style to look page proper. Here are the code snippets and screenshot; how it looks with the above calculation.

index.js

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Dimensions, Image } from 'react-native'
3import styles from './style'
4const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
5
6const imageWidth = (screenWidth) / 2
7export default class MovieHome extends Component {
8 render() {
9 return (
10 <View style={styles.container}>
11 <View style={styles.navBar}>
12 <Text style={styles.navBarTitle}>MOVIES</Text>
13 </View>
14 <ScrollView
15 contentContainerStyle={styles.scrollViewContainerStyle}
16 horizontal
17 pagingEnabled
18 showsHorizontalScrollIndicator={false}
19 >
20 <View style={{ width: screenWidth, flex: 1 }}>
21 <View style={{ width: imageWidth }}>
22 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg' }}
23style={{ width: imageWidth, height: 250 }} />
24 </View>
25 <View style={{ width: imageWidth }}>
26 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/z7FCF54Jvzv9Anxyf82QeqFXXOO.jpg' }}
27style={{ width: imageWidth, height: 250 }} />
28 </View>
29 </View>
30 </ScrollView>
31 </View>
32 )
33 }
34}

styles.js

1import { StyleSheet } from 'react-native'
2
3const styles = StyleSheet.create({
4 container: {
5 flex: 1,
6 justifyContent: 'center',
7 alignItems: 'center',
8 alignContent: 'center',
9 backgroundColor: '#FFFFFF',
10 },
11 navBar: {
12 width: '100%',
13 height: 70,
14 backgroundColor: '#0D0D0D',
15 alignContent: 'center',
16 justifyContent: 'center'
17 },
18 navBarTitle: {
19 color: 'white',
20 fontSize: 16,
21 fontWeight: "bold",
22 alignSelf: 'center'
23 },
24 scrollViewContainerStyle: {
25 alignContent: 'center',
26 justifyContent: 'center',
27 alignItems: 'center',
28 height: 600
29 }
30});
31
32export default styles

Here is a screenshot of how the above code will look on page.

In the above screenshot, we can see that card rendered vertically we need to arrange them horizontally. Here we add some style that allows to card arrangement horizontally. In the screenshot, I had added a background color to scrollview so you can see edges.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Dimensions, Image } from 'react-native'
3import styles from './style'
4const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
5
6const imageWidth = (screenWidth) / 2
7const imageHeight = 250
8export default class MovieHome extends Component {
9 render() {
10 return (
11 <View style={styles.container}>
12 <View style={styles.navBar}>
13 <Text style={styles.navBarTitle}>MOVIES</Text>
14 </View>
15 <ScrollView
16 ref={(ref) => { this.stepCarousel = ref }}
17 contentContainerStyle={styles.scrollViewContainerStyle}
18 horizontal
19 pagingEnabled
20 showsHorizontalScrollIndicator={false}
21 >
22 <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: screenWidth }}>
23 <View style={{ width: imageWidth }}>
24 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg' }}
25 style={{ width: imageWidth, height: imageHeight }} />
26 </View>
27 <View style={{ width: imageWidth }}>
28 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/z7FCF54Jvzv9Anxyf82QeqFXXOO.jpg' }}
29 style={{ width: imageWidth, height: imageHeight }} />
30 </View>
31 </View>
32 </ScrollView>
33 </View>
34 )
35 }
36}

You can see that both cards are fully covering screen we need to add also padding around and between card as well. Also, note that scrollview is also covering the full screen. We need to adjust screen scrollview and image height based on the aspect ratio of the device screen. Which we will cover in the next point. As of no we will add padding. Check out the updated snippet and screenshot.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Dimensions, Image, ToastAndroid } from 'react-native'
3import styles from './style'
4const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
5
6const cardPadding = 15
7const totalPadding = 15 * 3 // left-side, in-between, right-side
8const imageWidth = (screenWidth - totalPadding) / 2
9const imageHeight = 250
10export default class MovieHome extends Component {
11 render() {
12 return (
13 <View style={styles.container}>
14 <View style={styles.navBar}>
15 <Text style={styles.navBarTitle}>MOVIES</Text>
16 </View>
17 <ScrollView
18 ref={(ref) => { this.stepCarousel = ref }}
19 contentContainerStyle={styles.scrollViewContainerStyle}
20 horizontal
21 pagingEnabled
22 showsHorizontalScrollIndicator={false}
23 >
24 <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: screenWidth, paddingHorizontal: cardPadding }}>
25 <View style={{ width: imageWidth }}>
26 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg' }}
27 style={{ width: imageWidth, height: imageHeight }} />
28 </View>
29 <View style={{ width: imageWidth }}>
30 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/z7FCF54Jvzv9Anxyf82QeqFXXOO.jpg' }}
31 style={{ width: imageWidth, height: imageHeight }} />
32 </View>
33 </View>
34 </ScrollView>
35 </View>
36 )
37 }
38}

Adjust image based on aspect-ratio

In the preview section, you have seen that the image has static height and we had also fixed 2 cards on a single slide. Adjusting image height will allow you to set image height properly so that the image looks as per aspect ratio. This calculation will also allow us to add dynamic no of a card in a slide. This means how many cards we need on a slide that we can configure.

To adjust the image aspect ratio, first of all, you need to identify the image aspect ratio. I am considering that images will be having the same size as the aspect ratio. The image that we are using in the card has width: 1920 and height: 1280, here if we try to find out the aspect ratio of image them it will be 2:3. You can find out aspect ratio from here. So now we have our image aspect ratio we can now calculate image height based on that. Here is the code snippet of calculation.

1const cardPadding = 15
2const totalPadding = cardPadding * 3 // left-side, in-between, right-side
3const imageWidth = (screenWidth - totalPadding) / 2
4
5// we had calculated imageHeight based on imageWidth and image aspect ratio
6
7const imageHeight = (imageWidth / (2 / 3)) //2:3 aspect ratio

Here you can see two screen images. One screen has a static image height of 150 and the second screen has a dynamic height image based on above mention calculation.

Dynamic no of card on slide (calculation & configuration)

Now for example if you need three images in one slide then there will be simple calculation and you can do that easily. Here is code which help us to dynamically adjust card width, height and padding.

1const cardPerSlide = 2
2const cardPadding = 15
3const paddingAround = cardPadding * 2 // slide horizontal padding
4const cardBetweenPadding = cardPadding * (cardPerSlide - 1)
5const totalPadding = paddingAround + cardBetweenPadding
6const imageWidth = (screenWidth - totalPadding) / cardPerSlide
7const imageHeight = (imageWidth / (2 / 3))

Now just add one more image in ScrollView and make cardPerSlide to 3. And see how image are adjusted in slide without stretching or cutting any part of image. Here is the code snippet and screenshot.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Dimensions, Image, ToastAndroid } from 'react-native'
3import styles from './style'
4const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
5
6const cardPerSlide = 3
7const cardPadding = 15
8const paddingAround = cardPadding * 2 // slide horizontal padding
9const cardBetweenPadding = cardPadding * (cardPerSlide - 1)
10const totalPadding = paddingAround + cardBetweenPadding
11const imageWidth = (screenWidth - totalPadding) / cardPerSlide
12const imageHeight = (imageWidth / (2 / 3))
13export default class MovieHome extends Component {
14 render() {
15 return (
16 <View style={styles.container}>
17 <View style={styles.navBar}>
18 <Text style={styles.navBarTitle}>MOVIES</Text>
19 </View>
20 <ScrollView
21 ref={(ref) => { this.stepCarousel = ref }}
22 contentContainerStyle={styles.scrollViewContainerStyle}
23 horizontal
24 pagingEnabled
25 showsHorizontalScrollIndicator={false}
26 >
27 <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: screenWidth, paddingHorizontal: cardPadding }}>
28 <View style={{ width: imageWidth }}>
29 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg' }}
30 style={{ width: imageWidth, height: imageHeight }} />
31 </View>
32 <View style={{ width: imageWidth }}>
33 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/z7FCF54Jvzv9Anxyf82QeqFXXOO.jpg' }}
34 style={{ width: imageWidth, height: imageHeight }} />
35 </View>
36 <View style={{ width: imageWidth }}>
37 <Image source={{ uri: 'https://image.tmdb.org/t/p/w1280/tgcrYiyG75iDcyk3en9NzZis0dh.jpg' }} style={{ width: imageWidth, height: imageHeight }} />
38 </View>
39 </View>
40 </ScrollView>
41 </View>
42 )
43 }
44}

Swipe carousel using external buttons

As of now, our carousel is ready and we can swipe left and right to see a card on different slides. Sometimes we need also control over swiping cards means we should be able to change the carousel slide without swiping on it using external buttons. So in that section, we will be seeing how you can change cards using external buttons.

Before starting this section, I had performed a clean up on code. I had created one data file which has card images. I had created slide component that will be rendered through the loop and render slide based on the card count we have. Also written down logic to identify how many slides we can have in the carousel based on data we have. Also added two-button and it's style for this section. Please feel free to check out the code here on (Github).

Here are the code snippets of two main file and you can see how it looks after cleanup.

index.js

1import React, { Component } from 'react'
2import { View, Text, ScrollView, Image, TouchableOpacity } from 'react-native'
3import styles from './style'
4import { cardPerSlide } from './config'
5import CarouselSlide from './components/Slide'
6import movies from './data'
7
8export default class MovieHome extends Component {
9 render() {
10 const noOfSlides = Math.ceil(movies.length / cardPerSlide)
11 return (
12 <View style={styles.container}>
13 <View style={styles.navBar}>
14 <Text style={styles.navBarTitle}>MOVIES</Text>
15 </View>
16 <ScrollView
17 ref={(ref) => { this.stepCarousel = ref }}
18 contentContainerStyle={styles.scrollViewContainerStyle}
19 horizontal
20 pagingEnabled
21 showsHorizontalScrollIndicator={false}
22 decelerationRate={0}
23 >
24 {[...Array(noOfSlides)].map((e, i) => {
25 const startIndex = i + 1
26 const startPosition = ((startIndex + (startIndex - 1)) - 1)
27 const endPosition = (startIndex * 2)
28 return <CarouselSlide key={i} cards={movies.slice(startPosition, endPosition)} />
29 })}
30 </ScrollView>
31 <View style={styles.buttonContainer}>
32 <TouchableOpacity style={styles.button}>
33 <Text style={styles.buttonText}>Prev</Text>
34 </TouchableOpacity>
35 <TouchableOpacity style={[styles.button]}>
36 <Text style={styles.buttonText}>Next</Text>
37 </TouchableOpacity>
38 </View>
39
40 </View>
41 );
42 }
43}

slider.js

1import React, { Component } from 'react'
2import { View, Image } from 'react-native'
3import styles from './style'
4
5class CarouselSlide extends Component {
6 render() {
7 const { cards } = this.props
8 return <View style={styles.slide}>
9 {cards.map((card, index) => {
10 return <Image source={{ uri: card.url }} style={styles.imageCard} key={index} />
11 })}
12 </View>
13
14 }
15}
16
17export default CarouselSlide

Now to handle the carousel using a button we need to write down logic. First of all, we need to keep track of how many slides the carousel had and which slide is currently visible means index of the slide.

Two functions of scrollview will help us to identify our requirements.

  • onContentSizeChange: This function will return contentWidth and contentHeight which has been rendered by scrollview. From  this function, we will get total width of content to render by the carousel. We need only contentWidth and we can do the math on that to identify total no of cards in a carousel.

    Here are a few facts things based on our upper section calculation.
    1. Carousel one tile is covering device screenWidth means. 1 slide width = device screen width. Now we have a width of total content render by scrollview and we have width of a single slide. so we can easily identify total slide count using contentWidth/slideWidth which will provide us the number of slides.
1const approxSlide = contentWidth / screenWidth
  • onMomentumScrollEnd: Called when the momentum scroll ends (scroll which occurs as the ScrollView glides to a stop). We will use this function to identify the current slide after swipe. This function provides native events with x and y offset. It provides us scrollview x and y offset. So here we only need contentOffSet of x. We can divide contentOffset by device-width and you will get float number which is the current slide number you need to round of it and convert it to an integer.
1const approxCurrentSlide = nativeEvent.contentOffset.x / screenWidth
2currentSlide = parseInt(Math.ceil(approxCurrentSlide.toFixed(2)) + 1)

Here is full source code after managing state of current and total number of slides. And utilising above mention two function.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, TouchableOpacity, Dimensions } from 'react-native'
3import styles from './style'
4import { cardPerSlide } from './config'
5import CarouselSlide from './components/Slide'
6import movies from './data'
7
8const { width: screenWidth } = Dimensions.get('window')
9
10export default class MovieHome extends Component {
11
12 constructor(props) {
13 super(props)
14
15 // Initially state
16 this.state = {
17 totalSlide: 0,
18 currentSlide: 1
19 }
20 }
21
22 // function will find out total no of slide and set to state
23 setTotalSlides = (contentWidth) => {
24 const { totalSlide } = this.state
25 // contentWidth received from onContentSizeChange
26 if (contentWidth !== 0) {
27 const approxSlide = contentWidth / screenWidth
28 if (totalSlide !== parseInt(approxSlide)) {
29 this.setState({
30 totalSlide: parseInt(Math.ceil(approxSlide.toFixed(2)))
31 })
32 }
33 }
34 }
35
36 setCurrentSlide = (currentSlide) => {
37 this.setState({
38 currentSlide
39 })
40 }
41
42
43 // function will identify current slide visible on screen
44 // Also maintaining current slide on carousel swipe.
45 handleScrollEnd = (e) => {
46 if (!e) {
47 return
48 }
49 const { nativeEvent } = e
50 if (nativeEvent && nativeEvent.contentOffset) {
51 let currentSlide = 1
52 if (nativeEvent.contentOffset.x === 0) {
53 this.setCurrentSlide(currentSlide)
54 } else {
55 const approxCurrentSlide = nativeEvent.contentOffset.x / screenWidth
56 currentSlide = parseInt(Math.ceil(approxCurrentSlide.toFixed(2)) + 1)
57 this.setCurrentSlide(currentSlide)
58 }
59 }
60 }
61
62 render() {
63 const { totalSlide, currentSlide } = this.state
64 const noOfSlides = Math.ceil(movies.length / cardPerSlide)
65 return (
66 <View style={styles.container}>
67 <View style={styles.navBar}>
68 <Text style={styles.navBarTitle}>MOVIES</Text>
69 </View>
70 <ScrollView
71 ref={(ref) => { this.stepCarousel = ref }}
72 contentContainerStyle={styles.scrollViewContainerStyle}
73 horizontal
74 pagingEnabled
75 showsHorizontalScrollIndicator={false}
76 decelerationRate={0}
77 snapToAlignment={'center'}
78 onContentSizeChange={this.setTotalSlides}
79 onMomentumScrollEnd={this.handleScrollEnd}
80 >
81 {[...Array(noOfSlides)].map((e, i) => {
82 const startIndex = i + 1
83 const startPosition = ((startIndex + (startIndex - 1)) - 1)
84 const endPosition = (startIndex * 2)
85 return <CarouselSlide key={i} cards={movies.slice(startPosition, endPosition)} />
86 })}
87 </ScrollView>
88 <View style={styles.cardCountContainer}>
89 <Text style={styles.countText}>Total Slides : {totalSlide}</Text>
90 <Text style={styles.countText}>Current Slide : {currentSlide}</Text>
91 </View>
92 <View style={styles.buttonContainer}>
93 <TouchableOpacity style={styles.button}>
94 <Text style={styles.buttonText}>Prev</Text>
95 </TouchableOpacity>
96 <TouchableOpacity style={[styles.button]}>
97 <Text style={styles.buttonText}>Next</Text>
98 </TouchableOpacity>
99 </View>
100
101 </View>
102 );
103 }
104}

Now to change slide based on button click. We need to call the scrollview scrollTo method. For that, we need to have a carousel's reference, using that we can access scrollview events.

  • scrollTo(): Scrolls to a given x, y offset, either immediately, with a smooth animation. Here is the syntax of the scrollTo function. x offset is for scrolling horizontally and y offset is for scrolling vertically. We will be utilizing x offset because we need to swiper card horizontally.
1scrollTo({x: 0, y: 0, animated: true})

On the next button press, we need to scroll to the next slide. Here we need to write down logic to get x offset for the next slide so we can pass it in scrollTo function. And for the prev button click we need logic to get x offset for the previous slide.

As I had already explained how 1 slide width = device screen width. Here it will be useful. For the next slide, we can do currentSlide * screenWidth. It will give us offset for the next slide. Whatever value you will get is end offset of the current slide. Here is the code snippet which you can call on next button click.

1const scrollPoint = currentSlide * screenWidth
2
3// this.stepCarousel is refernce to scrollView
4this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })

Now we will see how we can get offset of the previous slide to scroll. For the previous slide, logic is the same but to understand is tricky. Here we will be deducting count 2 from the current slide. You may think it should be 1 why we are deducting 2. Here is the reason we need starting x offset of a slide to scrollTo. Then only it can slide on the screen. If we do 1 then it will not work properly in case you have a large no of slides. Here is the code snippet which you can call on prev button click.

1const pageToGo = currentSlide - 2
2const scrollPoint = (pageToGo) * screenWidth
3this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })

Here is full code that is working with the next and prev button.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, TouchableOpacity, Dimensions, Platform } from 'react-native'
3import styles from './style'
4import { cardPerSlide } from './config'
5import CarouselSlide from './components/Slide'
6import movies from './data'
7
8const { width: screenWidth } = Dimensions.get('window')
9
10export default class MovieHome extends Component {
11
12 constructor(props) {
13 super(props)
14
15 // Initially state
16 this.state = {
17 totalSlide: 0,
18 currentSlide: 1
19 }
20 }
21
22 // function will find out total no of slide and set to state
23 setTotalSlides = (contentWidth) => {
24 const { totalSlide } = this.state
25 // contentWidth received from onContentSizeChange
26 if (contentWidth !== 0) {
27 const approxSlide = contentWidth / screenWidth
28 if (totalSlide !== parseInt(approxSlide)) {
29 this.setState({
30 totalSlide: parseInt(Math.ceil(approxSlide.toFixed(2)))
31 })
32 }
33 }
34 }
35
36 setCurrentSlide = (currentSlide) => {
37 this.setState({
38 currentSlide
39 })
40 }
41
42
43 // function will identify current slide visible on screen
44 // Also maintaining current slide on carousel swipe.
45 handleScrollEnd = (e) => {
46 if (!e) {
47 return
48 }
49 const { nativeEvent } = e
50 if (nativeEvent && nativeEvent.contentOffset) {
51 let currentSlide = 1
52 if (nativeEvent.contentOffset.x === 0) {
53 this.setCurrentSlide(currentSlide)
54 } else {
55 const approxCurrentSlide = nativeEvent.contentOffset.x / screenWidth
56 currentSlide = parseInt(Math.ceil(approxCurrentSlide.toFixed(2)) + 1)
57 this.setCurrentSlide(currentSlide)
58 }
59 }
60 }
61
62 goToNext = () => {
63 const { currentSlide } = this.state
64 if (this.stepCarousel) {
65 const scrollPoint = currentSlide * screenWidth
66 this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })
67 // following condition is for android only because in android onMomentumScrollEnd doesn't
68 // call when we scrollContent with scroll view reference.
69 if (Platform.OS === 'android') {
70 this.handleScrollEnd({ nativeEvent: { contentOffset: { y: 0, x: scrollPoint } } })
71 }
72 }
73 }
74
75 goToPrev = () => {
76 const { currentSlide } = this.state
77 if (this.stepCarousel) {
78 const pageToGo = currentSlide - 2
79 const scrollPoint = (pageToGo) * screenWidth
80 this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })
81 // following condition is for android only because in android onMomentumScrollEnd doesn't
82 // call when we scrollContent with scrollview reference.
83 if (Platform.OS === 'android') {
84 this.handleScrollEnd({ nativeEvent: { contentOffset: { y: 0, x: scrollPoint } } })
85 }
86 }
87 }
88
89 render() {
90 const { totalSlide, currentSlide } = this.state
91 const noOfSlides = Math.ceil(movies.length / cardPerSlide)
92 return (
93 <View style={styles.container}>
94 <View style={styles.navBar}>
95 <Text style={styles.navBarTitle}>MOVIES</Text>
96 </View>
97 <ScrollView
98 ref={(ref) => { this.stepCarousel = ref }}
99 contentContainerStyle={styles.scrollViewContainerStyle}
100 horizontal
101 pagingEnabled
102 showsHorizontalScrollIndicator={false}
103 decelerationRate={0}
104 snapToAlignment={'center'}
105 onContentSizeChange={this.setTotalSlides}
106 onMomentumScrollEnd={this.handleScrollEnd}
107 >
108 {[...Array(noOfSlides)].map((e, i) => {
109 const startIndex = i + 1
110 const startPosition = ((startIndex + (startIndex - 1)) - 1)
111 const endPosition = (startIndex * 2)
112 return <CarouselSlide key={i} cards={movies.slice(startPosition, endPosition)} />
113 })}
114 </ScrollView>
115 <View style={styles.cardCountContainer}>
116 <Text style={styles.countText}>Total Slides : {totalSlide}</Text>
117 <Text style={styles.countText}>Current Slide : {currentSlide}</Text>
118 </View>
119 <View style={styles.buttonContainer}>
120 <TouchableOpacity style={styles.button} onPress={this.goToPrev}>
121 <Text style={styles.buttonText}>Prev</Text>
122 </TouchableOpacity>
123 <TouchableOpacity style={[styles.button]} onPress={this.goToNext}>
124 <Text style={styles.buttonText}>Next</Text>
125 </TouchableOpacity>
126 </View>
127
128 </View>
129 );
130 }
131}

Restrict next and previous button

Restriction on next and previous button means to identify is there a next slide to go or is there a prev slide to go. For example, if you are on 1st slide of the carousel then you can't scroll to previous and if you are on the last slide of the carousel then you can't go next. So in these cases, we need to make button disable or provide and visual feedback so the user can identify that there no more slide to go previous or not more slide to go next.

In this section, we will make a button disable when there is no possibility to swiper next or previous. We need to do that logically. For that, we need to additionally maintain 2 more variable states. That is:

1constructor(props) {
2 super(props)
3
4 // Initially state
5 this.state = {
6 totalSlide: 0,
7 currentSlide: 1,
8 isNext: false,
9 isPrev: false
10 }
11 }

Here is logic to calculate possibility of next and previous slide.

1calculateNextPrev = (totalPage, currentPage) => {
2 if (totalPage > currentPage) {
3 this.setNext(true)
4 }
5 if (currentPage === 1) {
6 this.setPrev(false)
7 }
8 if (currentPage === totalPage) {
9 this.setNext(false)
10 }
11 if (currentPage > 1) {
12 this.setPrev(true)
13 }
14 }

After implementing the next and previous calculation here is a screenshot. You can see the slide count, total slide and button style based on the current slide.

Here is final code snippet of carousel file in which we had cover all then mention 5 points.

1import React, { Component } from 'react'
2import { View, Text, ScrollView, TouchableOpacity, Dimensions, Platform } from 'react-native'
3import styles from './style'
4import { cardPerSlide } from './config'
5import CarouselSlide from './components/Slide'
6import movies from './data'
7
8const { width: screenWidth } = Dimensions.get('window')
9
10export default class MovieHome extends Component {
11
12 constructor(props) {
13 super(props)
14
15 // Initially state
16 this.state = {
17 totalSlide: 0,
18 currentSlide: 1,
19 isNext: false,
20 isPrev: false
21 }
22 }
23
24 // function will find out total no of slide and set to state
25 setTotalSlides = (contentWidth) => {
26 const { totalSlide, currentSlide } = this.state
27 // contentWidth received from onContentSizeChange
28 if (contentWidth !== 0) {
29 const approxSlide = contentWidth / screenWidth
30 if (totalSlide !== parseInt(approxSlide)) {
31 this.setState({
32 totalSlide: parseInt(Math.ceil(approxSlide.toFixed(2)))
33 })
34 this.calculateNextPrev(parseInt(approxSlide), currentSlide)
35 }
36 }
37 }
38
39 setCurrentSlide = (currentSlide) => {
40 this.setState({
41 currentSlide
42 })
43 }
44
45
46 // function will identify current slide visible on screen
47 // Also maintaining current slide on carousel swipe.
48 handleScrollEnd = (e) => {
49 if (!e) {
50 return
51 }
52 const { nativeEvent } = e
53 const { totalSlide} = this.state
54 if (nativeEvent && nativeEvent.contentOffset) {
55 let currentSlide = 1
56 if (nativeEvent.contentOffset.x === 0) {
57 this.setCurrentSlide(currentSlide)
58 } else {
59 const approxCurrentSlide = nativeEvent.contentOffset.x / screenWidth
60 currentSlide = parseInt(Math.ceil(approxCurrentSlide.toFixed(2)) + 1)
61 this.setCurrentSlide(currentSlide)
62 }
63 this.calculateNextPrev(totalSlide, currentSlide)
64 }
65 }
66
67 goToNext = () => {
68 const { currentSlide } = this.state
69 if (this.stepCarousel) {
70 const scrollPoint = currentSlide * screenWidth
71 this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })
72 // following condition is for android only because in android onMomentumScrollEnd doesn't
73 // call when we scrollContent with scroll view reference.
74 if (Platform.OS === 'android') {
75 this.handleScrollEnd({ nativeEvent: { contentOffset: { y: 0, x: scrollPoint } } })
76 }
77 }
78 }
79
80 goToPrev = () => {
81 const { currentSlide } = this.state
82 if (this.stepCarousel) {
83 const pageToGo = currentSlide - 2
84 const scrollPoint = (pageToGo) * screenWidth
85 this.stepCarousel.scrollTo({ x: scrollPoint, y: 0, animated: true })
86 // following condition is for android only because in android onMomentumScrollEnd doesn't
87 // call when we scrollContent with scrollview reference.
88 if (Platform.OS === 'android') {
89 this.handleScrollEnd({ nativeEvent: { contentOffset: { y: 0, x: scrollPoint } } })
90 }
91 }
92 }
93
94 setNext = (status) => {
95 const { isNext } = this.state
96 if (status !== isNext) {
97 this.setState({
98 isNext: status
99 })
100 }
101 }
102
103 setPrev = (status) => {
104 const { isPrev } = this.state
105 if (status !== isPrev) {
106 this.setState({
107 isPrev: status
108 })
109 }
110 }
111
112 calculateNextPrev = (totalPage, currentPage) => {
113 if (totalPage > currentPage) {
114 this.setNext(true)
115 }
116 if (currentPage === 1) {
117 this.setPrev(false)
118 }
119 if (currentPage === totalPage) {
120 this.setNext(false)
121 }
122 if (currentPage > 1) {
123 this.setPrev(true)
124 }
125 }
126
127 render() {
128 const { totalSlide, currentSlide, isNext, isPrev } = this.state
129 const noOfSlides = Math.ceil(movies.length / cardPerSlide)
130 return (
131 <View style={styles.container}>
132 <View style={styles.navBar}>
133 <Text style={styles.navBarTitle}>MOVIES</Text>
134 </View>
135 <ScrollView
136 ref={(ref) => { this.stepCarousel = ref }}
137 contentContainerStyle={styles.scrollViewContainerStyle}
138 horizontal
139 pagingEnabled
140 showsHorizontalScrollIndicator={false}
141 decelerationRate={0}
142 snapToAlignment={'center'}
143 onContentSizeChange={this.setTotalSlides}
144 onMomentumScrollEnd={this.handleScrollEnd}
145 >
146 {[...Array(noOfSlides)].map((e, i) => {
147 const startIndex = i + 1
148 const startPosition = ((startIndex + (startIndex - 1)) - 1)
149 const endPosition = (startIndex * 2)
150 return <CarouselSlide key={i} cards={movies.slice(startPosition, endPosition)} />
151 })}
152 </ScrollView>
153 <View style={styles.cardCountContainer}>
154 <Text style={styles.countText}>Total Slides : {totalSlide}</Text>
155 <Text style={styles.countText}>Current Slide : {currentSlide}</Text>
156 </View>
157 <View style={styles.buttonContainer}>
158 <TouchableOpacity style={[styles.button, !isPrev && styles.disable ]} onPress={this.goToPrev} disabled={!isPrev}>
159 <Text style={styles.buttonText}>Prev</Text>
160 </TouchableOpacity>
161 <TouchableOpacity style={[styles.button, !isNext && styles.disable]} onPress={this.goToNext} disabled={!isNext}>
162 <Text style={styles.buttonText}>Next</Text>
163 </TouchableOpacity>
164 </View>
165
166 </View>
167 );
168 }
169}
That's it folks in this article. I hope you all will like my article. Feel free to ask the question in the comment section. Also, share the article with other folks as well.

You will find the article source code in GitHub repo: here . Everything that we performed through this article has been pushed to the repo. Also, I had added a video showing how that step carousel works.

Thank you. I will be back with more interesting articles.