diff --git a/Images/autoPlay.gif b/Images/autoPlay.gif new file mode 100644 index 0000000..ff633e6 Binary files /dev/null and b/Images/autoPlay.gif differ diff --git a/Images/vertical_render.gif b/Images/vertical_render.gif new file mode 100644 index 0000000..6581f44 Binary files /dev/null and b/Images/vertical_render.gif differ diff --git a/README.md b/README.md index 692791b..3e642a0 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,17 @@ yarn add react-native-app-intro-slider | ---------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ | | ![Basic example gif](Images/basic-example.gif) | ![showSkipButton example image](Images/skipbutton-example.jpg) | ![bottomButton example image](Images/bottomskipbutton-example.jpg) | + +`Vertical Render` | `Auto Play` +| ---------------------------------------------- | -------------------------------------------------------------- +| ![Basic example gif](Images/vertical_render.gif) | ![showSkipButton example image](Images/autoPlay.gif) | + The component is based on FlatList so usage is very similar. Pass a data-array to AppIntroSlider along with a `renderItem`-function: ```javascript import React from 'react'; import { StyleSheet } from 'react-native'; -import AppIntroSlider from 'react-native-app-intro-slider'; +import AppIntroSlider, { Orientation } from 'react-native-app-intro-slider'; const slides = [ { @@ -133,6 +138,7 @@ export default class App extends React.Component { return ( @@ -162,10 +168,12 @@ The component extends `FlatList` so all FlatList-props are valid. | dotStyle | `style` | {backgroundColor: 'rgba(0, 0, 0, .2)'} | Style of inactive pagination dots | | dotClickEnabled | `boolean` | `true` | Whether users can navigate using the pagination dots | | activeDotStyle | `style` | {backgroundColor: 'rgba(255, 255, 255, .9)'} | Style of active pagination dot | +| orientation | enum | HORIZONTAL | defines the orientation of the intro slides | | skipLabel | `string` | `Skip` | Custom label for Skip button | | doneLabel | `string` | `Done` | Custom label for Done button | | nextLabel | `string` | `Next` | Custom label for Next button | | prevLabel | `string` | `Back` | Custom label for Prev button | +| autoPlay | `boolean` | `false` | When true slides starts playing automatically | | showSkipButton | `boolean` | `false` | Enable to show a skip button to the left of pagination dots. When `bottomButton == true` the skip button is a small text under the full-width next button | | showPrevButton | `boolean` | `false` | Enable to show a previous button. If `showSkipButton` is true, the skip button will be displayed on the first page and prev button on subsequent one | | showNextButton | `boolean` | `true` | Disable to hide the next button | diff --git a/src/index.tsx b/src/index.tsx index a2f6a7c..babffbc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,12 @@ import mergeExtraData from './merge-extradata'; const isAndroidRTL = I18nManager.isRTL && Platform.OS === 'android'; + +export enum Orientation { + HORIZONTAL, + VERTICAL +} + type Props = { data: ItemT[]; renderItem: ( @@ -46,6 +52,8 @@ type Props = { showPrevButton: boolean; showSkipButton: boolean; bottomButton: boolean; + orientation: Orientation; + autoPlay: boolean, } & FlatListProps; type State = { @@ -75,6 +83,8 @@ export default class AppIntroSlider extends React.Component< showPrevButton: false, showSkipButton: false, bottomButton: false, + orientation: Orientation.HORIZONTAL, + autoPlay: false }; state = { width: 0, @@ -82,17 +92,42 @@ export default class AppIntroSlider extends React.Component< activeIndex: 0, }; flatList: FlatList | undefined; + activeInterval: any; + + componentDidMount() { + if (this.props.autoPlay) this.startScroll(); + } + + // Clear interval when user closes + componentWillUnmount() { + clearInterval(this.activeInterval); + } + + startScroll() { + this.activeInterval = setInterval(this.scroll, 2000); + } + + scroll = () => { + if (this.state.activeIndex < this.props.data.length - 1) { + this.goToSlide(this.state.activeIndex + 1) + } else { + clearInterval(this.activeInterval); + } + } goToSlide = (pageNum: number, triggerOnSlideChange?: boolean) => { + const sizeOfSlide = this.props.orientation === Orientation.HORIZONTAL + ? this.state.width + : this.state.height const prevNum = this.state.activeIndex; - this.setState({activeIndex: pageNum}); + this.setState({ activeIndex: pageNum }); this.flatList?.scrollToOffset({ - offset: this._rtlSafeIndex(pageNum) * this.state.width, + offset: this._rtlSafeIndex(pageNum) * sizeOfSlide, }); if (triggerOnSlideChange && this.props.onSlideChange) { this.props.onSlideChange(pageNum, prevNum); } - }; + } // Get the list ref getListRef = () => this.flatList; @@ -141,15 +176,22 @@ export default class AppIntroSlider extends React.Component< name: string, onPress?: (e: GestureResponderEvent) => void, ) => { + const leftButtonStyle = this.props.orientation === Orientation.VERTICAL + ? styles.topButtonContainer + : styles.leftButtonContainer + const rightButtonStyle = this.props.orientation === Orientation.VERTICAL + ? styles.bottomButtonContainer + : styles.rightButtonContainer const style = name === 'Skip' || name === 'Prev' - ? styles.leftButtonContainer - : styles.rightButtonContainer; + ? leftButtonStyle + : rightButtonStyle; return ( + style={this.props.bottomButton && styles.flexOne} + > {content} @@ -177,10 +219,10 @@ export default class AppIntroSlider extends React.Component< _renderDoneButton = () => this.props.showDoneButton && this._renderButton( - 'Done', - this.props.doneLabel, - this.props.onDone, - this.props.renderDoneButton, + 'Done', + this.props.doneLabel, + this.props.onDone, + this.props.renderDoneButton, ); _renderSkipButton = () => @@ -206,11 +248,16 @@ export default class AppIntroSlider extends React.Component< const primaryButton = isLastSlide ? this._renderDoneButton() : this._renderNextButton(); - + const containerStyle = this.props.orientation === Orientation.VERTICAL + ? styles.paginationContainerV + : styles.paginationContainerH + const dotsContainerStyle = this.props.orientation === Orientation.VERTICAL + ? styles.paginationDotsV + : styles.paginationDotsH return ( - - - + + + {this.props.data.length > 1 && this.props.data.map((_, i) => this.props.dotClickEnabled ? ( @@ -244,21 +291,27 @@ export default class AppIntroSlider extends React.Component< ); }; - _onMomentumScrollEnd = (e: {nativeEvent: NativeScrollEvent}) => { - const offset = e.nativeEvent.contentOffset.x; + _onMomentumScrollEnd = (e: { nativeEvent: NativeScrollEvent }) => { + const offset = this.props.orientation === Orientation.HORIZONTAL + ? e.nativeEvent.contentOffset.x + : e.nativeEvent.contentOffset.y; + const total = this.props.orientation === Orientation.HORIZONTAL + ? this.state.width + : this.state.height + // Touching very very quickly and continuous brings about // a variation close to - but not quite - the width. // That's why we round the number. // Also, Android phones and their weird numbers - const newIndex = this._rtlSafeIndex(Math.round(offset / this.state.width)); + const newIndex = this._rtlSafeIndex(Math.round(offset / total)); if (newIndex === this.state.activeIndex) { // No page change, don't do anything return; } const lastIndex = this.state.activeIndex; - this.setState({activeIndex: newIndex}); + this.setState({ activeIndex: newIndex }); this.props.onSlideChange && this.props.onSlideChange(newIndex, lastIndex); - }; + } _onLayout = ({nativeEvent}: LayoutChangeEvent) => { const {width, height} = nativeEvent.layout; @@ -268,7 +321,7 @@ export default class AppIntroSlider extends React.Component< // Set new scroll position const func = () => { this.flatList?.scrollToOffset({ - offset: this._rtlSafeIndex(this.state.activeIndex) * width, + offset: this._rtlSafeIndex(this.state.activeIndex) * (this.props.orientation === Orientation.VERTICAL ? height : width), animated: false, }); }; @@ -290,6 +343,7 @@ export default class AppIntroSlider extends React.Component< renderItem, data, extraData, + orientation, ...otherProps } = this.props; /* eslint-enable @typescript-eslint/no-unused-vars */ @@ -297,16 +351,18 @@ export default class AppIntroSlider extends React.Component< // Merge component width and user-defined extraData const extra = mergeExtraData(extraData, this.state.width); + const flatListStyle = orientation === Orientation.VERTICAL ? styles.flatListV : styles.flatListH + return ( (this.flatList = ref as FlatList)} data={this.props.data} - horizontal + horizontal={orientation === Orientation.HORIZONTAL} pagingEnabled showsHorizontalScrollIndicator={false} bounces={false} - style={styles.flatList} + style={flatListStyle} renderItem={this._renderItem} onMomentumScrollEnd={this._onMomentumScrollEnd} extraData={extra} @@ -327,29 +383,49 @@ const styles = StyleSheet.create({ flexOne: { flex: 1, }, - flatList: { + flatListH: { flex: 1, flexDirection: isAndroidRTL ? 'row-reverse' : 'row', }, - paginationContainer: { + flatListV: { + flex: 1, + }, + paginationContainerH: { position: 'absolute', bottom: 16, left: 16, right: 16, justifyContent: 'center', }, - paginationDots: { + paginationContainerV: { + position: 'absolute', + bottom: 16, + right: 8, + top: 16, + justifyContent: 'center', + }, + paginationSafeArea: { + flex : 1, + justifyContent: "center" + }, + paginationDotsH: { height: 16, margin: 16, flexDirection: isAndroidRTL ? 'row-reverse' : 'row', justifyContent: 'center', alignItems: 'center', }, - dot: { + paginationDotsV: { + width: 16, + margin: 10, + justifyContent: 'center', + alignItems: 'center', + }, + dot :{ width: 10, height: 10, borderRadius: 5, - marginHorizontal: 4, + margin : 5 }, leftButtonContainer: { position: 'absolute', @@ -359,6 +435,14 @@ const styles = StyleSheet.create({ position: 'absolute', right: 0, }, + topButtonContainer: { + position: 'absolute', + top: 0, + }, + bottomButtonContainer: { + position: 'absolute', + bottom: 0, + }, bottomButton: { flex: 1, backgroundColor: 'rgba(0, 0, 0, .3)',