Stacking Cards in react native - reactjs

Currently I am learning react native and I have some problem while I am making stack of the cards.
Here is my code:
constructor(props) {
super(props);
const position = new Animated.ValueXY();
const panResponder = new PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (event, gesture) => {
position.setValue({ x: gesture.dx, y: gesture.dy })
},
});
this.state = { panResponder, position, index: 0 };
}
renderCards() {
if(this.state.index >= this.props.data.length) {
return this.props.renderNoMoreCard();
}
return this.props.data.map((item, i) => {
if( i < this.state.index) { return null}
if(i === this.state.index) {
return (
<Animated.View
key={item.id}
style={[ styles.cardStyle, this.getCardStyle()]}
{...this.state.panResponder.panHandlers}
>
{this.props.renderCard(item)}
</Animated.View>
);
}
return (
<Animated.View key={item.id} style={styles.cardStyle}>
{this.props.renderCard(item)}
</Animated.View>
);
}).reverse();
}
render() {
return(
<View>
{this.renderCards()}
</View>
);
}
}
const styles = {
cardStyle: {
position: 'absolute',
width: SCREEN_WIDTH,
top: 0,
}
};
export default Deck;
The title card 2 is showing instead of card 1 but when I swipe the card. Card 1 is removed from view and title is changed to 3.

The mistake was that while using PanResponder which is use to reconciles several touches into a single gesture. So,
I was declaring a object const panResponder = new PanResponder.create
but the code should be
const panResponder = PanResponder.create and by Removing this single word "new" it worked!

Related

React Native: Dynamic state allocation inside a .map loop giving error

My goal of this code:
Render some view elements with a loop
Inside the loop, set the state
On clicking the elements, update that value
Here is my code:
import React, {Component} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
maths: {},
};
}
prepareMaths = function() {
var count = 5;
var updateMath = key => {
var stateMaths = this.state.maths;
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths}, () => {
console.log(this.state.maths);
});
};
var stateMaths = this.state.maths;
return [...Array(count)].map((options, key) => {
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths});
return (
<TouchableOpacity
key={key}
onPress={() => updateMath(key)}
style={{
height: 100,
width: 200,
marginRight: 20,
marginBottom: 10,
backgroundColor: 'green',
}}>
<Text>{this.state.maths['position_' + key]}</Text>
</TouchableOpacity>
);
});
};
render() {
return (
<View>
<View>{this.prepareMaths()}</View>
</View>
);
}
}
I'm getting this error with this code:
I'm very confused. Because if I remove setState... code inside the loop, it's showing random maths naturally. But how? Since I'm using this.state.maths['position_' + key] on render. I really don't know how that data is generating.
Please help.
Thanks in advance.
Issues
State mutations
var stateMaths = this.state.maths; // <-- state
stateMaths['position_' + key] = Math.random(); // <-- mutation!!
Updating state in render function causes rerender. render is a pure function with zero side-effects
return [...Array(count)].map((options, key) => {
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths}); // <-- trigger rerender
Solution
Factor prepareMaths and updateMath into standalone utility functions
Convert maths state to array
Use componentDidMount to initialize state
Use componentDidUpdate to log updated state
Move the JSX from prepareMaths to render function for mapping from state
Updated component
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
maths: [],
};
}
componentDidMount() {
this.prepareMaths();
}
componentDidUpdate() {
console.log(this.state.maths);
}
updateMath = (key) => {
this.setState((prevState) => ({
maths: prevState.maths.map((el, i) => (i === key ? Math.random() : el)),
}));
};
prepareMaths = function () {
const count = 5;
this.setState({ maths: [...Array(count)].map(Math.random) });
};
render() {
const { maths } = this.state;
return (
<View>
<View>
{maths.map((value, key) => (
<TouchableOpacity
key={key}
onPress={() => this.updateMath(key)}
style={{
height: 100,
width: 200,
marginRight: 20,
marginBottom: 10,
backgroundColor: 'green',
}}>
<Text>{value}</Text>
</TouchableOpacity>
))}
</View>
</View>
);
}
}
Expo Snack Demo

How to pass down item from a flatlist to a grandchild?

I have a flatlist of items. When I pass the item to the renderItem component everything works perfectly fine. Then when I pass that exact same item to a child within my component responsible for rendering items there are problems.
Normally it works perfectly fine but if there are multiple list items and the one above it gets deleted, it loses proper functionality and becomes very buggy. I think the issue is because an item assumes a previous item's index for whatever reason the grandchild still thinks it is that item rather than it was a different item moving into that index.
My flatlist:
<FlatList
data={this.props.items}
extraData={this.props.items}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<TodoItem
todoItem={item}
/>
);
}}
/>
Then in the TodoItem this is how I pass item to the grandchild:
class TodoItem extends Component {
render() {
const todoItem = this.props.todoItem;
return (
<View>
<ItemSwipeRow
item={todoItem}
completeItem={this.props.deleteTodo}
>
Then in the itemSwipeRow this is how I receive the prop
import React, { Component } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { RectButton } from 'react-native-gesture-handler';
import Swipeable from 'react-native-gesture-handler/Swipeable';
import { Ionicons, MaterialCommunityIcons } from '#expo/vector-icons';
import { connect } from 'react-redux';
import { openNotesModal, openDateModal } from '../actions';
const AnimatedIcon = Animated.createAnimatedComponent(Ionicons);
class ItemSwipeRow extends Component {
constructor(props) {
super(props);
this.item = props.item;
}
renderLeftActions = (progress, dragX) => {
const trans = dragX.interpolate({
inputRange: [0, 50, 100, 101],
outputRange: [-20, 0, 0, 1],
});
return (
<RectButton style={styles.leftAction}>
<AnimatedIcon
name='md-checkmark'
color='#28313b'
size={45}
style={[
styles.actionText,
{
transform: [{ translateX: trans }],
},
]}
/>
</RectButton>
);
};
renderRightAction = (action, name, color, x, progress) => {
const trans = progress.interpolate({
inputRange: [0, 1],
outputRange: [x, 0],
});
const pressHandler = () => {
action(this.item);
};
return (
<Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
<RectButton
style={[styles.rightAction, { backgroundColor: color }]}
onPress={pressHandler}
>
<MaterialCommunityIcons
name={name}
size={35}
color='#28313b'
/>
</RectButton>
</Animated.View>
);
};
renderRightActions = progress => (
<View style={styles.rightSwipeButtons}>
{this.renderRightAction(
this.props.openDateModal, 'calendar', '#B8B8F3', 192, progress
)}
{this.renderRightAction(
this.props.openNotesModal, 'pencil', '#F0A202', 128, progress
)}
{this.renderRightAction(
this.props.openDateModal, 'bell', '#db5461', 64, progress
)}
</View>
);
updateRef = ref => {
this.currItem = ref;
};
close = () => {
if (this.currItem !== null) { this.currItem.close(); }
};
onSwipeableLeftOpen = () => {
this.props.completeItem();
this.close();
}
onSwipeableRightWillOpen = () => {
console.log(this.item.text); //tried passing in item here but didn't
} //work, instead of console logging on call it did every item # start
// then none when it was suppose to log it.
render() {
const { children } = this.props;
const { item } = this.props;
return (
<Swipeable
ref={this.updateRef}
friction={2}
leftThreshold={70}
rightThreshold={40}
renderLeftActions={this.renderLeftActions}
renderRightActions={this.renderRightActions}
onSwipeableLeftOpen={this.onSwipeableLeftOpen}
onSwipeableRightWillOpen={this.onSwipeableRightWillOpen}
>
{children}
</Swipeable>
);
}
}
const styles = StyleSheet.create({
leftAction: {
flex: 1,
backgroundColor: '#82ff9e',
justifyContent: 'center',
},
rightAction: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
rightSwipeButtons: {
width: 192,
flexDirection: 'row'
}
});
export default connect(null, { openNotesModal, openDateModal })(ItemSwipeRow);
My console logs prove the right item isn't always being rendered. The deleting of the item works properly however if an item is deleted and there is something under it the item that was under it assumes it was the item that was just deleted.
Any help with this would be greatly appreciated and I can provide more code if needed.
the code for deleting an item:
action that's sent on left swipe–
export const removeTodo = (item) => {
return {
type: REMOVE_TODO,
id: item.id
};
};
reducer action goes to–
case REMOVE_TODO: {
const newList = state.todos.filter(item => item.id !== action.id);
for (let i = 0, newId = 0; i < newList.length; i++, newId++) {
newList[i].id = newId;
}
return {
...state,
todos: newList
};
}
You could use destructuring assignment to copy the object, rather than refering to the same object:
const { todoItem } = this.props
further reading: https://medium.com/#junshengpierre/javascript-primitive-values-object-references-361cfc1cbfb0

FlatList renderItem not being highlighted when clicked the first time

Basically, I'm trying to setup a Flatlist in which multiple values can be selected.
My problem is with the styling of elements, when clicked the first time they don't get highlighted but when clicked the 2nd time they get highlighted.
FlatList Code
renderRow = ({item}) => (
<RowItem data={item} />
)
data = [
{
value: 'element1'
},
{
value: 'element2'
}
]
render(){
return (
<FlatList
data={this.data}
renderItem={this.renderRow}
keyExtractor={(item, index) => item + index}/>
)
}
RowItem Code
export default class RowItem extends React.Component {
state = {
isElementActive: false,
}
highlightElement = () => {
this.setState(prevState => ({
isElementActive: !prevState.isElementActive
}))
}
render() {
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={this.highlightElement}
style={[styles.container, this.state.isElementActive ? styles.activeContainer : styles.inactiveContainer]}>
<Text>{this.props.data.value}</Text>
</TouchableOpacity>
)
}
}
const styles = Stylesheet.create({
container: {
height: 100,
width: 300,
backgroundColor: 'red',
},
activeContainer: {
opacity: 0.7,
},
inactiveContainer: {
opacity: 1,
}
});
When clicking on the element once, the value of the isElementActive returns true (when I console.log it) but the styling "activeContainer" does not apply. However, when I click it again, the styling applies even though the value of isElementActive then becomes false.
By default the value starts off as false, and they are not highlighted (i.e. have opacity of 1) and for this reason I'm kind of confused when clicked the first time, the value of isElementActive changes but the styling does not apply.
I was able to make it work with setOpacityTo after the setState.
Working example: https://snack.expo.io/SJNSKQPIB
import React from 'react';
import {TouchableOpacity, FlatList, StyleSheet, Text} from 'react-native';
type State = {
active: boolean;
};
type Props = {
value: string;
};
class RowItem extends React.Component<Props, State> {
state = {
active: null,
};
ref = null;
highlightElement = () => {
this.setState(
prevState => ({
active: !prevState.active,
}),
() => {
this.ref.setOpacityTo(this.state.active ? 0.7 : 1);
},
);
};
render() {
return (
<TouchableOpacity
ref={ref => (this.ref = ref)}
onPress={this.highlightElement}
style={[styles.container]}>
<Text>{this.props.value}</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: 100,
backgroundColor: 'red',
},
});
export default class App extends React.Component {
data = [
{
value: 'element1',
},
{
value: 'element2',
},
];
render() {
return (
<FlatList
keyExtractor={(_, index) => index.toString()}
data={this.data}
renderItem={({item}) => <RowItem value={item.value} />}
/>
);
}
}

react native multiple video with Swiper component play pause issue

I have multiple videos in the swiper to show videos one by one, but all the videos are loaded and playing at the same time and audios are messed up, I want current video only play at a time.
import * as React from 'react';
import { Text, View, StyleSheet,Image, Dimensions } from 'react-native';
import { Constants } from 'expo';
import { Video } from 'expo';
import Swiper from './Swiper';
import InViewPort from './InViewport';
const screenWidth = Dimensions.get('window').width ;
const screenHeight = Dimensions.get('window').height;
export default class App extends React.Component {
constructor(props) {
super(props);
// Your source data
this.state = {
images: {},
muted : false,
paused: true,
};
this.player = Array();
this.onChangeImage = this.onChangeImage.bind(this);
}
videoError(err){
console.warn(err);
}
pauseVideo = () => {
var curr = this.state.currentIndex;
console.warn(curr);
if(this.player[curr]) {
this.setState({paused: true });
}
}
playVideo = () => {
var curr = this.state.currentIndex;
console.warn(curr);
if(this.player[curr]) {
this.setState({paused: false});
}
}
handlePlaying = (isVisible) => {
isVisible ? this.playVideo() : this.pauseVideo();
}
onChangeImage (index) {
this.setState({ currentIndex: index});
}
render() {
let items = Array.apply(null, Array(15)).map((v, i) => {
return {
id: i,
caption: i + 1,
source: { uri: 'http://placehold.it/200x200?text=' + (i + 1) },
dimension: '{ width: 150, height: 150 }',
};
});
return(
<View style={styles.DefaultView}>
<Swiper
showsPagination={false}
onIndexChanged={this.onChangeImage}
index={0}
>
{items.map((item, key) => {
if(key==1 || key ==5){
return (
<InViewPort onChange={this.handlePlaying} key={key}>
<Video onError={this.videoError}
muted={this.state.muted}
paused={this.state.paused}
source={{uri: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4' }}
style={styles.backgroundVideo}
ref={(ref) => {
this.player[key] = ref;
}}
controls={true}
/>
</InViewPort>
)
}else{
return(
<Image
resizeMode='contain'
style={{width:screenWidth, height: screenHeight}}
source={item.source}
key={key}
/>
)
}
})}
</Swiper>
</View>
)
}
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
flexDirection: 'row',
},
DefaultView: {
flex: 1,
backgroundColor: '#000',
width: screenWidth,
justifyContent:'center',
alignItems:'center'
},
iconContainer: {
flexDirection: "row",
justifyContent: "space-evenly",
width: 150,
},
backgroundVideo: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
width: screenWidth,
height: 300,
marginTop:'50%',
position:'absolute',
},
});
I need some idea on this, we have a player reference to be used, also swiper component have onIndexChanged which will trigger when we moved to next video, how we can link the reference of the player to onIndexChanged and when we do swipe how we make it current video only to play?
As per Andrew suggestion I have used InPortView component too determine the current view of swipe, but still I am not sure how to make reference for video elements to be used in the functions for play and pause the concern video.
Components used:
For video react-native-video
For Swiper : react-native-swiper
Updated Full code with Expo example : Expo Snack
So taking your snack. I managed to get it to work.
I moved the Video out into its own component and passed a few additional props to it, the index in the array and the currentIndex showing.
export default class App extends React.Component {
constructor(props) {
super(props);
// Your source data
this.state = {
images: {},
muted : false,
paused: true,
currentIndex: 0
};
}
onChangeImage = (index) => {
console.log('currentIndex ', index)
this.setState({ currentIndex: index});
}
render() {
let items = Array.apply(null, Array(15)).map((v, i) => {
return {
id: i,
caption: i + 1,
source: { uri: 'http://placehold.it/200x200?text=' + (i + 1) },
dimension: '{ width: 150, height: 150 }',
};
});
return(
<View style={styles.DefaultView}>
<Swiper
showsPagination={false}
onIndexChanged={this.onChangeImage}
index={0}
>
{items.map((item, key) => {
if(key==1 || key ==5){
return (
<VideoPlayer key={key} index={key} currentIndex={this.state.currentIndex}/>
)
}else{
return(
<Image
resizeMode='contain'
style={{width:screenWidth, height: screenHeight}}
source={item.source}
key={key}
/>
)
}
})}
</Swiper>
</View>
)
}
}
The video component uses react-native-inviewport to help handle whether or not it is in the viewport. However it doesn't play nicely with react-native-swiper but it is possible to get it to work.
export default class VideoPlayer extends React.Component {
pauseVideo = () => {
if(this.video) {
this.video.pauseAsync();
}
}
playVideo = () => {
if(this.video) {
this.video.playAsync();
}
}
handlePlaying = (isVisible) => {
this.props.index === this.props.currentIndex ? this.playVideo() : this.pauseVideo();
}
render() {
return (
<View style={styles.container}>
<InViewPort onChange={this.handlePlaying}>
<Video
ref={ref => {this.video = ref}}
source={{ uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4' }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode="cover"
shouldPlay
style={{ width: WIDTH, height: 300 }}
/>
</InViewPort>
</View>
)
}
}
When I used the InViewPort component alone it seemed to think that the video in position 6 was in the viewport and would play it. So what I use the InviewPort is to perform a check to compare the index of the video with the currentIndex if they match play the video otherwise pause. I suppose this could be updated to use componentDidUpdate to handle the changes in the props. However, additional checks will need to be performed when the component mounts so that it doesn't play the video.
Here is my snack with it working. https://snack.expo.io/#andypandy/swiper-video

How do I make a list (FlatList) automatically scroll through the elements using Animated?

I have a horizontal FlatList, where each time it reaches the end, it automatically adds new elements to the list, so it kind of is an infinite list. I want the app to scroll through the list by itself automatically, while the user must still be able to scroll back and forth. This is what I have to far
export default class ImageCarousel extends Component {
constructor(props) {
super(props);
this.scrollX = 0;
this.offset = new Animated.Value(0);
this.scrollTo = this.scrollTo.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.stopAnimation = this.stopAnimation.bind(this);
// Listener to call the scrollToOffset function
this.offset.addListener(this.scrollTo);
}
_scroller() {
toValue = this.scrollX + 10; // Scroll 10 pixels in each loop
this.animation = Animated.timing(
this.offset,
{
toValue: toValue,
duration: 1000, // A loop takes a second
easing: Easing.linear,
}
);
this.animation.start(() => this._scroller()); //Repeats itself when done
}
scrollTo(e) {
this.carousel.scrollToOffset({offset: e.value});
}
handleScroll(event) {
// Save the x (horizontal) value each time a scroll occurs
this.scrollX = event.nativeEvent.contentOffset.x;
}
componentDidMount() {
this._scroller();
}
render() {
return (
<View>
<FlatList
ref={el => this.carousel = el}
data={someData}
renderItem={renderFunction}
horizontal={true}
keyExtractor={someKeyFunction}
onEndReached={loadMoreElementsFunction}
onScroll={this.handleScroll}
/>
</View>
);
}
}
It works in the sense that it is automatically scrolling through the list, the problem however, is I cannot manually scroll through the list, since the scroll position is constantly updated by the scrollTo listener. I have tried to add an onPress callback to disable the animation when the FlatList is pressed, I have however not been able to get it to work.
This is my Data.
Blockquote
state = {
link: [
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
],};
Define FlatList Ref
flatList = createRef();
FlatList component
<FlatList
style={{flex: 1}}
data={this.state.link}
keyExtractor={this._keyExtractor.bind(this)}
renderItem={this._renderItem.bind(this)}
horizontal={true}
flatListRef={React.createRef()}
ref={this.flatList}
/>
Next slide
_goToNextPage = () => {
if (CurrentSlide >= this.state.link.length-1) CurrentSlide = 0;
this.flatList.current.scrollToIndex({
index: ++CurrentSlide,
animated: true,
});
};
Start and stop Interval
_startAutoPlay = () => {
this._timerId = setInterval(this._goToNextPage, IntervalTime);
};
_stopAutoPlay = () => {
if (this._timerId) {
clearInterval(this._timerId);
this._timerId = null;
}
};
Associated function
componentDidMount() {
this._stopAutoPlay();
this._startAutoPlay();
}
componentWillUnmount() {
this._stopAutoPlay();
}
_renderItem({item, index}) {
return <Image source={{uri: item}} style={styles.sliderItems} />;
}
_keyExtractor(item, index) {
return index.toString();
}
Full Code:
import React, {Component, createRef} from 'react';
import {
Text,
View,
ScrollView,
Image,
StyleSheet,
Dimensions,
FlatList,
} from 'react-native';
let CurrentSlide = 0;
let IntervalTime = 4000;
export default class Slider extends Component {
flatList = createRef();
// TODO _goToNextPage()
_goToNextPage = () => {
if (CurrentSlide >= this.state.link.length-1) CurrentSlide = 0;
this.flatList.current.scrollToIndex({
index: ++CurrentSlide,
animated: true,
});
};
_startAutoPlay = () => {
this._timerId = setInterval(this._goToNextPage, IntervalTime);
};
_stopAutoPlay = () => {
if (this._timerId) {
clearInterval(this._timerId);
this._timerId = null;
}
};
componentDidMount() {
this._stopAutoPlay();
this._startAutoPlay();
}
componentWillUnmount() {
this._stopAutoPlay();
}
// TODO _renderItem()
_renderItem({item, index}) {
return <Image source={{uri: item}} style={styles.sliderItems} />;
}
// TODO _keyExtractor()
_keyExtractor(item, index) {
// console.log(item);
return index.toString();
}
state = {
link: [
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
// 'https://picsum.photos/200/300',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
],
};
render() {
return (
<View style={{marginTop: 10, marginBottom: 10}}>
<FlatList
style={{
flex: 1,
// TODO Remove extera global padding
// marginLeft: -size.padding,
// marginRight: -size.padding,
}}
data={this.state.link}
keyExtractor={this._keyExtractor.bind(this)}
renderItem={this._renderItem.bind(this)}
horizontal={true}
flatListRef={React.createRef()}
ref={this.flatList}
/>
</View>
);
}
}
const styles = StyleSheet.create({
sliderItems: {
marginLeft: 5,
marginRight: 5,
height: 200,
width: Dimensions.get('window').width,
},
});
Just in case you're still not found the answer yet,
this is my approach to create autoscroll carousel using FlatList
import React, { Component } from 'react'
import {
StyleSheet,
View,
FlatList,
ScrollView,
Dimensions,
Image
} from 'react-native'
const { width } = Dimensions.get('window');
const height = width * 0.2844;
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
search: '',
sliderIndex: 0,
maxSlider: 2,
banners: [
{_id: 1, imageUrl: 'https://www.do-cart.com/img/slider/1.jpg'},
{_id: 2, imageUrl: 'https://www.do-cart.com/img/slider/2.jpg'},
{_id: 3, imageUrl: 'https://www.do-cart.com/img/slider/3.jpg'},
],
}
}
setRef = (c) => {
this.listRef = c;
}
scrollToIndex = (index, animated) => {
this.listRef && this.listRef.scrollToIndex({ index, animated })
}
componentWillMount() {
setInterval(function() {
const { sliderIndex, maxSlider } = this.state
let nextIndex = 0
if (sliderIndex < maxSlider) {
nextIndex = sliderIndex + 1
}
this.scrollToIndex(nextIndex, true)
this.setState({sliderIndex: nextIndex})
}.bind(this), 3000)
}
render() {
return (
<View style={styles.container}>
<View style={{height: 80, backgroundColor: '#123866', width:'100%'}}></View>
<ScrollView style={styles.scrollContainer} showsVerticalScrollIndicator={false}>
<FlatList
ref={this.setRef}
data={this.state.banners}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
keyExtractor={item => item._id}
renderItem={({item, i}) => (
<View key={i} style={{ height, width}}>
<Image style={{ height, width }} source={{ uri: item.imageUrl }} />
</View>
)}
onMomentumScrollEnd={(event) => {
let sliderIndex = event.nativeEvent.contentOffset.x ? event.nativeEvent.contentOffset.x/width : 0
this.setState({sliderIndex})
}}
/>
<View style={styles.sliderContainer}>
{
this.state.banners.map(function(item, index) {
return (
<View key={index} style={styles.sliderBtnContainer}>
<View style={styles.sliderBtn}>
{
this.state.sliderIndex == index ? <View style={styles.sliderBtnSelected}/> : null
}
</View>
</View>
)
}.bind(this))
}
</View>
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
scrollContainer: {
flex: 1
},
sliderContainer: {
flexDirection: 'row',
position: 'absolute',
top: 80,
alignSelf: 'center'
},
sliderBtn: {
height: 13,
width: 13,
borderRadius: 12,
borderWidth: 1,
borderColor: 'white',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10
},
sliderBtnSelected: {
height: 12,
width: 12,
borderRadius: 6,
backgroundColor: 'white',
},
sliderBtnContainer: {
flexDirection: 'row', marginBottom: 24
},
});
https://snack.expo.io/rJ9DOn0Ef
For those looking for a function-based component, this is my approach. The user can interact with the carousel and the automatic scroller will simply continue from the current slide.
The trick to achieving this is using an "onViewableItemsChanged" callback, where the "itemVisiblePercentThreshold" is >= 50%. This ensures the callback fires after the scroll to the new page is more than 50% complete (otherwise the automatic scroller triggers the callback to early and makes it scroll back).
import { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions } from "react-native";
import { FlatList, Image, StyleSheet } from "react-native";
const width = Dimensions.get("screen").width;
export const CarouselAutoScroll = ({ data, interval }) => {
const imageRef = useRef();
const [active, setActive] = useState(0);
const indexRef = useRef(active);
indexRef.current = active;
useInterval(() => {
if (active < Number(data?.length) - 1) {
setActive(active + 1);
} else {
setActive(0);
}
}, interval);
useEffect(() => {
imageRef.current.scrollToIndex({ index: active, animated: true });
}, [active]);
const onViewableItemsChangedHandler = useCallback(
({ viewableItems, changed }) => {
if (active != 0) {
setActive(viewableItems[0].index);
}
},
[]
);
return (
<FlatList
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={onViewableItemsChangedHandler}
viewabilityConfig={{
itemVisiblePercentThreshold: 50,
}}
ref={imageRef}
pagingEnabled
data={data}
horizontal
renderItem={({ item, index }) => (
<Image
key={index}
source={item.image}
resizeMode={"contain"}
style={{
flex: 1,
height: "100%",
width: width,
}}
/>
)}
style={{ ...StyleSheet.AbsoluteFill }}
/>
);
};
const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};

Resources