Create a Vertical Carousel with FlatList in React Native — Expo?

Steve Blue
8 min readJul 9, 2023

--

This code represents a React Native component called VideoViews. It imports various components, libraries, and styles required to display and interact with videos.

Let’s break down the code step by step:

  1. Import statements:
// react
import React, {FC, useRef, useState} from 'react';

// modules
import {Dimensions, FlatList, Image, Text, TouchableHighlight, TouchableOpacity, View} from "react-native";
import {StackNavigationProp} from "@react-navigation/stack";
import {ResizeMode, Video} from "expo-av";
import {StackNavigatorParams} from "@config/navigator";
import {ViewToken} from "react-native/Libraries/Lists/VirtualizedList";

// assets
import {AntDesign} from '@expo/vector-icons';
import {Fontisto} from '@expo/vector-icons';
import {MaterialCommunityIcons} from '@expo/vector-icons';
import {Ionicons} from '@expo/vector-icons';
import {MaterialIcons} from '@expo/vector-icons';

// styles
import styles from './video-views.styles';

These statements import various components, icons, libraries, and styles used in the VideoViews component.

2. VideoViewsProps type declaration:

type VideoViewsProps = {
navigation: StackNavigationProp<StackNavigatorParams, "VideoViews">;
};

This type declaration specifies the type of the props expected by the VideoViews component. It includes a navigation prop of type StackNavigationProp from the @react-navigation/stack library.

3. videos array:

const videos = [
{
"description": "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://www.bigbuckbunny.org",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
"subtitle": "By Blender Foundation",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg",
"title": "Big Buck Bunny"
},
]

This array contains a list of video objects, each representing a video with properties like description, sources (URL of the video), subtitle, thumbnail URL, and title. This array is used to render the list of videos in the FlatList component later in the code.

4. VideoViews component declaration:

const VideoViews: FC<VideoViewsProps> = () => {
// Component implementation
};

This declares the VideoViews component as a functional component that accepts VideoViewsProps as its props. The component implementation follows inside the function body.

5. vidRef and currentVid state variables:

const vidRef = useRef(null);
const [currentVid, setCurrentVid] = useState<number>(0);

6. viewAbilityConfigCallbackPairs ref:

const viewAbilityConfigCallbackPairs = useRef(
({changed, viewableItems}: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => {
// Function implementation
}
);

This useRef hook creates a ref that holds a callback function. This function is called when the viewability of items in the FlatList changes. It receives two parameters: changed (the items that have changed their viewability) and viewableItems (the currently viewable items).

7. renderVids function:

const renderVids = ({item, index}: { item: any, index: number }) => {
// Function implementation
};

This function is responsible for rendering each video item in the FlatList. It takes an item and its index as parameters and returns the JSX (UI) for the video item.

8. Return statement:

return (
<View style={styles.container}>
<FlatList
data={videos}
renderItem={renderVids}
style={styles.page}
keyExtractor={(_, index) => index.toString()}
pagingEnabled
scrollEventThrottle={16}
viewabilityConfig={{
itemVisiblePercentThreshold: 95,
}}
onViewableItemsChanged={viewAbilityConfigCallbackPairs.current}
/>
</View>
);

This is the JSX that represents the UI of the VideoViews component. It contains a View component as the container and a FlatList component to render the list of videos. The FlatList component is configured with various props like data (the video array), renderItem (the function to render each video item), style (applied to the FlatList component itself), keyExtractor (to extract unique keys for each item), pagingEnabled (enables pagination), scrollEventThrottle (the rate at which scroll events are fired), viewabilityConfig (configures when an item is considered viewable), and onViewableItemsChanged (callback when the viewability changes).

Overall, the VideoViews component is responsible for rendering a list of videos using the FlatList component and providing video playback functionality with options like play/pause, video controls, and interaction buttons.

/** video-views.tsx */

// react
import React, {FC, useRef, useState} from 'react'

// modules
import {FlatList, Image, Text, TouchableHighlight, TouchableOpacity, View} from "react-native";
import {StackNavigationProp} from "@react-navigation/stack";
import {ResizeMode, Video} from "expo-av";
import {ViewToken} from "react-native/Libraries/Lists/VirtualizedList";

// assets
import {AntDesign} from '@expo/vector-icons';
import {Fontisto} from '@expo/vector-icons';
import {MaterialCommunityIcons} from '@expo/vector-icons';
import {Ionicons} from '@expo/vector-icons';
import {MaterialIcons} from '@expo/vector-icons';

// styles
import styles from './video-views.styles'

// navigator
import {StackNavigatorParams} from "@config/navigator";

type VideoViewsProps = {
navigation: StackNavigationProp<StackNavigatorParams, "VideoViews">;
};

// TODO move to dummy file
const videos = [
{
"description": "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://www.bigbuckbunny.org",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
"subtitle": "By Blender Foundation",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg",
"title": "Big Buck Bunny"
},
{
"description": "The first Blender Open Movie from 2006",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
"subtitle": "By Blender Foundation",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg",
"title": "Elephant Dream"
},
{
"description": "HBO GO now works with Chromecast -- the easiest way to enjoy online video on your TV. For when you want to settle into your Iron Throne to watch the latest episodes. For $35.\nLearn how to use Chromecast with HBO GO and more at google.com/chromecast.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
"subtitle": "By Google",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/mages/ForBiggerBlazes.jpg",
"title": "For Bigger Blazes"
},
{
"description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when Batman's escapes aren't quite big enough. For $35. Learn how to use Chromecast with Google Play Movies and more at google.com/chromecast.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
"subtitle": "By Google",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerEscapes.jpg",
"title": "For Bigger Escape"
},
{
"description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV. For $35. Find out more at google.com/chromecast.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
"subtitle": "By Google",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerFun.jpg",
"title": "For Bigger Fun"
},
{
"description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for the times that call for bigger joyrides. For $35. Learn how to use Chromecast with YouTube and more at google.com/chromecast.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
"subtitle": "By Google",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerJoyrides.jpg",
"title": "For Bigger Joyrides"
},
{
"description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when you want to make Buster's big meltdowns even bigger. For $35. Learn how to use Chromecast with Netflix and more at google.com/chromecast.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
"subtitle": "By Google",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerMeltdowns.jpg",
"title": "For Bigger Meltdowns"
},
{
"description": "Sintel is an independently produced short film, initiated by the Blender Foundation as a means to further improve and validate the free/open source 3D creation suite Blender. With initial funding provided by 1000s of donations via the internet community, it has again proven to be a viable development model for both open 3D technology as for independent animation film.\nThis 15 minute film has been realized in the studio of the Amsterdam Blender Institute, by an international team of artists and developers. In addition to that, several crucial technical and creative targets have been realized online, by developers and artists and teams all over the world.\nwww.sintel.org",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
"subtitle": "By Blender Foundation",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg",
"title": "Sintel"
},
{
"description": "Smoking Tire takes the all-new Subaru Outback to the highest point we can find in hopes our customer-appreciation Balloon Launch will get some free T-shirts into the hands of our viewers.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4",
"subtitle": "By Garage419",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/SubaruOutbackOnStreetAndDirt.jpg",
"title": "Subaru Outback On Street And Dirt"
},
{
"description": "Tears of Steel was realized with crowd-funding by users of the open source 3D creation tool Blender. Target was to improve and test a complete open and free pipeline for visual effects in film - and to make a compelling sci-fi film in Amsterdam, the Netherlands. The film itself, and all raw material used for making it, have been released under the Creatieve Commons 3.0 Attribution license. Visit the tearsofsteel.org website to find out more about this, or to purchase the 4-DVD box with a lot of extras. (CC) Blender Foundation - http://www.tearsofsteel.org",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
"subtitle": "By Blender Foundation",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/TearsOfSteel.jpg",
"title": "Tears of Steel"
},
{
"description": "The Smoking Tire heads out to Adams Motorsports Park in Riverside, CA to test the most requested car of 2010, the Volkswagen GTI. Will it beat the Mazdaspeed3's standard-setting lap time? Watch and see...",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4",
"subtitle": "By Garage419",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/VolkswagenGTIReview.jpg",
"title": "Volkswagen GTI Review"
},
{
"description": "The Smoking Tire is going on the 2010 Bullrun Live Rally in a 2011 Shelby GT500, and posting a video from the road every single day! The only place to watch them is by subscribing to The Smoking Tire or watching at BlackMagicShine.com",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4",
"subtitle": "By Garage419",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/WeAreGoingOnBullrun.jpg",
"title": "We Are Going On Bullrun"
},
{
"description": "The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.",
"sources": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
"subtitle": "By Garage419",
"thumb": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/WhatCarCanYouGetForAGrand.jpg",
"title": "What care can you get for a grand?"
}
]

const VideoViews: FC<VideoViewsProps> = () => {
const vidRef = useRef<Video>(null);

const [currentVid, setCurrentVid] = useState<number>(0);

const viewAbilityConfigCallbackPairs = useRef(
({changed, viewableItems}: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => {
// console.log("CHANGED", changed);
// console.log("VIEWABLE ITEMS", viewableItems);

// this sets the index of the video that is view able on the screen right now.
if (changed && changed[0] && changed[0].index) {
setCurrentVid(changed[0].index);
}

}
);

// TODO: adding item interface
const renderVids = ({item, index}: { item: any, index: number }) => {
const shouldPlay = index === currentVid
const handleOnPlayPausePress = () => {
// pausing.
if (index === currentVid) {
setCurrentVid(-1);
} else {
setCurrentVid(index);
}
};

return (
<View>
<TouchableHighlight onPress={handleOnPlayPausePress}>
<View style={styles.videos}>
<Video
ref={vidRef}
source={{
uri: item.sources,
}}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode={ResizeMode.CONTAIN}
shouldPlay={shouldPlay}
isLooping
style={styles.videos}
posterSource={{uri: item.thumb}}
usePoster
posterStyle={{
resizeMode: 'contain'
}}
/>
{!shouldPlay ?
<AntDesign
style={styles.pauseIcon}
name="caretright"
size={50}
color="white"
/> : null}
</View>
</TouchableHighlight>

<View style={styles.wrapContent}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subTitle}>{item.subtitle}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>

{/*TODO: implement action buttons, heart, comment, bookmark, share, thumb, ...,*/}
</View>
);
};


return (
<View style={styles.container}>
<FlatList
data={videos}
renderItem={renderVids}
style={styles.page}
keyExtractor={(_, index) => index.toString()}
pagingEnabled
scrollEventThrottle={16}
viewabilityConfig={{
itemVisiblePercentThreshold: 95,
}}
onViewableItemsChanged={viewAbilityConfigCallbackPairs.current}
/>
</View>
)
}

export default VideoViews
/** video-views.styles.ts */
import {Dimensions, StyleSheet} from "react-native";

const width = Dimensions.get("window").width;
const height = Dimensions.get("window").height;


const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black'
},
page: {
width: "100%",
},
videos: {
width: width,
height: height,
justifyContent: 'center'
},
pauseIcon: {
position: 'absolute',
alignSelf: 'center',
opacity: 0.8
},
wrapContent: {
position: 'absolute',
bottom: 30,
width: '70%'
},
title: {
color: 'white',
fontSize: 15,
fontWeight: 'bold'
},
subTitle: {
color: 'white',
fontSize: 12,
fontWeight: 'bold'
},
description: {
color: 'white',
fontSize: 12
}
})

export default styles

--

--

Steve Blue
Steve Blue

Written by Steve Blue

Experienced Mobile Application Developer with a demonstrated history of working in the computer software industry.

Responses (1)