UNPKG

17.7 kBJavaScriptView Raw
1/**
2 * react-native-swiper
3 * @author leecade<leecade@163.com>
4 */
5import React, { Component } from 'react'
6import PropTypes from 'prop-types'
7import {
8 Text,
9 View,
10 ViewPropTypes,
11 ScrollView,
12 Dimensions,
13 TouchableOpacity,
14 ViewPagerAndroid,
15 Platform,
16 ActivityIndicator
17} from 'react-native'
18
19const { width, height } = Dimensions.get('window')
20
21/**
22 * Default styles
23 * @type {StyleSheetPropType}
24 */
25const styles = {
26 container: {
27 backgroundColor: 'transparent',
28 position: 'relative',
29 flex: 1
30 },
31
32 wrapperIOS: {
33 backgroundColor: 'transparent',
34 },
35
36 wrapperAndroid: {
37 backgroundColor: 'transparent',
38 flex: 1
39 },
40
41 slide: {
42 backgroundColor: 'transparent',
43 },
44
45 pagination_x: {
46 position: 'absolute',
47 bottom: 25,
48 left: 0,
49 right: 0,
50 flexDirection: 'row',
51 flex: 1,
52 justifyContent: 'center',
53 alignItems: 'center',
54 backgroundColor: 'transparent'
55 },
56
57 pagination_y: {
58 position: 'absolute',
59 right: 15,
60 top: 0,
61 bottom: 0,
62 flexDirection: 'column',
63 flex: 1,
64 justifyContent: 'center',
65 alignItems: 'center',
66 backgroundColor: 'transparent'
67 },
68
69 title: {
70 height: 30,
71 justifyContent: 'center',
72 position: 'absolute',
73 paddingLeft: 10,
74 bottom: -30,
75 left: 0,
76 flexWrap: 'nowrap',
77 width: 250,
78 backgroundColor: 'transparent'
79 },
80
81 buttonWrapper: {
82 backgroundColor: 'transparent',
83 flexDirection: 'row',
84 position: 'absolute',
85 top: 0,
86 left: 0,
87 flex: 1,
88 paddingHorizontal: 10,
89 paddingVertical: 10,
90 justifyContent: 'space-between',
91 alignItems: 'center'
92 },
93
94 buttonText: {
95 fontSize: 50,
96 color: '#007aff',
97 fontFamily: 'Arial'
98 }
99}
100
101// missing `module.exports = exports['default'];` with babel6
102// export default React.createClass({
103export default class extends Component {
104 /**
105 * Props Validation
106 * @type {Object}
107 */
108 static propTypes = {
109 horizontal: PropTypes.bool,
110 children: PropTypes.node.isRequired,
111 containerStyle: PropTypes.oneOfType([
112 PropTypes.object,
113 PropTypes.number,
114 ]),
115 style: PropTypes.oneOfType([
116 PropTypes.object,
117 PropTypes.number,
118 ]),
119 pagingEnabled: PropTypes.bool,
120 showsHorizontalScrollIndicator: PropTypes.bool,
121 showsVerticalScrollIndicator: PropTypes.bool,
122 bounces: PropTypes.bool,
123 scrollsToTop: PropTypes.bool,
124 removeClippedSubviews: PropTypes.bool,
125 automaticallyAdjustContentInsets: PropTypes.bool,
126 showsPagination: PropTypes.bool,
127 showsButtons: PropTypes.bool,
128 loadMinimal: PropTypes.bool,
129 loadMinimalSize: PropTypes.number,
130 loadMinimalLoader: PropTypes.element,
131 loop: PropTypes.bool,
132 autoplay: PropTypes.bool,
133 autoplayTimeout: PropTypes.number,
134 autoplayDirection: PropTypes.bool,
135 index: PropTypes.number,
136 renderPagination: PropTypes.func,
137 dotStyle: PropTypes.object,
138 activeDotStyle: PropTypes.object,
139 dotColor: PropTypes.string,
140 activeDotColor: PropTypes.string,
141 /**
142 * Called when the index has changed because the user swiped.
143 */
144 onIndexChanged: PropTypes.func
145 }
146
147 /**
148 * Default props
149 * @return {object} props
150 * @see http://facebook.github.io/react-native/docs/scrollview.html
151 */
152 static defaultProps = {
153 horizontal: true,
154 pagingEnabled: true,
155 showsHorizontalScrollIndicator: false,
156 showsVerticalScrollIndicator: false,
157 bounces: false,
158 scrollsToTop: false,
159 removeClippedSubviews: true,
160 automaticallyAdjustContentInsets: false,
161 showsPagination: true,
162 showsButtons: false,
163 loop: true,
164 loadMinimal: false,
165 loadMinimalSize: 1,
166 autoplay: false,
167 autoplayTimeout: 2.5,
168 autoplayDirection: true,
169 index: 0,
170 onIndexChanged: () => null
171 }
172
173 /**
174 * Init states
175 * @return {object} states
176 */
177 state = this.initState(this.props)
178
179 /**
180 * autoplay timer
181 * @type {null}
182 */
183 autoplayTimer = null
184 loopJumpTimer = null
185
186 componentWillReceiveProps (nextProps) {
187 if (!nextProps.autoplay && this.autoplayTimer) clearTimeout(this.autoplayTimer)
188 this.setState(this.initState(nextProps))
189 }
190
191 componentDidMount () {
192 this.autoplay()
193 }
194
195 componentWillUnmount () {
196 this.autoplayTimer && clearTimeout(this.autoplayTimer)
197 this.loopJumpTimer && clearTimeout(this.loopJumpTimer)
198 }
199
200 componentWillUpdate (nextProps, nextState) {
201 // If the index has changed, we notify the parent via the onIndexChanged callback
202 if (this.state.index !== nextState.index) this.props.onIndexChanged(nextState.index)
203 }
204
205 initState (props) {
206 // set the current state
207 const state = this.state || { width: 0, height: 0, offset: { x: 0, y: 0 } }
208
209 const initState = {
210 autoplayEnd: false,
211 loopJump: false
212 }
213
214 initState.total = props.children ? props.children.length || 1 : 0
215
216 if (state.total === initState.total) {
217 // retain the index
218 initState.index = state.index
219 } else {
220 initState.index = initState.total > 1 ? Math.min(props.index, initState.total - 1) : 0
221 }
222
223 // Default: horizontal
224 initState.dir = props.horizontal === false ? 'y' : 'x'
225 initState.width = props.width || width
226 initState.height = props.height || height
227
228 this.internals = {
229 ...this.internals,
230 isScrolling: false
231 };
232 return initState
233 }
234
235 // include internals with state
236 fullState () {
237 return Object.assign({}, this.state, this.internals)
238 }
239
240 onLayout = (event) => {
241 const { width, height } = event.nativeEvent.layout
242 const offset = this.internals.offset = {}
243 const state = { width, height }
244
245 if (this.state.total > 1) {
246 let setup = this.state.index
247 if (this.props.loop) {
248 setup++
249 }
250 offset[this.state.dir] = this.state.dir === 'y'
251 ? height * setup
252 : width * setup
253 }
254
255 // only update the offset in state if needed, updating offset while swiping
256 // causes some bad jumping / stuttering
257 if (!this.state.offset || width !== this.state.width || height !== this.state.height) {
258 state.offset = offset
259 }
260 this.setState(state)
261 }
262
263 loopJump = () => {
264 if (!this.state.loopJump) return
265 const i = this.state.index + (this.props.loop ? 1 : 0)
266 const scrollView = this.refs.scrollView
267 this.loopJumpTimer = setTimeout(() => scrollView.setPageWithoutAnimation &&
268 scrollView.setPageWithoutAnimation(i), 50)
269 }
270
271 /**
272 * Automatic rolling
273 */
274 autoplay = () => {
275 if (!Array.isArray(this.props.children) ||
276 !this.props.autoplay ||
277 this.internals.isScrolling ||
278 this.state.autoplayEnd) return
279
280 this.autoplayTimer && clearTimeout(this.autoplayTimer)
281 this.autoplayTimer = setTimeout(() => {
282 if (!this.props.loop && (
283 this.props.autoplayDirection
284 ? this.state.index === this.state.total - 1
285 : this.state.index === 0
286 )
287 ) return this.setState({ autoplayEnd: true })
288
289 this.scrollBy(this.props.autoplayDirection ? 1 : -1)
290 }, this.props.autoplayTimeout * 1000)
291 }
292
293 /**
294 * Scroll begin handle
295 * @param {object} e native event
296 */
297 onScrollBegin = e => {
298 // update scroll state
299 this.internals.isScrolling = true
300 this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e, this.fullState(), this)
301 }
302
303 /**
304 * Scroll end handle
305 * @param {object} e native event
306 */
307 onScrollEnd = e => {
308 // update scroll state
309 this.internals.isScrolling = false
310
311 // making our events coming from android compatible to updateIndex logic
312 if (!e.nativeEvent.contentOffset) {
313 if (this.state.dir === 'x') {
314 e.nativeEvent.contentOffset = {x: e.nativeEvent.position * this.state.width}
315 } else {
316 e.nativeEvent.contentOffset = {y: e.nativeEvent.position * this.state.height}
317 }
318 }
319
320 this.updateIndex(e.nativeEvent.contentOffset, this.state.dir, () => {
321 this.autoplay()
322 this.loopJump()
323
324 // if `onMomentumScrollEnd` registered will be called here
325 this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e, this.fullState(), this)
326 })
327 }
328
329 /*
330 * Drag end handle
331 * @param {object} e native event
332 */
333 onScrollEndDrag = e => {
334 const { contentOffset } = e.nativeEvent
335 const { horizontal, children } = this.props
336 const { index } = this.state
337 const { offset } = this.internals
338 const previousOffset = horizontal ? offset.x : offset.y
339 const newOffset = horizontal ? contentOffset.x : contentOffset.y
340
341 if (previousOffset === newOffset &&
342 (index === 0 || index === children.length - 1)) {
343 this.internals.isScrolling = false
344 }
345 }
346
347 /**
348 * Update index after scroll
349 * @param {object} offset content offset
350 * @param {string} dir 'x' || 'y'
351 */
352 updateIndex = (offset, dir, cb) => {
353 const state = this.state
354 let index = state.index
355 const diff = offset[dir] - this.internals.offset[dir]
356 const step = dir === 'x' ? state.width : state.height
357 let loopJump = false
358
359 // Do nothing if offset no change.
360 if (!diff) return
361
362 // Note: if touch very very quickly and continuous,
363 // the variation of `index` more than 1.
364 // parseInt() ensures it's always an integer
365 index = parseInt(index + Math.round(diff / step))
366
367 if (this.props.loop) {
368 if (index <= -1) {
369 index = state.total - 1
370 offset[dir] = step * state.total
371 loopJump = true
372 } else if (index >= state.total) {
373 index = 0
374 offset[dir] = step
375 loopJump = true
376 }
377 }
378
379 const newState = {}
380 newState.index = index
381 newState.loopJump = loopJump
382
383 this.internals.offset = offset
384
385 // only update offset in state if loopJump is true
386 if (loopJump) {
387 // when swiping to the beginning of a looping set for the third time,
388 // the new offset will be the same as the last one set in state.
389 // Setting the offset to the same thing will not do anything,
390 // so we increment it by 1 then immediately set it to what it should be,
391 // after render.
392 if (offset[dir] === this.internals.offset[dir]) {
393 newState.offset = { x: 0, y: 0 }
394 newState.offset[dir] = offset[dir] + 1
395 this.setState(newState, () => {
396 this.setState({ offset: offset }, cb)
397 })
398 } else {
399 newState.offset = offset
400 this.setState(newState, cb)
401 }
402 } else {
403 this.setState(newState, cb)
404 }
405 }
406
407 /**
408 * Scroll by index
409 * @param {number} index offset index
410 * @param {bool} animated
411 */
412
413 scrollBy = (index, animated = true) => {
414 if (this.internals.isScrolling || this.state.total < 2) return
415 const state = this.state
416 const diff = (this.props.loop ? 1 : 0) + index + this.state.index
417 let x = 0
418 let y = 0
419 if (state.dir === 'x') x = diff * state.width
420 if (state.dir === 'y') y = diff * state.height
421
422 if (Platform.OS === 'android') {
423 this.refs.scrollView && this.refs.scrollView[animated ? 'setPage' : 'setPageWithoutAnimation'](diff)
424 } else {
425 this.refs.scrollView && this.refs.scrollView.scrollTo({ x, y, animated })
426 }
427
428 // update scroll state
429 this.internals.isScrolling = true
430 this.setState({
431 autoplayEnd: false
432 })
433
434 // trigger onScrollEnd manually in android
435 if (!animated || Platform.OS === 'android') {
436 setImmediate(() => {
437 this.onScrollEnd({
438 nativeEvent: {
439 position: diff
440 }
441 })
442 })
443 }
444 }
445
446 scrollViewPropOverrides = () => {
447 const props = this.props
448 let overrides = {}
449
450 /*
451 const scrollResponders = [
452 'onMomentumScrollBegin',
453 'onTouchStartCapture',
454 'onTouchStart',
455 'onTouchEnd',
456 'onResponderRelease',
457 ]
458 */
459
460 for (let prop in props) {
461 // if(~scrollResponders.indexOf(prop)
462 if (typeof props[prop] === 'function' &&
463 prop !== 'onMomentumScrollEnd' &&
464 prop !== 'renderPagination' &&
465 prop !== 'onScrollBeginDrag'
466 ) {
467 let originResponder = props[prop]
468 overrides[prop] = (e) => originResponder(e, this.fullState(), this)
469 }
470 }
471
472 return overrides
473 }
474
475 /**
476 * Render pagination
477 * @return {object} react-dom
478 */
479 renderPagination = () => {
480 // By default, dots only show when `total` >= 2
481 if (this.state.total <= 1) return null
482
483 let dots = []
484 const ActiveDot = this.props.activeDot || <View style={[{
485 backgroundColor: this.props.activeDotColor || '#007aff',
486 width: 8,
487 height: 8,
488 borderRadius: 4,
489 marginLeft: 3,
490 marginRight: 3,
491 marginTop: 3,
492 marginBottom: 3
493 }, this.props.activeDotStyle]} />
494 const Dot = this.props.dot || <View style={[{
495 backgroundColor: this.props.dotColor || 'rgba(0,0,0,.2)',
496 width: 8,
497 height: 8,
498 borderRadius: 4,
499 marginLeft: 3,
500 marginRight: 3,
501 marginTop: 3,
502 marginBottom: 3
503 }, this.props.dotStyle ]} />
504 for (let i = 0; i < this.state.total; i++) {
505 dots.push(i === this.state.index
506 ? React.cloneElement(ActiveDot, {key: i})
507 : React.cloneElement(Dot, {key: i})
508 )
509 }
510
511 return (
512 <View pointerEvents='none' style={[styles['pagination_' + this.state.dir], this.props.paginationStyle]}>
513 {dots}
514 </View>
515 )
516 }
517
518 renderTitle = () => {
519 const child = this.props.children[this.state.index]
520 const title = child && child.props && child.props.title
521 return title
522 ? (<View style={styles.title}>
523 {this.props.children[this.state.index].props.title}
524 </View>)
525 : null
526 }
527
528 renderNextButton = () => {
529 let button = null
530
531 if (this.props.loop ||
532 this.state.index !== this.state.total - 1) {
533 button = this.props.nextButton || <Text style={styles.buttonText}>›</Text>
534 }
535
536 return (
537 <TouchableOpacity onPress={() => button !== null && this.scrollBy(1)}>
538 <View>
539 {button}
540 </View>
541 </TouchableOpacity>
542 )
543 }
544
545 renderPrevButton = () => {
546 let button = null
547
548 if (this.props.loop || this.state.index !== 0) {
549 button = this.props.prevButton || <Text style={styles.buttonText}>‹</Text>
550 }
551
552 return (
553 <TouchableOpacity onPress={() => button !== null && this.scrollBy(-1)}>
554 <View>
555 {button}
556 </View>
557 </TouchableOpacity>
558 )
559 }
560
561 renderButtons = () => {
562 return (
563 <View pointerEvents='box-none' style={[styles.buttonWrapper, {
564 width: this.state.width,
565 height: this.state.height
566 }, this.props.buttonWrapperStyle]}>
567 {this.renderPrevButton()}
568 {this.renderNextButton()}
569 </View>
570 )
571 }
572
573 renderScrollView = pages => {
574 if (Platform.OS === 'ios') {
575 return (
576 <ScrollView ref='scrollView'
577 {...this.props}
578 {...this.scrollViewPropOverrides()}
579 contentContainerStyle={[styles.wrapperIOS, this.props.style]}
580 contentOffset={this.state.offset}
581 onScrollBeginDrag={this.onScrollBegin}
582 onMomentumScrollEnd={this.onScrollEnd}
583 onScrollEndDrag={this.onScrollEndDrag}>
584 {pages}
585 </ScrollView>
586 )
587 }
588 return (
589 <ViewPagerAndroid ref='scrollView'
590 {...this.props}
591 initialPage={this.props.loop ? this.state.index + 1 : this.state.index}
592 onPageSelected={this.onScrollEnd}
593 key={pages.length}
594 style={[styles.wrapperAndroid, this.props.style]}>
595 {pages}
596 </ViewPagerAndroid>
597 )
598 }
599
600 /**
601 * Default render
602 * @return {object} react-dom
603 */
604 render () {
605 const state = this.state
606 const props = this.props
607 const {
608 index,
609 total,
610 width,
611 height
612 } = this.state;
613 const {
614 children,
615 containerStyle,
616 loop,
617 loadMinimal,
618 loadMinimalSize,
619 loadMinimalLoader,
620 renderPagination,
621 showsButtons,
622 showsPagination,
623 } = this.props;
624 // let dir = state.dir
625 // let key = 0
626 const loopVal = loop ? 1 : 0
627 let pages = []
628
629 const pageStyle = [{width: width, height: height}, styles.slide]
630 const pageStyleLoading = {
631 width,
632 height,
633 flex: 1,
634 justifyContent: 'center',
635 alignItems: 'center'
636 }
637
638 // For make infinite at least total > 1
639 if (total > 1) {
640 // Re-design a loop model for avoid img flickering
641 pages = Object.keys(children)
642 if (loop) {
643 pages.unshift(total - 1 + '')
644 pages.push('0')
645 }
646
647 pages = pages.map((page, i) => {
648 if (loadMinimal) {
649 if (i >= (index + loopVal - loadMinimalSize) &&
650 i <= (index + loopVal + loadMinimalSize)) {
651 return <View style={pageStyle} key={i}>{children[page]}</View>
652 } else {
653 return (
654 <View style={pageStyleLoading} key={i}>
655 {loadMinimalLoader ? loadMinimalLoader : <ActivityIndicator />}
656 </View>
657 )
658 }
659 } else {
660 return <View style={pageStyle} key={i}>{children[page]}</View>
661 }
662 })
663 } else {
664 pages = <View style={pageStyle} key={0}>{children}</View>
665 }
666
667 return (
668 <View style={[styles.container, containerStyle]} onLayout={this.onLayout}>
669 {this.renderScrollView(pages)}
670 {showsPagination && (renderPagination
671 ? renderPagination(index, total, this)
672 : this.renderPagination())}
673 {this.renderTitle()}
674 {showsButtons && this.renderButtons()}
675 </View>
676 )
677 }
678}