react-native-app-intro-slider
Version:
Simple and configurable app introduction slider for react native
234 lines (233 loc) • 10.2 kB
JavaScript
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const React = __importStar(require("react"));
const react_native_1 = require("react-native");
const merge_extradata_1 = __importDefault(require("./merge-extradata"));
const isAndroidRTL = react_native_1.I18nManager.isRTL && react_native_1.Platform.OS === 'android';
class AppIntroSlider extends React.Component {
constructor() {
super(...arguments);
this.state = {
width: 0,
height: 0,
activeIndex: 0,
};
this.goToSlide = (pageNum, triggerOnSlideChange) => {
const prevNum = this.state.activeIndex;
this.setState({ activeIndex: pageNum });
this.flatList?.scrollToOffset({
offset: this._rtlSafeIndex(pageNum) * this.state.width,
});
if (triggerOnSlideChange && this.props.onSlideChange) {
this.props.onSlideChange(pageNum, prevNum);
}
};
// Get the list ref
this.getListRef = () => this.flatList;
// Index that works across Android's weird rtl bugs
this._rtlSafeIndex = (i) => isAndroidRTL ? this.props.data.length - 1 - i : i;
// Render a slide
this._renderItem = (flatListArgs) => {
const { width, height } = this.state;
const props = { ...flatListArgs, dimensions: { width, height } };
// eslint-disable-next-line react-native/no-inline-styles
return <react_native_1.View style={{ width, flex: 1 }}>{this.props.renderItem(props)}</react_native_1.View>;
};
this._renderButton = (name, label, onPress, render) => {
const content = render ? render() : this._renderDefaultButton(name, label);
return this._renderOuterButton(content, name, onPress);
};
this._renderDefaultButton = (name, label) => {
let content = <react_native_1.Text style={styles.buttonText}>{label}</react_native_1.Text>;
if (this.props.bottomButton) {
content = (<react_native_1.View style={[
name === 'Skip' || name === 'Prev'
? styles.transparentBottomButton
: styles.bottomButton,
]}>
{content}
</react_native_1.View>);
}
return content;
};
this._renderOuterButton = (content, name, onPress) => {
const style = name === 'Skip' || name === 'Prev'
? styles.leftButtonContainer
: styles.rightButtonContainer;
return (<react_native_1.View style={!this.props.bottomButton && style}>
<react_native_1.TouchableOpacity onPress={onPress} style={this.props.bottomButton && styles.flexOne}>
{content}
</react_native_1.TouchableOpacity>
</react_native_1.View>);
};
this._renderNextButton = () => this.props.showNextButton &&
this._renderButton('Next', this.props.nextLabel, () => this.goToSlide(this.state.activeIndex + 1, true), this.props.renderNextButton);
this._renderPrevButton = () => this.props.showPrevButton &&
this._renderButton('Prev', this.props.prevLabel, () => this.goToSlide(this.state.activeIndex - 1, true), this.props.renderPrevButton);
this._renderDoneButton = () => this.props.showDoneButton &&
this._renderButton('Done', this.props.doneLabel, this.props.onDone, this.props.renderDoneButton);
this._renderSkipButton = () =>
// scrollToEnd does not work in RTL so use goToSlide instead
this.props.showSkipButton &&
this._renderButton('Skip', this.props.skipLabel, () => this.props.onSkip
? this.props.onSkip()
: this.goToSlide(this.props.data.length - 1), this.props.renderSkipButton);
this._renderPagination = () => {
const isLastSlide = this.state.activeIndex === this.props.data.length - 1;
const isFirstSlide = this.state.activeIndex === 0;
const secondaryButton = (!isFirstSlide && this._renderPrevButton()) ||
(!isLastSlide && this._renderSkipButton());
const primaryButton = isLastSlide
? this._renderDoneButton()
: this._renderNextButton();
return (<react_native_1.View style={styles.paginationContainer}>
<react_native_1.SafeAreaView>
<react_native_1.View style={styles.paginationDots}>
{this.props.data.length > 1 &&
this.props.data.map((_, i) => this.props.dotClickEnabled ? (<react_native_1.TouchableOpacity key={i} style={[
styles.dot,
this._rtlSafeIndex(i) === this.state.activeIndex
? this.props.activeDotStyle
: this.props.dotStyle,
]} onPress={() => this.goToSlide(i, true)}/>) : (<react_native_1.View key={i} style={[
styles.dot,
this._rtlSafeIndex(i) === this.state.activeIndex
? this.props.activeDotStyle
: this.props.dotStyle,
]}/>))}
</react_native_1.View>
{primaryButton}
{secondaryButton}
</react_native_1.SafeAreaView>
</react_native_1.View>);
};
this._onMomentumScrollEnd = (e) => {
const offset = e.nativeEvent.contentOffset.x;
// 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));
if (newIndex === this.state.activeIndex) {
// No page change, don't do anything
return;
}
const lastIndex = this.state.activeIndex;
this.setState({ activeIndex: newIndex });
this.props.onSlideChange && this.props.onSlideChange(newIndex, lastIndex);
};
this._onLayout = ({ nativeEvent }) => {
const { width, height } = nativeEvent.layout;
if (width !== this.state.width || height !== this.state.height) {
// Set new width to update rendering of pages
this.setState({ width, height });
// Set new scroll position
const func = () => {
this.flatList?.scrollToOffset({
offset: this._rtlSafeIndex(this.state.activeIndex) * width,
animated: false,
});
};
setTimeout(func, 0); // Must be called like this to avoid bugs :/
}
};
}
render() {
// Separate props used by the component to props passed to FlatList
/* eslint-disable @typescript-eslint/no-unused-vars */
const { renderPagination, activeDotStyle, dotStyle, skipLabel, doneLabel, nextLabel, prevLabel, renderItem, data, extraData, ...otherProps } = this.props;
/* eslint-enable @typescript-eslint/no-unused-vars */
// Merge component width and user-defined extraData
const extra = merge_extradata_1.default(extraData, this.state.width);
return (<react_native_1.View style={styles.flexOne}>
<react_native_1.FlatList ref={(ref) => (this.flatList = ref)} data={this.props.data} horizontal pagingEnabled showsHorizontalScrollIndicator={false} bounces={false} style={styles.flatList} renderItem={this._renderItem} onMomentumScrollEnd={this._onMomentumScrollEnd} extraData={extra} onLayout={this._onLayout}
// make sure all slides are rendered so we can use dots to navigate to them
initialNumToRender={data.length} {...otherProps}/>
{renderPagination
? renderPagination(this.state.activeIndex)
: this._renderPagination()}
</react_native_1.View>);
}
}
exports.default = AppIntroSlider;
AppIntroSlider.defaultProps = {
activeDotStyle: {
backgroundColor: 'rgba(255, 255, 255, .9)',
},
dotStyle: {
backgroundColor: 'rgba(0, 0, 0, .2)',
},
dotClickEnabled: true,
skipLabel: 'Skip',
doneLabel: 'Done',
nextLabel: 'Next',
prevLabel: 'Back',
showDoneButton: true,
showNextButton: true,
showPrevButton: false,
showSkipButton: false,
bottomButton: false,
};
const styles = react_native_1.StyleSheet.create({
flexOne: {
flex: 1,
},
flatList: {
flex: 1,
flexDirection: isAndroidRTL ? 'row-reverse' : 'row',
},
paginationContainer: {
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
justifyContent: 'center',
},
paginationDots: {
height: 16,
margin: 16,
flexDirection: isAndroidRTL ? 'row-reverse' : 'row',
justifyContent: 'center',
alignItems: 'center',
},
dot: {
width: 10,
height: 10,
borderRadius: 5,
marginHorizontal: 4,
},
leftButtonContainer: {
position: 'absolute',
left: 0,
},
rightButtonContainer: {
position: 'absolute',
right: 0,
},
bottomButton: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, .3)',
alignItems: 'center',
justifyContent: 'center',
},
transparentBottomButton: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
color: 'white',
fontSize: 18,
padding: 12,
},
});