citong-react-component
Version:
> A component framework for React Native / React Web.
627 lines (548 loc) • 16.7 kB
JavaScript
'use strict';
/**
* Copyright (c) 2016 Copyright citongs All Rights Reserved.
* Author: lipengxiang
*/
import React,{ Component, PropTypes } from 'react';
import ReactNative, {
AppRegistry,
StyleSheet,
Text,
View,
Image,
Dimensions,
Platform,
ViewPagerAndroid,
ScrollView,
TouchableOpacity
} from 'react-native';
import TimerMgr from './timerMgr';
var {
width,
height
} = Dimensions.get('window');
/**
* @desc view class
*/
export default class Page extends Component {
/**
* autoplay timer
* @type {null}
*/
//this.autoplayTimer = null;
constructor(props) {
super(props);
this.timerMgr = new TimerMgr();
this.state = this.initState(props);
this.onScrollBegin = this.onScrollBegin.bind(this);
this.onScrollEnd = this.onScrollEnd.bind(this);
this.onScrollEndDrag = this.onScrollEndDrag.bind(this);
this.updateIndex = this.updateIndex.bind(this);
this.scrollBy = this.scrollBy.bind(this);
this.scrollViewPropOverrides = this.scrollViewPropOverrides.bind(this);
}
componentDidMount() {
this.autoplay()
}
componentWillUnmount() {
this.timerMgr.dispose();
}
componentWillReceiveProps(props) {
this.setState(this.initState(props))
};
initState(props) {
// set the current state
const state = this.state || {}
let initState = {
isScrolling: false,
autoplayEnd: false,
loopJump: false,
}
initState.total = props.children ? props.children.length || 1 : 0
if (state.total === initState.total) {
// retain the index
initState.index = state.index
} else {
// reset the index
initState.index = initState.total > 1 ? Math.min(props.index, initState.total - 1) : 0
}
// Default: horizontal
initState.dir = props.horizontal === false ? 'y' : 'x'
initState.width = (props.style && props.style.width) || width;
initState.height = (props.style && props.style.height) || height;
initState.offset = {}
if (initState.total > 1) {
var setup = initState.index
if ( this.isLoop ) {
setup++
}
initState.offset[initState.dir] = initState.dir === 'y'
? initState.height * setup
: initState.width * setup
}
return initState
};
get isLoop() {
return this.props.loop;// && Platform.OS != 'web';
}
/**
* @desc:
* @return:
*/
loopJump() {
if(this.state.loopJump){
if (this._prePageIndex == this.state.index)
return;
var i = this.state.index + (this.isLoop ? 1 : 0);
if (this.refs.scrollView.setPageWithoutAnimation)
this.refs.scrollView.setPageWithoutAnimation(i);
else if (this.refs.scrollView.setPage)
this.refs.scrollView.setPage(i);
}
};
/**
* Automatic rolling
*/
autoplay() {
if(
!Array.isArray(this.props.children)
|| !this.props.autoplay
|| this.state.isScrolling
|| this.state.autoplayEnd
) {
return
}
this.timerMgr.clearTimeout(this.autoplayTimer)
this.autoplayTimer = this.timerMgr.setTimeout(() => {
if(
!this.isLoop && (
this.props.autoplayDirection
? this.state.index === this.state.total - 1
: this.state.index === 0
)
) {
return this.setState({ autoplayEnd: true })
}
this.scrollBy(this.props.autoplayDirection ? 1 : -1)
}, this.props.autoplayTimeout)
};
/**
* Scroll begin handle
* @param {object} e native event
*/
onScrollBegin(e) {
// update scroll state
this.setState({ isScrolling: true })
this.timerMgr.setTimeout(() => {
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e, this.state, this)
})
};
/**
* Scroll end handle
* @param {object} e native event
*/
onScrollEnd(e) {
// update scroll state
this.setState({
isScrolling: false
})
// making our events coming from android compatible to updateIndex logic
if (!e.nativeEvent.contentOffset) {
if (this.state.dir === 'x') {
e.nativeEvent.contentOffset = {x: e.nativeEvent.position * this.state.width}
} else {
e.nativeEvent.contentOffset = {y: e.nativeEvent.position * this.state.height}
}
}
this._prePageIndex = this.state.index;
this.updateIndex(e.nativeEvent.contentOffset, this.state.dir)
// Note: `this.setState` is async, so I call the `onMomentumScrollEnd`
// in setTimeout to ensure synchronous update `index`
this.timerMgr.setTimeout(() => {
this.autoplay()
this.loopJump();
// if `onMomentumScrollEnd` registered will be called here
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e, this.state, this)
})
};
/*
* Drag end handle
* @param {object} e native event
*/
onScrollEndDrag(e) {
let { contentOffset } = e.nativeEvent
let { horizontal, children } = this.props
let { offset, index } = this.state
let previousOffset = horizontal ? offset.x : offset.y
let newOffset = horizontal ? contentOffset.x : contentOffset.y
if (previousOffset === newOffset && (index === 0 || index === children.length - 1)) {
this.setState({
isScrolling: false
})
}
};
/**
* Update index after scroll
* @param {object} offset content offset
* @param {string} dir 'x' || 'y'
*/
updateIndex(offset, dir) {
let state = this.state
let index = state.index
let diff = offset[dir] - state.offset[dir]
let step = dir === 'x' ? state.width : state.height
let loopJump = false;
// Do nothing if offset no change.
if(!diff) return
// Note: if touch very very quickly and continuous,
// the variation of `index` more than 1.
// parseInt() ensures it's always an integer
index = parseInt(index + Math.round(diff / step))
if(this.isLoop) {
if(index <= -1) {
//index = state.total - 1
index %= state.total;
index += state.total;
offset[dir] = step * state.total
loopJump = true;
}
else if(index >= state.total) {
index %= state.total;
offset[dir] = step
loopJump = true;
}
}
this.setState({
index: index,
offset: offset,
loopJump: loopJump,
})
};
/**
* Scroll by index
* @param {number} index offset index
*/
scrollBy(index) {
if (this.state.isScrolling || this.state.total < 2) return
let state = this.state
let diff = (this.isLoop ? 1 : 0) + index + this.state.index
let x = 0
let y = 0
if(state.dir === 'x') x = diff * state.width
if(state.dir === 'y') y = diff * state.height
if (Platform.OS === 'ios' || this.props.children.length <= 1) {
this.refs.scrollView && this.refs.scrollView.scrollTo({ x, y })
} else {
this.refs.scrollView && this.refs.scrollView.setPage(diff)
}
// update scroll state
this.setState({
isScrolling: true,
autoplayEnd: false,
})
// trigger onScrollEnd manually in android
if (!(Platform.OS === 'ios' || this.props.children.length <= 1)) {
this.timerMgr.setTimeout(() => {
this.onScrollEnd({
nativeEvent: {
position: diff,
}
});
}, 0);
}
};
scrollViewPropOverrides() {
var props = this.props
var overrides = {}
/*
const scrollResponders = [
'onMomentumScrollBegin',
'onTouchStartCapture',
'onTouchStart',
'onTouchEnd',
'onResponderRelease',
]
*/
for(let prop in props) {
// if(~scrollResponders.indexOf(prop)
if(typeof props[prop] === 'function'
&& prop !== 'onMomentumScrollEnd'
&& prop !== 'renderPagination'
&& prop !== 'onScrollBeginDrag'
) {
let originResponder = props[prop]
overrides[prop] = (e) => originResponder(e, this.state, this)
}
}
return overrides
};
/**
* Render pagination
* @return {object} react-dom
*/
renderPagination() {
// By default, dots only show when `total` > 2
if(this.state.total <= 1) return null
let dots = []
let ActiveDot = (this.props.activeDot || Page.activeDot) || <View style={[styles.activeDot,Page.activeDotStyle,this.activeDotStyle]} />;
let Dot = (this.props.dot || Page.dot) || <View style={[styles.dot,Page.dotStyle,this.dotStyle]} />;
for(let i = 0; i < this.state.total; i++) {
dots.push(i === this.state.index
?
React.cloneElement(ActiveDot, {key: i})
:
React.cloneElement(Dot, {key: i})
)
}
return (
<View pointerEvents='none' style={[styles['pagination_' + this.state.dir], this.props.paginationStyle]}>
{dots}
</View>
)
};
renderTitle() {
let child = this.props.children[this.state.index]
let title = child && child.props.title
return title
? (
<View style={styles.title}>
{this.props.children[this.state.index].props.title}
</View>
)
: null
};
renderNextButton() {
let button;
if (this.isLoop || this.state.index != this.state.total - 1) {
button = this.props.nextButton || <Text style={styles.buttonText}>›</Text>
}
return (
<TouchableOpacity onPress={() => button !== null && this.scrollBy.call(this, 1)}>
<View>
{button}
</View>
</TouchableOpacity>
)
};
renderPrevButton() {
let button = null
if (this.isLoop || this.state.index != 0) {
button = this.props.prevButton || <Text style={styles.buttonText}>‹</Text>
}
return (
<TouchableOpacity onPress={() => button !== null && this.scrollBy.call(this, -1)}>
<View>
{button}
</View>
</TouchableOpacity>
)
};
renderButtons() {
return (
<View pointerEvents='box-none' style={[styles.buttonWrapper, {width: this.state.width, height: this.state.height}, this.props.buttonWrapperStyle]}>
{this.renderPrevButton()}
{this.renderNextButton()}
</View>
)
};
renderScrollView(pages) {
if (Platform.OS === 'ios' || this.props.children.length <= 1)
return (
<ScrollView ref="scrollView"
{...this.props}
{...this.scrollViewPropOverrides()}
contentContainerStyle={[styles.wrapper, this.props.style, {overflow:'hidden'}]}
contentOffset={this.state.offset}
onScrollBeginDrag={this.onScrollBegin}
onMomentumScrollEnd={this.onScrollEnd}
onScrollEndDrag={this.onScrollEndDrag}
>
{pages}
</ScrollView>
);
else
return (
<ViewPagerAndroid ref="scrollView"
{...this.props}
initialPage={this.isLoop ? this.state.index + 1 : this.state.index}
onPageSelected={this.onScrollEnd}
style={{flex: 1, overflow:'hidden'}}>
{pages}
</ViewPagerAndroid>
);
};
/**
* Default render
* @return {object} react-dom
*/
render() {
let state = this.state
let props = this.props
let children = props.children
let index = state.index
let total = state.total
let loop = this.isLoop
let dir = state.dir
let key = 0
let pages = []
let pageStyle = [{width: state.width, height: state.height}, styles.slide]
// For make infinite at least total > 1
if(total > 1) {
// Re-design a loop model for avoid img flickering
pages = Object.keys(children)
if(loop) {
pages.unshift(total - 1 + '')
pages.push('0')
}
pages = pages.map((page, i) =>
<View style={pageStyle} key={i}>{children[page]}</View>
)
}
else pages = <View style={pageStyle}>{children}</View>
return (
<View style={[styles.container, {
width: state.width,
height: state.height
}]}>
{this.renderScrollView(pages)}
{props.showsPagination && (props.renderPagination
? this.props.renderPagination(state.index, state.total, this)
: this.renderPagination())}
{this.renderTitle()}
{this.props.showsButtons && this.renderButtons()}
</View>
)
}
}
/**
* Props Validation
* @type {Object}
*/
Page.propTypes = {
horizontal : React.PropTypes.bool,
children : React.PropTypes.node.isRequired,
style : View.propTypes.style,
pagingEnabled : React.PropTypes.bool,
showsHorizontalScrollIndicator : React.PropTypes.bool,
showsVerticalScrollIndicator : React.PropTypes.bool,
bounces : React.PropTypes.bool,
scrollsToTop : React.PropTypes.bool,
removeClippedSubviews : React.PropTypes.bool,
automaticallyAdjustContentInsets : React.PropTypes.bool,
showsPagination : React.PropTypes.bool,
showsButtons : React.PropTypes.bool,
loop : React.PropTypes.bool,
autoplay : React.PropTypes.bool,
autoplayTimeout : React.PropTypes.number,
autoplayDirection : React.PropTypes.bool,
index : React.PropTypes.number,
renderPagination : React.PropTypes.func,
};
Page.defaultProps = {
horizontal : true,
pagingEnabled : true,
showsHorizontalScrollIndicator : false,
showsVerticalScrollIndicator : false,
bounces : false,
scrollsToTop : false,
removeClippedSubviews : true,
automaticallyAdjustContentInsets : false,
showsPagination : true,
showsButtons : false,
loop : true,
autoplay : false,
autoplayTimeout : 2500,
autoplayDirection : true,
index : 0,
dotStyle : null,
activeDotStyle : null,
};
Page.dotStyle = null;
Page.activeDotStyle = null;
Page.dot = null;
Page.activeDot = null;
/**
* @desc view style
*/
const styles = StyleSheet.create({
container: {
backgroundColor: 'transparent',
position: 'relative',
},
wrapper: {
backgroundColor: 'transparent',
},
slide: {
backgroundColor: 'transparent',
},
pagination_x: {
position: 'absolute',
bottom: 10,
left: 0,
right: 0,
flexDirection: 'row',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor:'transparent',
},
pagination_y: {
position: 'absolute',
right: 10,
top: 0,
bottom: 0,
flexDirection: 'column',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor:'transparent',
},
title: {
height: 30,
justifyContent: 'center',
position: 'absolute',
paddingLeft: 10,
bottom: -30,
left: 0,
flexWrap: 'nowrap',
width: 250,
backgroundColor: 'transparent',
},
buttonWrapper: {
backgroundColor: 'transparent',
flexDirection: 'row',
position: 'absolute',
top: 0,
left: 0,
flex: 1,
paddingHorizontal: 10,
paddingVertical: 10,
justifyContent: 'space-between',
alignItems: 'center'
},
buttonText: {
fontSize: 50,
color: '#007aff',
fontFamily: 'Arial',
},
activeDot: {
backgroundColor: '#007aff',
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 3,
marginRight: 3,
marginTop: 3,
marginBottom: 3
},
dot: {
backgroundColor:'rgba(0,0,0,.2)',
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 3,
marginRight: 3,
marginTop: 3,
marginBottom: 3
}
})