import * as React from 'react'; import { Animated, InteractionManager, ScrollView, StyleSheet, View, ViewStyle, } from 'react-native'; import { swiper } from '../../_styles/themes/default.components'; export interface IMDSwiperProps { styles?: IMDSwiperStyle; autoplay?: number; transition?: SwiperTrans; defaultIndex?: number; hasDots?: boolean; isLoop?: boolean; dragable?: boolean; width: number; height: number; onBeforeChange?: (from: number, to: number) => void; onAfterChange?: (from: number, to: number) => void; } interface IMDSwiperState { opacityAnim: Animated.AnimatedValue; opacitys: Animated.AnimatedValue[]; userScrolling: boolean; index: number; fromIndex: number; toIndex: number; firstIndex: number; lastIndex: number; oItemCount: number; // original item count rItemCount: number; // real item count dimension: number; timer: any; noDrag: boolean; isStoped: boolean; autoplay: number; ready: boolean; } export interface IMDSwiperStyle { wrapper?: ViewStyle; dots?: ViewStyle; dotsVertical?: ViewStyle; dot?: ViewStyle; dotVertical?: ViewStyle; dotActive?: ViewStyle; } type SwiperTrans = 'slide' | 'slideY' | 'fade'; export const MDSwiperStyles: IMDSwiperStyle = { wrapper: { justifyContent: 'center', position: 'relative', overflow: 'hidden', }, dots: { position: 'absolute', bottom: 10, flexDirection: 'row', alignSelf: 'center', }, dotsVertical: { position: 'absolute', flexDirection: 'column', alignSelf: 'center', right: 10, }, dot: { width: 8, height: 2, backgroundColor: '#ddd', marginHorizontal: 1.5, }, dotVertical: { width: 2, height: 8, backgroundColor: '#ddd', marginVertical: 1.2, }, dotActive: { backgroundColor: swiper.indicatorFill, }, }; const styles = StyleSheet.create(MDSwiperStyles); const addPropsToSwiperItem = ( children: any, width: number, height: number, backup: boolean, transition: string, key: any ): React.ReactNode | React.ReactNode[] => { if (!children) { return null; } if (React.Children.count(children) > 0) { const _children = []; // 用于循环,在最前面复制最后一条数据 backup && _children.push( renderSwiperItem( children[children.length - 1], width, height, transition, -1 ) ); React.Children.forEach(children, (child, index) => { _children.push( renderSwiperItem(child, width, height, transition, index) ); }); // 用于循环,在最后面复制第一条数据 backup && _children.push( renderSwiperItem( children[0], width, height, transition, children.length ) ); return _children; } return children; }; const renderSwiperItem = ( children: any, width: number, height: number, transition: string, key: any ): React.ReactNode | React.ReactNode[] => { if (children.props.isSwiperItem) { // 有 type 属性,代表是一个 React.ReactNode const _style: ViewStyle = { width, height, }; if (transition === 'slideY') { const _styleWrapper: ViewStyle = { height: width, width: height, justifyContent: 'center', alignItems: 'center', overflow: 'hidden', }; return ( {children} ); } return ( {children} ); } }; const addAnimToSwiperItem = ( children: any, width: number, height: number, opacitys: Animated.AnimatedValue[], key: any ): React.ReactNode | React.ReactNode[] => { if (!children) { return null; } if (children.type && children.type.name === 'MDSwiperItem') { // 有 type 属性,代表是一个 React.ReactNode const _style: ViewStyle = { width, height, position: 'absolute', top: 0, left: 0, }; return ( {children} ); } else if (React.Children.count(children) > 0) { const _children: any[] = []; React.Children.forEach(children, (child, index) => { _children.push(addAnimToSwiperItem(child, width, height, opacitys, index)); }); return _children; } return children; }; export default class MDSwiperCommon extends React.Component< IMDSwiperProps, IMDSwiperState > { public static defaultProps = { styles, autoplay: 3000, transition: 'slide', defaultIndex: 0, hasDots: true, isLoop: true, dragable: true, }; protected scrollView = React.createRef(); constructor (props: IMDSwiperProps) { super(props); // @ts-ignore const { children, defaultIndex, autoplay } = props; // @ts-ignore const _oCount = children ? React.Children.count(children) || 1 : 0; const opacitys = new Array(_oCount).fill(0); opacitys[defaultIndex || 0] = 1; this.state = { opacityAnim: new Animated.Value(0), opacitys, userScrolling: false, index: defaultIndex || 0, fromIndex: 0, toIndex: 0, firstIndex: 0, lastIndex: 0, oItemCount: _oCount, // original item count rItemCount: _oCount, // real item count dimension: 0, timer: null, noDrag: false, isStoped: false, autoplay: autoplay || 3000, ready: false, }; } public componentDidMount () { const { children } = this.props; // @ts-ignore if (!children || !children.length) { return; } this.getDimension(); this.initState(() => { if (this.isSlide()) { this.calcuItemCount(() => { InteractionManager.runAfterInteractions(() => { this.setState({ ready: true }, () => { this.translate(this.state.index, false); this.startPlay(); }); }); }); } else { this.opacity(false); this.startPlay(); } }); } public componentWillUnmount () { this.clearTimer(); } // MARK: public methods public next () { this.doTransition('next'); } public prev () { this.doTransition('prev'); } public goto (index: number) { if (isNaN(index)) { return; } if ( index === this.state.index || index < 0 || index >= this.state.oItemCount ) { return; } this.clearTimer(); const towards = index > this.state.index ? 'next' : 'prev'; this.setState({ index, }); this.doTransition(towards, { index }); this.startPlay(); } public getIndex (): number { return this.calcuRealIndex(this.state.index); } public play (autoplay = 3000) { this.clearTimer(); if (autoplay < 500) { return; } this.setState({ autoplay: autoplay || this.props.autoplay || 3000, isStoped: false, }); this.startPlay(); } public stop () { this.clearTimer(); this.setState({ isStoped: true, }); } public render () { const { width, height, transition } = this.props; const _styles = this.props.styles!; const _scrollView = transition === 'fade' ? this.renderFadeView() : this.renderScrollView(); const _dots = this.renderDots(); return ( {_scrollView} {_dots} ); } protected renderChildren () { const { children, width, height, isLoop, transition } = this.props; if (transition === 'fade') { return addAnimToSwiperItem( children, width, height, this.state.opacitys, 0 ); } return addPropsToSwiperItem( children, width, height, this.isSlide() && !!isLoop, transition!, 0 ); } protected renderFadeView () { const { width, height } = this.props; const _children = this.renderChildren(); const _style: ViewStyle = { width, height, position: 'relative', overflow: 'hidden', }; return {_children}; } protected renderScrollView () { const { width, height } = this.props; const _children = this.renderChildren(); let _style: ViewStyle = { width, height, position: 'relative', overflow: 'hidden', opacity: this.state.ready ? 1 : 0, }; let _scrollStyle: ViewStyle = { width, height, overflow: 'hidden', }; let _conStyle: ViewStyle = { height, }; if (this.isVertical()) { _style = { width: height, height: width, overflow: 'hidden', alignSelf: 'center', transform: [ { rotate: '90deg', }, ], }; _scrollStyle = { width: height, height: width, overflow: 'hidden', }; _conStyle = { height: width, }; } return ( {_children} ); } protected renderDots () { const _styles = this.props.styles!; let dotsStyle = _styles.dots; let dotStyle = [_styles.dot]; if (this.isVertical()) { dotsStyle = _styles.dotsVertical; dotStyle = [_styles.dotVertical]; } const dots = []; for (let index = 0; index < this.state.oItemCount; index++) { const _dotStyle = Array.prototype.concat.call([], dotStyle); if (index === this.originalIndex()) { _dotStyle.push(_styles.dotActive!); } dots.push(); } return {dots}; } protected solveContentOffset (e: any) { const contentOffset = e && e.nativeEvent && e.nativeEvent.contentOffset; return contentOffset; } protected onScrollEnd (e: any) { const { onBeforeChange } = this.props; const { dimension, index, userScrolling, noDrag, isStoped } = this.state; if (!userScrolling) { this.afterTrans(); return; } const contentOffset = this.solveContentOffset(e); if (!noDrag) { const offset = contentOffset.x; this.setState( { userScrolling: false, }, () => { if (offset === dimension * index) { return; } const towards = offset > dimension * index ? 'next' : 'prev'; const { newFromIndex, newToIndex, newIndex } = this.calcuNewIndex( towards ); this.setState( { index: newIndex, fromIndex: newFromIndex, toIndex: newToIndex, }, () => { if (!isStoped) { this.startPlay(); } this.afterTrans(); onBeforeChange && onBeforeChange(newFromIndex, newToIndex); } ); } ); } } protected onBeginDrag () { if (this.state.noDrag) { return; } this.setState({ userScrolling: true, }); this.clearTimer(); } protected translate (index: number, animated = true) { const x = index * this.state.dimension; const y = 0; this.scrollView.current && this.scrollView.current.scrollTo({ x, y, animated }); } protected opacity (animate = true, opacity?: number) { const { onAfterChange } = this.props; const { fromIndex, toIndex, rItemCount } = this.state; if (fromIndex === toIndex) { return; } this.setState( { opacitys: this.interpolateAnim(fromIndex, toIndex, rItemCount), }, () => { Animated.timing(this.state.opacityAnim, { duration: 500, toValue: 1, }).start(); } ); if (animate) { setTimeout(() => { this.setState({ opacityAnim: new Animated.Value(0), }); onAfterChange && onAfterChange(fromIndex, toIndex); }, 800); } } protected interpolateAnim ( fromIndex: number, toIndex: number, rItemCount: number ): any[] { if (rItemCount && rItemCount <= 1) { return [1]; } const opacitys = []; for (let index = 0; index < rItemCount; index++) { if (fromIndex === index) { opacitys.push( this.state.opacityAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0], }) ); // opacitys.push(0) } else if (toIndex === index) { opacitys.push( this.state.opacityAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }) ); // opacitys.push(1) } else { opacitys.push(0); } } return opacitys; } protected isFirstItem () { return this.state.index === 0; } protected isLastItem () { return this.state.index === this.state.rItemCount - 1; } protected originalIndex () { if (this.props.isLoop && this.isSlide()) { return this.state.index - 1; } else { return this.state.index; } } protected isSlide () { return this.props.transition!.toLowerCase().indexOf('slide') > -1; } protected isVertical () { return this.props.transition === 'slideY'; } protected getDimension () { this.setState({ dimension: this.isVertical() ? this.props.height : this.props.width, }); } protected initState (cb?: () => void) { const { defaultIndex, dragable, children } = this.props; const { index, firstIndex, lastIndex } = this.state; const _defaultIndex = defaultIndex!; // @ts-ignore const _lenght = children.length; const rItemCount = _lenght; const oItemCount = _lenght; const noDrag = _lenght === 1 || !dragable; const _index = _defaultIndex >= 0 && _defaultIndex < _lenght ? _defaultIndex : 0; const _firstIndex = 0; const _lastIndex = _lenght - 1; const fromIndex = index === firstIndex ? lastIndex : index + 1; const toIndex = index; this.setState( { rItemCount, oItemCount, noDrag, index: _index, firstIndex: _firstIndex, lastIndex: _lastIndex, fromIndex, toIndex, }, () => { cb && cb(); } ); } protected startPlay () { const { autoplay, isLoop } = this.props; const { oItemCount, rItemCount, index } = this.state; if (autoplay! > 0 && oItemCount > 1 && isLoop) { const _timer = setInterval(() => { if (!isLoop && index >= rItemCount - 1) { return this.clearTimer(); } this.doTransition('next'); }, this.state.autoplay); this.setState({ timer: _timer, }); } } protected clearTimer () { if (this.state.timer) { clearInterval(this.state.timer); this.setState({ timer: null, }); } } protected calcuItemCount (cb: () => void) { // @ts-ignore if ( this.props.children && React.Children.count(this.props.children) > 1 && this.props.isLoop ) { const { firstIndex, lastIndex, index, rItemCount } = this.state; this.setState( { firstIndex: firstIndex + 1, lastIndex: lastIndex + 1, index: index + 1, rItemCount: rItemCount + 2, }, () => { cb && cb(); } ); } } protected calcuRealIndex (index: number) { if (this.props.isLoop && this.isSlide() && this.state.oItemCount > 0) { return index - 1 < 0 ? this.state.oItemCount - 1 : index - 1 > this.state.oItemCount - 1 ? 0 : index - 1; } return index; } protected calcuNewIndex ( towards: string, options?: any ): { newFromIndex: number; newToIndex: number; newIndex: number } { const { isLoop } = this.props; const { oItemCount, rItemCount, index, fromIndex, toIndex } = this.state; const result = { newFromIndex: fromIndex, newToIndex: toIndex, newIndex: index, }; if (oItemCount === 0) { return result; } if (!options && oItemCount < 2) { return result; } const itemCount = rItemCount; let newIndex = index; const oldIndex = index; if (!towards) { return result; } if (options && options.index) { newIndex = options.index + (isLoop && this.isSlide() ? 1 : 0); } else if (towards === 'prev') { if (index > 0) { newIndex = index - 1; } else if (!this.isSlide() && index === 0) { newIndex = itemCount - 1; } else if (isLoop && index === 0) { newIndex = itemCount - 1; } } else if (towards === 'next') { if (index < itemCount - 1) { newIndex = index + 1; } else if (!this.isSlide() && index === itemCount - 1) { newIndex = 0; } else if (isLoop && index === itemCount - 1) { newIndex = 1; } } let newFromIndex = toIndex; let newToIndex = newIndex; if (isLoop && this.isSlide()) { newFromIndex = this.calcuRealIndex(oldIndex); newToIndex = this.calcuRealIndex(newIndex); } return { newFromIndex, newToIndex, newIndex }; } protected doTransition (towards: string, options?: any) { const { onBeforeChange } = this.props; const { newFromIndex, newToIndex, newIndex } = this.calcuNewIndex( towards, options ); this.setState( { index: newIndex, fromIndex: newFromIndex, toIndex: newToIndex, }, () => { onBeforeChange && onBeforeChange(newFromIndex, newToIndex); if (!this.isSlide()) { this.opacity(); return; } this.translate(newIndex); } ); } protected afterTrans () { const { isLoop, onAfterChange } = this.props; const { firstIndex, lastIndex, fromIndex, toIndex } = this.state; if (isLoop && (this.isLastItem() || this.isFirstItem())) { const _index = this.isLastItem() ? firstIndex : lastIndex; this.setState( { index: _index, }, () => { this.translate(_index, false); } ); } onAfterChange && onAfterChange(fromIndex, toIndex); } }