1 |
|
2 |
|
3 |
|
4 |
|
5 | import React, { Component } from 'react'
|
6 | import PropTypes from 'prop-types'
|
7 | import {
|
8 | Text,
|
9 | View,
|
10 | ViewPropTypes,
|
11 | ScrollView,
|
12 | Dimensions,
|
13 | TouchableOpacity,
|
14 | ViewPagerAndroid,
|
15 | Platform,
|
16 | ActivityIndicator
|
17 | } from 'react-native'
|
18 |
|
19 | const { width, height } = Dimensions.get('window')
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | const 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 |
|
102 |
|
103 | export default class extends Component {
|
104 | |
105 |
|
106 |
|
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 |
|
143 |
|
144 | onIndexChanged: PropTypes.func
|
145 | }
|
146 |
|
147 | |
148 |
|
149 |
|
150 |
|
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 |
|
175 |
|
176 |
|
177 | state = this.initState(this.props)
|
178 |
|
179 | |
180 |
|
181 |
|
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 |
|
202 | if (this.state.index !== nextState.index) this.props.onIndexChanged(nextState.index)
|
203 | }
|
204 |
|
205 | initState (props) {
|
206 |
|
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 |
|
218 | initState.index = state.index
|
219 | } else {
|
220 | initState.index = initState.total > 1 ? Math.min(props.index, initState.total - 1) : 0
|
221 | }
|
222 |
|
223 |
|
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 |
|
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 |
|
256 |
|
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 |
|
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 |
|
295 |
|
296 |
|
297 | onScrollBegin = e => {
|
298 |
|
299 | this.internals.isScrolling = true
|
300 | this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e, this.fullState(), this)
|
301 | }
|
302 |
|
303 | |
304 |
|
305 |
|
306 |
|
307 | onScrollEnd = e => {
|
308 |
|
309 | this.internals.isScrolling = false
|
310 |
|
311 |
|
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 |
|
325 | this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e, this.fullState(), this)
|
326 | })
|
327 | }
|
328 |
|
329 | |
330 |
|
331 |
|
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 |
|
349 |
|
350 |
|
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 |
|
360 | if (!diff) return
|
361 |
|
362 |
|
363 |
|
364 |
|
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 |
|
386 | if (loopJump) {
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
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 |
|
409 |
|
410 |
|
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 |
|
429 | this.internals.isScrolling = true
|
430 | this.setState({
|
431 | autoplayEnd: false
|
432 | })
|
433 |
|
434 |
|
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 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 | for (let prop in props) {
|
461 |
|
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 |
|
477 |
|
478 |
|
479 | renderPagination = () => {
|
480 |
|
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 | }
|