Created
July 13, 2016 04:11
-
-
Save lrettig/b34d9b74fac88fe6324a08ddb2efb632 to your computer and use it in GitHub Desktop.
Add react-native-swiper to RNPlay
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2015-present, Facebook, Inc. | |
* All rights reserved. | |
* | |
* This source code is licensed under the BSD-style license found in the | |
* LICENSE file in the root directory of this source tree. An additional grant | |
* of patent rights can be found in the PATENTS file in the same directory. | |
* | |
*/ | |
var GLOBAL = typeof window === 'undefined' ? global : window; | |
var setter = function(_setter, _clearer, array) { | |
return function(callback, delta) { | |
var id = _setter(function() { | |
_clearer.call(this, id); | |
callback.apply(this, arguments); | |
}.bind(this), delta); | |
if (!this[array]) { | |
this[array] = [id]; | |
} else { | |
this[array].push(id); | |
} | |
return id; | |
}; | |
}; | |
var clearer = function(_clearer, array) { | |
return function(id) { | |
if (this[array]) { | |
var index = this[array].indexOf(id); | |
if (index !== -1) { | |
this[array].splice(index, 1); | |
} | |
} | |
_clearer(id); | |
}; | |
}; | |
var _timeouts = 'TimerMixin_timeouts'; | |
var _clearTimeout = clearer(GLOBAL.clearTimeout, _timeouts); | |
var _setTimeout = setter(GLOBAL.setTimeout, _clearTimeout, _timeouts); | |
var _intervals = 'TimerMixin_intervals'; | |
var _clearInterval = clearer(GLOBAL.clearInterval, _intervals); | |
var _setInterval = setter(GLOBAL.setInterval, function() {/* noop */}, _intervals); | |
var _immediates = 'TimerMixin_immediates'; | |
var _clearImmediate = clearer(GLOBAL.clearImmediate, _immediates); | |
var _setImmediate = setter(GLOBAL.setImmediate, _clearImmediate, _immediates); | |
var _rafs = 'TimerMixin_rafs'; | |
var _cancelAnimationFrame = clearer(GLOBAL.cancelAnimationFrame, _rafs); | |
var _requestAnimationFrame = setter(GLOBAL.requestAnimationFrame, _cancelAnimationFrame, _rafs); | |
var TimerMixin = { | |
componentWillUnmount: function() { | |
this[_timeouts] && this[_timeouts].forEach(function(id) { | |
GLOBAL.clearTimeout(id); | |
}); | |
this[_timeouts] = null; | |
this[_intervals] && this[_intervals].forEach(function(id) { | |
GLOBAL.clearInterval(id); | |
}); | |
this[_intervals] = null; | |
this[_immediates] && this[_immediates].forEach(function(id) { | |
GLOBAL.clearImmediate(id); | |
}); | |
this[_immediates] = null; | |
this[_rafs] && this[_rafs].forEach(function(id) { | |
GLOBAL.cancelAnimationFrame(id); | |
}); | |
this[_rafs] = null; | |
}, | |
setTimeout: _setTimeout, | |
clearTimeout: _clearTimeout, | |
setInterval: _setInterval, | |
clearInterval: _clearInterval, | |
setImmediate: _setImmediate, | |
clearImmediate: _clearImmediate, | |
requestAnimationFrame: _requestAnimationFrame, | |
cancelAnimationFrame: _cancelAnimationFrame, | |
}; | |
var Swiper = (function() { | |
/** | |
* react-native-swiper | |
* @author leecade<leecade@163.com> | |
*/ | |
var React = require('react'); | |
var ReactNative = require('react-native'); | |
var { | |
StyleSheet, | |
Text, | |
View, | |
ScrollView, | |
Dimensions, | |
TouchableOpacity, | |
ViewPagerAndroid, | |
Platform | |
} = ReactNative; | |
// Using bare setTimeout, setInterval, setImmediate | |
// and requestAnimationFrame calls is very dangerous | |
// because if you forget to cancel the request before | |
// the component is unmounted, you risk the callback | |
// throwing an exception. | |
//import TimerMixin from 'react-timer-mixin' | |
let { width, height } = Dimensions.get('window') | |
/** | |
* Default styles | |
* @type {StyleSheetPropType} | |
*/ | |
let styles = StyleSheet.create({ | |
container: { | |
backgroundColor: 'transparent', | |
position: 'relative', | |
}, | |
wrapper: { | |
backgroundColor: 'transparent', | |
}, | |
slide: { | |
backgroundColor: 'transparent', | |
}, | |
pagination_x: { | |
position: 'absolute', | |
bottom: 25, | |
left: 0, | |
right: 0, | |
flexDirection: 'row', | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
backgroundColor:'transparent', | |
}, | |
pagination_y: { | |
position: 'absolute', | |
right: 15, | |
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', | |
}, | |
}) | |
return React.createClass({ | |
/** | |
* Props Validation | |
* @type {Object} | |
*/ | |
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, | |
}, | |
mixins: [TimerMixin], | |
/** | |
* Default props | |
* @return {object} props | |
* @see http://facebook.github.io/react-native/docs/scrollview.html | |
*/ | |
getDefaultProps() { | |
return { | |
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 : 2.5, | |
autoplayDirection : true, | |
index : 0, | |
} | |
}, | |
/** | |
* Init states | |
* @return {object} states | |
*/ | |
getInitialState() { | |
return this.initState(this.props) | |
}, | |
/** | |
* autoplay timer | |
* @type {null} | |
*/ | |
autoplayTimer: null, | |
componentWillReceiveProps(props) { | |
this.setState(this.initState(props)) | |
}, | |
componentDidMount() { | |
this.autoplay() | |
}, | |
initState(props) { | |
// set the current state | |
const state = this.state || {} | |
let initState = { | |
isScrolling: false, | |
autoplayEnd: 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.width || width | |
initState.height = props.height || height | |
initState.offset = {} | |
if (initState.total > 1) { | |
var setup = initState.index | |
if ( props.loop ) { | |
setup++ | |
} | |
initState.offset[initState.dir] = initState.dir === 'y' | |
? initState.height * setup | |
: initState.width * setup | |
} | |
return initState | |
}, | |
/** | |
* Automatic rolling | |
*/ | |
autoplay() { | |
if( | |
!Array.isArray(this.props.children) | |
|| !this.props.autoplay | |
|| this.state.isScrolling | |
|| this.state.autoplayEnd | |
) { | |
return | |
} | |
clearTimeout(this.autoplayTimer) | |
this.autoplayTimer = this.setTimeout(() => { | |
if( | |
!this.props.loop && ( | |
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 * 1000) | |
}, | |
/** | |
* Scroll begin handle | |
* @param {object} e native event | |
*/ | |
onScrollBegin(e) { | |
// update scroll state | |
this.setState({ isScrolling: true }) | |
this.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.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.setTimeout(() => { | |
this.autoplay() | |
// 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 | |
// 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 + diff / step) | |
if(this.props.loop) { | |
if(index <= -1) { | |
index = state.total - 1 | |
offset[dir] = step * state.total | |
} | |
else if(index >= state.total) { | |
index = 0 | |
offset[dir] = step | |
} | |
} | |
this.setState({ | |
index: index, | |
offset: offset, | |
}) | |
}, | |
/** | |
* 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.props.loop ? 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 === 'android') { | |
this.refs.scrollView && this.refs.scrollView.setPage(diff) | |
} else { | |
this.refs.scrollView && this.refs.scrollView.scrollTo({ x, y }) | |
} | |
// update scroll state | |
this.setState({ | |
isScrolling: true, | |
autoplayEnd: false, | |
}) | |
// trigger onScrollEnd manually in android | |
if (Platform.OS === 'android') { | |
this.setTimeout(() => { | |
this.onScrollEnd({ | |
nativeEvent: { | |
position: diff, | |
} | |
}); | |
}, 50); | |
} | |
}, | |
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 || <View style={{ | |
backgroundColor: '#007aff', | |
width: 8, | |
height: 8, | |
borderRadius: 4, | |
marginLeft: 3, | |
marginRight: 3, | |
marginTop: 3, | |
marginBottom: 3, | |
}} />; | |
let Dot = this.props.dot || <View style={{ | |
backgroundColor:'rgba(0,0,0,.2)', | |
width: 8, | |
height: 8, | |
borderRadius: 4, | |
marginLeft: 3, | |
marginRight: 3, | |
marginTop: 3, | |
marginBottom: 3, | |
}} />; | |
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.props.loop || 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.props.loop || 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') | |
return ( | |
<ScrollView ref="scrollView" | |
{...this.props} | |
{...this.scrollViewPropOverrides()} | |
contentContainerStyle={[styles.wrapper, this.props.style]} | |
contentOffset={this.state.offset} | |
onScrollBeginDrag={this.onScrollBegin} | |
onMomentumScrollEnd={this.onScrollEnd} | |
onScrollEndDrag={this.onScrollEndDrag}> | |
{pages} | |
</ScrollView> | |
); | |
return ( | |
<ViewPagerAndroid ref="scrollView" | |
{...this.props} | |
initialPage={this.props.loop ? this.state.index + 1 : this.state.index} | |
onPageSelected={this.onScrollEnd} | |
style={{flex: 1}}> | |
{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 = props.loop | |
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> | |
) | |
} | |
}) | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment