react-native-carousel-loop
Version:
A typescript based infinite carousel for react-native
402 lines • 16.7 kB
JavaScript
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, ScrollView, TouchableOpacity, View, TouchableWithoutFeedback, } from 'react-native';
import isEqual from 'lodash.isequal';
const PAGE_CHANGE_DELAY = 4000;
/**
* Animates pages in cycle
* (loop possible if children count > 1)
*/
let Carousel = /** @class */ (() => {
class Carousel extends Component {
constructor(props) {
super(props);
this._setCurrentPage = (currentPage) => {
this.setState({ currentPage }, () => {
if (this.props.onAnimateNextPage) {
// FIXME: called twice on ios with auto-scroll
this.props.onAnimateNextPage(currentPage);
}
});
};
this._onScrollBegin = () => {
this._clearTimer && this._clearTimer();
};
this._onScrollEnd = (event) => {
const offset = Object.assign({}, event.nativeEvent.contentOffset);
const page = this._calculateCurrentPage(offset.x);
this._placeCritical(page);
this._setCurrentPage(page);
this._setUpTimer();
};
this._onScroll = (event) => {
const currentOffset = event.nativeEvent.contentOffset.x;
const direction = currentOffset > this.offset ? 'right' : 'left';
this.offset = currentOffset;
const nextPage = this._calculateNextPage(direction);
if (this.nextPage !== nextPage) {
this.nextPage = nextPage;
if (this.props.onPageBeingChanged) {
this.props.onPageBeingChanged(this.nextPage);
}
}
};
this._onLayout = (event) => {
const { height, width } = event.nativeEvent.layout;
this.setState({ size: { width, height } });
// remove setTimeout wrapper when https://github.com/facebook/react-native/issues/6849 is resolved.
this._placeCritical(this.state.currentPage);
// setTimeout(() => this._placeCritical(this.state.currentPage as number), 0);
};
this._clearTimer = () => {
this.timer && clearTimeout(this.timer);
};
this._setUpTimer = () => {
// only for cycling
if (this.props.autoplay && React.Children.count(this.props.children) > 1) {
this._clearTimer();
this.timer = setTimeout(this._animateNextPage, this.props.delay);
}
};
this._scrollTo = ({ offset, animated, nofix }) => {
if (this.scrollView) {
this.scrollView.scrollTo({ y: 0, x: offset, animated });
// Fix bug #50
if (!nofix && Platform.OS === 'android' && !animated) {
this.scrollView.scrollTo({ y: 0, x: offset, animated: true });
}
}
};
this._animateNextPage = () => {
const { currentPage } = this.state;
const nextPage = this._normalizePageNumber(currentPage + 1);
// prevent from looping
if (!this.props.isLooped && nextPage < currentPage) {
return;
}
this.animateToPage(nextPage);
};
this._animatePreviousPage = () => {
const { currentPage } = this.state;
const nextPage = this._normalizePageNumber(currentPage - 1);
// prevent from looping
if (!this.props.isLooped && nextPage > currentPage) {
return;
}
this.animateToPage(nextPage);
};
this.animateToPage = (page) => {
const { currentPage, childrenLength, size: { width }, } = this.state;
const { isLooped } = this.props;
const nextPage = this._normalizePageNumber(page);
this._clearTimer();
if (nextPage === currentPage) {
// pass
}
else if (nextPage === 0) {
if (isLooped) {
// animate properly based on direction
if (currentPage !== childrenLength - 1) {
this._scrollTo({
offset: (childrenLength + 2) * width,
animated: false,
nofix: true,
});
}
this._scrollTo({ offset: childrenLength * width, animated: true });
}
else {
this._scrollTo({ offset: 0, animated: true });
}
}
else if (nextPage === 1) {
// To properly animate from the first page we need to move view
// to its original position first (not needed if not looped)
if (currentPage === 0 && isLooped) {
this._scrollTo({ offset: 0, animated: false, nofix: true });
}
this._scrollTo({ offset: width, animated: true });
}
else {
// Last page is allowed to jump to the first through the "border"
if (currentPage === 0 && nextPage !== childrenLength - 1) {
this._scrollTo({ offset: 0, animated: false, nofix: true });
}
this._scrollTo({ offset: nextPage * width, animated: true });
}
this._setCurrentPage(nextPage);
this._setUpTimer();
};
this._placeCritical = (page) => {
const { isLooped } = this.props;
const { childrenLength, size: { width }, } = this.state;
let offset = 0;
// if page number is bigger then length - something is incorrect
if (page < childrenLength) {
if (page === 0 && isLooped) {
// in "looped" scenario first page shold be placed after the last one
offset = childrenLength * width;
}
else {
offset = page * width;
}
}
this._scrollTo({ offset, animated: false });
};
this._normalizePageNumber = (page) => {
const { childrenLength } = this.state;
if (page === childrenLength) {
return 0;
}
else if (page > childrenLength) {
return 1;
}
else if (page < 0) {
return childrenLength - 1;
}
return page;
};
this._calculateCurrentPage = (offset) => {
const { width } = this.state.size;
const page = Math.round(offset / width);
return this._normalizePageNumber(page);
};
this._calculateNextPage = (direction) => {
const { width } = this.state.size;
const ratio = this.offset / width;
const page = direction === 'right' ? Math.ceil(ratio) : Math.floor(ratio);
return this._normalizePageNumber(page);
};
this._renderPageInfo = (pageLength) => (<View style={[styles.pageInfoBottomContainer, this.props.pageInfoBottomContainerStyle]} pointerEvents="none">
<View style={styles.pageInfoContainer}>
<View style={[styles.pageInfoPill, { backgroundColor: this.props.pageInfoBackgroundColor }]}>
<Text style={[styles.pageInfoText, this.props.pageInfoTextStyle]}>
{`${this.state.currentPage + 1}${this.props.pageInfoTextSeparator}${pageLength}`}
</Text>
</View>
</View>
</View>);
this._renderBullets = (pageLength) => {
const bullets = [];
for (let i = 0; i < pageLength; i += 1) {
bullets.push(<TouchableWithoutFeedback onPress={() => this.animateToPage(i)} key={`bullet${i}`}>
<View style={i === this.state.currentPage
? [styles.chosenBullet, this.props.chosenBulletStyle]
: [styles.bullet, this.props.bulletStyle]}/>
</TouchableWithoutFeedback>);
}
return (<View style={[styles.bullets, this.props.bulletsContainerStyle]} pointerEvents="box-none">
{bullets}
</View>);
};
this._renderArrows = () => {
let { currentPage } = this.state;
const { childrenLength } = this.state;
if (currentPage < 1) {
currentPage = childrenLength;
}
return (<View style={styles.arrows} pointerEvents="box-none">
<View style={[styles.arrowsContainer, this.props.arrowsContainerStyle]} pointerEvents="box-none">
<TouchableOpacity onPress={this._animatePreviousPage} style={this.props.arrowStyle}>
<Text style={this.props.leftArrowStyle}>
{this.props.leftArrowText ? this.props.leftArrowText : 'Left'}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this._animateNextPage} style={this.props.arrowStyle}>
<Text style={this.props.rightArrowStyle}>
{this.props.rightArrowText ? this.props.rightArrowText : 'Right'}
</Text>
</TouchableOpacity>
</View>
</View>);
};
const size = { width: 0, height: 0 };
if (props.children) {
const childrenLength = React.Children.count(props.children) || 1;
this.state = {
currentPage: props.currentPage,
size,
childrenLength,
contents: null,
};
}
else {
this.state = { size };
}
this.offset = 0;
this.nextPage = 0;
}
componentDidMount() {
if (this.state.childrenLength) {
this._setUpTimer();
}
}
componentWillUnmount() {
this._clearTimer && this._clearTimer();
}
componentDidUpdate({ children }) {
if (!isEqual(this.props.children, children)) {
const { currentPage } = this.state;
this._clearTimer && this._clearTimer();
let childrenLength = 0;
if (children) {
childrenLength = React.Children.count(children) || 1;
}
const nextPage = currentPage >= childrenLength ? childrenLength - 1 : currentPage;
this.setState({ childrenLength }, () => {
this.animateToPage(nextPage);
this._setUpTimer();
});
}
}
_setUpPages() {
const { size } = this.state;
const { children: propsChildren, isLooped, pageStyle } = this.props;
const children = React.Children.toArray(propsChildren);
const pages = [];
if (children && children.length > 1) {
// add all pages
pages.push(...children);
// We want to make infinite pages structure like this: 1-2-3-1-2
// so we add first and second page again to the end
if (isLooped) {
pages.push(children[0]);
pages.push(children[1]);
}
}
else if (children) {
pages.push(children[0]);
}
else {
pages.push(<View>
<Text>You are supposed to add children inside Carousel</Text>
</View>);
}
return pages.map((page, i) => (<View style={[Object.assign({}, size), pageStyle]} key={`page${i}`}>
{page}
</View>));
}
getCurrentPage() {
return this.state.currentPage;
}
render() {
const contents = this._setUpPages();
const { size, childrenLength } = this.state;
return (<View onLayout={this._onLayout} style={[this.props.style]}>
<ScrollView ref={(c) => {
this.scrollView = c;
}} onScrollBeginDrag={this._onScrollBegin} onMomentumScrollEnd={this._onScrollEnd} onScroll={this._onScroll} alwaysBounceHorizontal={false} alwaysBounceVertical={false} contentInset={{ top: 0 }} automaticallyAdjustContentInsets={false} showsHorizontalScrollIndicator={false} horizontal pagingEnabled bounces={false} scrollEnabled={this.props.swipe} contentContainerStyle={[
styles.horizontalScroll,
this.props.contentContainerStyle,
{
width: size.width * (childrenLength + (childrenLength > 1 && this.props.isLooped ? 2 : 0)),
height: size.height,
},
]}>
{contents}
</ScrollView>
{this.props.arrows && this._renderArrows()}
{this.props.bullets && this._renderBullets(childrenLength)}
{this.props.pageInfo && this._renderPageInfo(childrenLength)}
</View>);
}
}
Carousel.defaultProps = {
delay: PAGE_CHANGE_DELAY,
autoplay: false,
pageInfo: false,
bullets: false,
arrows: false,
pageInfoBackgroundColor: 'rgba(0, 0, 0, 0.25)',
pageInfoTextSeparator: ' / ',
currentPage: 0,
style: undefined,
pageStyle: undefined,
contentContainerStyle: undefined,
pageInfoTextStyle: undefined,
pageInfoBottomContainerStyle: undefined,
bulletsContainerStyle: undefined,
chosenBulletStyle: undefined,
bulletStyle: undefined,
arrowsContainerStyle: undefined,
arrowStyle: undefined,
leftArrowStyle: undefined,
rightArrowStyle: undefined,
leftArrowText: '',
rightArrowText: '',
onAnimateNextPage: undefined,
onPageBeingChanged: undefined,
swipe: true,
isLooped: true,
};
return Carousel;
})();
const styles = StyleSheet.create({
horizontalScroll: {
position: 'absolute',
},
pageInfoBottomContainer: {
position: 'absolute',
bottom: 20,
left: 0,
right: 0,
backgroundColor: 'transparent',
},
pageInfoContainer: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
},
pageInfoPill: {
width: 80,
height: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
pageInfoText: {
textAlign: 'center',
},
bullets: {
position: 'absolute',
left: 0,
right: 0,
bottom: 10,
height: 30,
backgroundColor: 'transparent',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
arrows: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
backgroundColor: 'transparent',
},
arrowsContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
chosenBullet: {
margin: 10,
width: 10,
height: 10,
borderRadius: 20,
backgroundColor: 'white',
},
bullet: {
margin: 10,
width: 10,
height: 10,
borderRadius: 20,
backgroundColor: 'transparent',
borderColor: 'white',
borderWidth: 1,
},
});
export default Carousel;
//# sourceMappingURL=index.js.map