├── src ├── index.js ├── Swiper.android.js └── Swiper.ios.js ├── .npmignore ├── package.json ├── .gitignore ├── .editorconfig ├── LICENSE └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Swiper'); 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | logs 3 | *.log 4 | pids 5 | *.pid 6 | *.seed 7 | lib-cov 8 | coverage 9 | .lock-wscript 10 | build/Release 11 | node_modules 12 | res 13 | examples 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-swiper2", 3 | "keywords": [ 4 | "react-component", 5 | "react-native", 6 | "ios" 7 | ], 8 | "version": "2.0.7", 9 | "description": "Swiper component for React Native. Supersede react-native-swiper", 10 | "main": "src/index.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/sunnylqm/react-native-swiper2.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/sunnylqm/react-native-swiper2/issues" 19 | }, 20 | "homepage": "https://github.com/sunnylqm/react-native-swipe2#readme", 21 | "dependencies": { 22 | "react-timer-mixin": "^0.13.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | /node_modules 30 | 31 | # Xcode 32 | *.xcuserstate 33 | examples/swiper.xcodeproj/project.xcworkspace/ 34 | examples/swiper.xcodeproj/xcuserdata 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [**.js] 11 | indent_style = space 12 | indent_size = 2 13 | quote_type = single 14 | space_after_anon_function = true 15 | curly_bracket_next_line = true 16 | brace_style = end-expand 17 | 18 | [**.jsx] 19 | indent_style = space 20 | indent_size = 2 21 | quote_type = single 22 | space_after_anon_function = true 23 | curly_bracket_next_line = true 24 | brace_style = end-expand 25 | 26 | [**.css] 27 | indent_style = space 28 | indent_size = 4 29 | 30 | [**.html] 31 | indent_style = space 32 | indent_size = 4 33 | max_char = 78 34 | brace_style = expand 35 | 36 | [node_modules/**.js] 37 | codepaint = false 38 | 39 | [*.json] 40 | indent_style = space 41 | indent_size = 2 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 斯人 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Swiper.android.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | react-native-swiper 4 | 5 | @author leecade 6 | 7 | react-native-swiper2 8 | 9 | @author sunnylqm 10 | */ 11 | import React from 'react'; 12 | import { 13 | StyleSheet, 14 | Text, 15 | View, 16 | ScrollView, 17 | TouchableOpacity, 18 | Dimensions, 19 | Platform, 20 | ViewPagerAndroid 21 | } from 'react-native' 22 | 23 | // Using bare setTimeout, setInterval, setImmediate 24 | // and requestAnimationFrame calls is very dangerous 25 | // because if you forget to cancel the request before 26 | // the component is unmounted, you risk the callback 27 | // throwing an exception. 28 | import TimerMixin from 'react-timer-mixin' 29 | 30 | let { width, height } = Dimensions.get('window') 31 | 32 | /** 33 | * Default styles 34 | * @type {StyleSheetPropType} 35 | */ 36 | let styles = StyleSheet.create({ 37 | container: { 38 | backgroundColor: 'transparent', 39 | position: 'relative', 40 | }, 41 | 42 | wrapper: { 43 | backgroundColor: 'transparent', 44 | }, 45 | 46 | slide: { 47 | backgroundColor: 'transparent', 48 | }, 49 | 50 | pagination: { 51 | position: 'absolute', 52 | bottom: 25, 53 | left: 0, 54 | right: 0, 55 | flexDirection: 'row', 56 | flex: 1, 57 | justifyContent: 'center', 58 | alignItems: 'center', 59 | backgroundColor:'transparent', 60 | }, 61 | 62 | 63 | title: { 64 | height: 30, 65 | justifyContent: 'center', 66 | position: 'absolute', 67 | paddingLeft: 10, 68 | bottom: -30, 69 | left: 0, 70 | flexWrap: 'nowrap', 71 | width: 250, 72 | backgroundColor: 'transparent', 73 | }, 74 | 75 | buttonWrapper: { 76 | backgroundColor: 'transparent', 77 | flexDirection: 'row', 78 | position: 'absolute', 79 | top: 0, 80 | left: 0, 81 | flex: 1, 82 | paddingHorizontal: 10, 83 | paddingVertical: 10, 84 | justifyContent: 'space-between', 85 | alignItems: 'center' 86 | }, 87 | 88 | buttonText: { 89 | fontSize: 50, 90 | color: '#007aff', 91 | fontFamily: 'Arial', 92 | }, 93 | }) 94 | 95 | export default React.createClass({ 96 | 97 | /** 98 | * Props Validation 99 | * @type {Object} 100 | */ 101 | propTypes: { 102 | horizontal : React.PropTypes.bool, 103 | children : React.PropTypes.node.isRequired, 104 | style : View.propTypes.style, 105 | pagingEnabled : React.PropTypes.bool, 106 | showsHorizontalScrollIndicator : React.PropTypes.bool, 107 | showsVerticalScrollIndicator : React.PropTypes.bool, 108 | bounces : React.PropTypes.bool, 109 | scrollsToTop : React.PropTypes.bool, 110 | removeClippedSubviews : React.PropTypes.bool, 111 | automaticallyAdjustContentInsets : React.PropTypes.bool, 112 | showsPagination : React.PropTypes.bool, 113 | showsButtons : React.PropTypes.bool, 114 | loop : React.PropTypes.bool, 115 | autoplay : React.PropTypes.bool, 116 | autoplayTimeout : React.PropTypes.number, 117 | autoplayDirection : React.PropTypes.bool, 118 | index : React.PropTypes.number, 119 | renderPagination : React.PropTypes.func, 120 | onPageChanged : React.PropTypes.func, 121 | }, 122 | 123 | mixins: [TimerMixin], 124 | 125 | /** 126 | * Default props 127 | * @return {object} props 128 | * @see http://facebook.github.io/react-native/docs/scrollview.html 129 | */ 130 | getDefaultProps() { 131 | return { 132 | horizontal : true, 133 | pagingEnabled : true, 134 | showsHorizontalScrollIndicator : false, 135 | showsVerticalScrollIndicator : false, 136 | bounces : false, 137 | scrollsToTop : false, 138 | removeClippedSubviews : true, 139 | automaticallyAdjustContentInsets : false, 140 | showsPagination : true, 141 | showsButtons : false, 142 | loop : true, 143 | autoplay : false, 144 | autoplayTimeout : 2.5, 145 | autoplayDirection : true, 146 | index : 0, 147 | } 148 | }, 149 | 150 | /** 151 | * Init states 152 | * @return {object} states 153 | */ 154 | getInitialState() { 155 | let props = this.props 156 | 157 | let initState = { 158 | autoplayEnd: false, 159 | } 160 | 161 | initState.total = props.children 162 | ? (props.children.length || 1) 163 | : 0 164 | 165 | initState.index = initState.total > 1 166 | ? Math.min(props.index, initState.total - 1) 167 | : 0 168 | 169 | // Default: horizontal 170 | initState.width = props.width || width 171 | initState.height = props.height || height 172 | 173 | return initState 174 | }, 175 | 176 | /** 177 | * autoplay timer 178 | * @type {null} 179 | */ 180 | autoplayTimer: null, 181 | 182 | componentWillMount() { 183 | this.props = this.injectState(this.props) 184 | }, 185 | 186 | componentDidMount() { 187 | this.autoplay() 188 | }, 189 | 190 | autoplay(){ 191 | if(!this.props.autoplay 192 | || this.state.autoplayEnd) return; 193 | console.log("Will autoplay."); 194 | clearTimeout(this.autoplayTimer) 195 | this.autoplayTimer = this.setTimeout(() => { 196 | if(!this.props.loop && (this.props.autoplayDirection 197 | ? this.state.index == this.state.total - 1 198 | : this.state.index == 0)) return this.setState({ 199 | autoplayEnd: true 200 | }) 201 | this.viewPager && console.log("Now autoplay."+this.state.index + (this.props.loop?1:0) +(this.props.autoplayDirection?1:-1)); 202 | this.viewPager && this.viewPager.setPage(this.state.index + (this.props.loop?1:0) +(this.props.autoplayDirection?1:-1)); 203 | }, this.props.autoplayTimeout * 1000) 204 | }, 205 | 206 | onPageSelected(ev){ 207 | 208 | }, 209 | 210 | onPageScroll(ev){ 211 | const hasLoop = this.props.loop && this.state.total > 1; 212 | const lastPage = this.state.index + (hasLoop ? 1 : 0); 213 | 214 | let page = ev.nativeEvent.position; 215 | if (hasLoop){ 216 | // do Loop 217 | if (page + ev.nativeEvent.offset <= 0){ 218 | page = this.state.total; 219 | this.viewPager && this.viewPager.setPageWithoutAnimation(page); 220 | } else if(page+ev.nativeEvent.offset >= this.state.total+1){ 221 | page = 1; 222 | this.viewPager && this.viewPager.setPageWithoutAnimation(page); 223 | } 224 | } 225 | this.autoplay(); 226 | 227 | if (page != lastPage){ 228 | this.setState({ 229 | index: page, 230 | }); 231 | this.props.onPageChanged && this.props.onPageChanged(page); 232 | } 233 | }, 234 | 235 | renderScrollView(pages) { 236 | return ( 237 | this.viewPager = val } 243 | > 244 | {pages} 245 | 246 | ); 247 | }, 248 | 249 | /** 250 | * Scroll by index 251 | * @param {number} index offset index 252 | */ 253 | scrollTo(indexOffset) { 254 | let newIndex = (this.props.loop ? 1 : 0) + indexOffset + this.state.index 255 | this.viewPager && this.viewPager.setPage(newIndex); 256 | }, 257 | 258 | /** 259 | * Render pagination 260 | * @return {object} react-dom 261 | */ 262 | renderPagination() { 263 | 264 | // By default, dots only show when `total` > 2 265 | if(this.state.total <= 1) return null 266 | 267 | let dots = [] 268 | let ActiveDot = this.props.activeDot || ; 278 | let Dot = this.props.dot || ; 288 | for(let i = 0; i < this.state.total; i++) { 289 | dots.push(i === this.state.index 290 | ? 291 | React.cloneElement(ActiveDot, {key: i}) 292 | : 293 | React.cloneElement(Dot, {key: i}) 294 | ) 295 | } 296 | 297 | return ( 298 | 299 | {dots} 300 | 301 | ) 302 | }, 303 | 304 | renderTitle() { 305 | let child = this.props.children[this.state.index] 306 | let title = child && child.props.title 307 | return title 308 | ? ( 309 | 310 | {this.props.children[this.state.index].props.title} 311 | 312 | ) 313 | : null 314 | }, 315 | 316 | renderButtons() { 317 | 318 | let nextButton = this.props.nextButton || 319 | 320 | let prevButton = this.props.prevButton || 321 | 322 | return ( 323 | 324 | !(!this.props.loop && this.state.index == 0) && this.scrollTo.call(this, -1)}> 325 | 326 | {prevButton} 327 | 328 | 329 | !(!this.props.loop && this.state.index == this.state.total - 1) && this.scrollTo.call(this, 1)}> 330 | 331 | {nextButton} 332 | 333 | 334 | 335 | ) 336 | }, 337 | 338 | /** 339 | * Inject state to ScrollResponder 340 | * @param {object} props origin props 341 | * @return {object} props injected props 342 | */ 343 | injectState(props) { 344 | /* const scrollResponders = [ 345 | 'onMomentumScrollBegin', 346 | 'onTouchStartCapture', 347 | 'onTouchStart', 348 | 'onTouchEnd', 349 | 'onResponderRelease', 350 | 'onPageChanged', 351 | ]*/ 352 | 353 | for(let prop in props) { 354 | // if(~scrollResponders.indexOf(prop) 355 | if(typeof props[prop] === 'function' 356 | && prop !== 'onMomentumScrollEnd' 357 | && prop !== 'renderPagination' 358 | && prop !== 'onScrollBeginDrag' 359 | && prop !== 'onPageChanged' 360 | ) { 361 | let originResponder = props[prop] 362 | props[prop] = (e) => originResponder(e, this.state, this) 363 | } 364 | } 365 | 366 | return props 367 | }, 368 | 369 | /** 370 | * Default render 371 | * @return {object} react-dom 372 | */ 373 | render() { 374 | let state = this.state 375 | let props = this.props 376 | let children = props.children 377 | let index = state.index 378 | let total = state.total 379 | let loop = props.loop 380 | let dir = state.dir 381 | let key = 0 382 | 383 | let pages = [] 384 | let pageStyle = [{width: state.width, height: state.height}, styles.slide] 385 | 386 | // For make infinite at least total > 1 387 | if(total > 1) { 388 | 389 | // Re-design a loop model for avoid img flickering 390 | pages = Object.keys(children) 391 | if(loop) { 392 | pages.unshift(total - 1) 393 | pages.push(0) 394 | } 395 | 396 | pages = pages.map((page, i) => 397 | {children[page]} 398 | ) 399 | } 400 | else pages = {children} 401 | 402 | return ( 403 | 407 | {this.renderScrollView(pages)} 408 | {props.showsPagination && (props.renderPagination 409 | ? this.props.renderPagination(state.index, state.total, this) 410 | : this.renderPagination())} 411 | {this.renderTitle()} 412 | {this.props.showsButtons && this.renderButtons()} 413 | 414 | ) 415 | } 416 | }) 417 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-swiper2 2 | 3 | [![npm version](http://img.shields.io/npm/v/react-native-swiper2.svg?style=flat-square)](https://npmjs.org/package/react-native-swiper2 "View this project on npm") 4 | 5 | ![logo](http://i.imgur.com/P4cRUgD.png) 6 | 7 | ## [react-native-swiper](https://github.com/leecade/react-native-swiper) is now active again, so swiper2 will stop maintaining. 8 | 9 | ## Changelogs 10 | 11 | - **[v2.0.7]** 12 | + correct onPageChange index. 13 | 14 | - **[v2.0.6]** 15 | + support RN 0.25+. ([@xirc](https://github.com/xirc)). 16 | 17 | - **[v2.0.5]** 18 | + add scrollTo for android. ([@LeeChSien](https://github.com/LeeChSien)). 19 | 20 | - **[v2.0.4]** 21 | + fix scrollTo warning. 22 | 23 | - **[v2.0.3]** 24 | + support pagination on android. 25 | 26 | - **[v2.0.2]** 27 | + add simple support for android. 28 | 29 | - **[v2.0.1]** 30 | + fix the dimensions module error. 31 | + fix the keys warning when render dots. 32 | 33 | - **[v1.3.0]** 34 | + [8d6d75c](https://github.com/leecade/react-native-swiper/commit/8d6d75c00edf87b603c218aad0018932277814b5) inject `state` in ScrollResponders (@thanks [@smothers](https://github.com/smothers)). 35 | 36 | - [v1.2.2] 37 | + [890c0ce](https://github.com/leecade/react-native-swiper/commit/890c0ce64e2192c2ca7830e6699f67b88171e74b) ensure `onMomentumScrollEnd` synchronous update `index`. 38 | 39 | - [v1.2.0] 40 | + [838b24c](https://github.com/leecade/react-native-swiper/commit/838b24cbeaf49b9ca1dabb4eed8305e314503fb1) Re-design a loop model for avoid img flickering. 41 | + [9cb91c5](https://github.com/leecade/react-native-swiper/commit/9cb91c58c84034b0b8b874dbfc2a44da982686a8) fixes [#7](https://github.com/leecade/react-native-swiper/issues/6) `onMomentumScrollEnd` lost `event` param. (thanks [@smothers](https://github.com/smothers)) 42 | 43 | - [v1.1.1] 44 | + [21f0b00](https://github.com/leecade/react-native-swiper/commit/21f0b00138b6936cd3dfac2eb107a14c99c7392b) fixes [#6](https://github.com/leecade/react-native-swiper/issues/6) a define-propType error. (thanks [@benjamingeorge](https://github.com/benjamingeorge)) 45 | 46 | - [v1.1.0] 47 | + [44ec630](https://github.com/leecade/react-native-swiper/commit/44ec630b62844dbeaccee73adaa0996e319ebffb) fixes [#4](https://github.com/leecade/react-native-swiper/issues/4) `onMoementumScrollEnd` gets overridden. (thanks [@subpublicanders](https://github.com/subpublicanders)) 48 | + [5de06a7](https://github.com/leecade/react-native-swiper/commit/5de06a7aa86318ad38720728022b80e5cf98a2ab) New prop: `renderPagination`. (thanks [@aksonov](https://github.com/aksonov)) 49 | 50 | - [v1.0.4] 51 | + [21cb373](https://github.com/leecade/react-native-swiper/commit/21cb3732578588f9d47ee7ddda541577ad691970) fixes [#2](https://github.com/leecade/react-native-swiper/issues/2) Solve the problem of installation. (thanks [@jamwaffles](https://github.com/jamwaffles)) 52 | 53 | - [v1.0.3] 54 | + [0f796f3](https://github.com/leecade/react-native-swiper/commit/0f796f3557b5aeb1772573cd7ecae2e835bccc0b) fixes [#1](https://github.com/leecade/react-native-swiper/issues/1) Two 'horizontal' in propTypes. (thanks [@MacyzZ](https://github.com/MacyzZ)) 55 | 56 | ## Show Cases 57 | 58 | > Try these cases by yourself very easy, Just open `examples/swiper.xcodeproj` with Xcode, then press `Cmd + R`; you may edit `examples/index.ios.js` for switch cases. 59 | 60 | ### [examples/basic.js](https://github.com/leecade/react-native-swiper/blob/master/examples/examples/basic.js) 61 | 62 | ![](http://i.imgur.com/zrsazAG.gif =300x) 63 | 64 | ### [examples/swiper.js](https://github.com/leecade/react-native-swiper/blob/master/examples/examples/swiper.js) 65 | 66 | ![](http://i.imgur.com/hP3f3oO.gif =300x) 67 | 68 | ### [examples/swiper_number.js](https://github.com/leecade/react-native-swiper/blob/master/examples/examples/swiper_number.js) 69 | 70 | ![](http://i.imgur.com/0rqESVb.gif =300x) 71 | 72 | ### [examples/phone.js](https://github.com/leecade/react-native-swiper/blob/master/examples/examples/phone.js) 73 | 74 | ![](http://i.imgur.com/c1BhjZm.gif =300x) 75 | 76 | ## Getting Started 77 | 78 | - [Installation](#installation) 79 | - [Basic Usage](#basic-usage) 80 | - [Properties](#properties) 81 | + [Basic](#basic) 82 | + [Custom basic style & content](#custom-basic-style--content) 83 | + [Pagination](#pagination) 84 | + [Autoplay](#autoplay) 85 | + [Control buttons](#control-buttons) 86 | + [Props of Children](#props-of-children) 87 | + [Basic props of ``](#basic-props-of-scrollview-) 88 | + [Supported ScrollResponder](#supported-scrollresponder) 89 | - [Examples](#examples) 90 | - [Development](#development) 91 | 92 | ### Installation 93 | 94 | ```bash 95 | $ npm i react-native-swiper2 --save 96 | ``` 97 | 98 | ### Basic Usage 99 | 100 | - Install `react-native-cli` first 101 | 102 | ```bash 103 | $ npm i react-native-cli -g 104 | ``` 105 | 106 | - Initialization of a react-native project 107 | 108 | ```bash 109 | $ react-native init myproject 110 | ``` 111 | 112 | - Then, edit `myproject/index.ios.js`, like this: 113 | 114 | ```jsx 115 | var Swiper = require('react-native-swiper2') 116 | // es6 117 | // import Swiper from 'react-native-swiper2' 118 | 119 | var React = require('react-native'); 120 | var { 121 | AppRegistry, 122 | StyleSheet, 123 | Text, 124 | View, 125 | } = React; 126 | 127 | var styles = StyleSheet.create({ 128 | wrapper: { 129 | }, 130 | slide1: { 131 | flex: 1, 132 | justifyContent: 'center', 133 | alignItems: 'center', 134 | backgroundColor: '#9DD6EB', 135 | }, 136 | slide2: { 137 | flex: 1, 138 | justifyContent: 'center', 139 | alignItems: 'center', 140 | backgroundColor: '#97CAE5', 141 | }, 142 | slide3: { 143 | flex: 1, 144 | justifyContent: 'center', 145 | alignItems: 'center', 146 | backgroundColor: '#92BBD9', 147 | }, 148 | text: { 149 | color: '#fff', 150 | fontSize: 30, 151 | fontWeight: 'bold', 152 | } 153 | }) 154 | 155 | var swiper = React.createClass({ 156 | render: function() { 157 | return ( 158 | 159 | 160 | Hello Swiper 161 | 162 | 163 | Beautiful 164 | 165 | 166 | And simple 167 | 168 | 169 | ) 170 | } 171 | }) 172 | 173 | AppRegistry.registerComponent('swiper', () => swiper) 174 | ``` 175 | 176 | ### Properties 177 | 178 | #### Basic 179 | 180 | | Prop | Default | Type | Description | 181 | | :------------ |:---------------:| :---------------:| :-----| 182 | | horizontal | true | `bool` | If `true`, the scroll view's children are arranged horizontally in a row instead of vertically in a column. | 183 | | loop | true | `bool` | Set to `true` to enable continuous loop mode. | 184 | | index | 0 | `number` | Index number of initial slide. | 185 | | showsButtons | false | `bool` | Set to `true` make control buttons visible. | 186 | | autoplay | false | `bool` | Set to `true` enable auto play mode. | 187 | 188 | #### Custom basic style & content 189 | 190 | | Prop | Default | Type | Description | 191 | | :------------ |:---------------:| :---------------:| :-----| 192 | | width | - | `number` | If no specify default enable fullscreen mode by `flex: 1`. | 193 | | height | - | `number` | If no specify default fullscreen mode by `flex: 1`. | 194 | | style | {...} | `style` | See default style in source. | 195 | 196 | #### Pagination 197 | 198 | | Prop | Default | Type | Description | 199 | | :------------ |:---------------:| :---------------:| :-----| 200 | | showsPagination | true | `bool` | Set to `true` make pagination visible. | 201 | | paginationStyle | {...} | `style` | Custom styles will merge with the default styles. | 202 | | renderPagination | - | `function` | Complete control how to render pagination with three params (`index`, `total`, `context`) ref to `this.state.index` / `this.state.total` / `this`, For example: show numbers instead of dots. | 203 | | dot | `` | `element` | Allow custom the dot element. | 204 | | activeDot | `` | `element` | Allow custom the active-dot element. | 205 | 206 | #### Autoplay 207 | 208 | | Prop | Default | Type | Description | 209 | | :------------ |:---------------:| :---------------:| :-----| 210 | | autoplay | true | `bool` | Set to `true` enable auto play mode. | 211 | | autoplayTimeout | 2.5 | `number` | Delay between auto play transitions (in second). | 212 | | autoplayDirection | true | `bool` | Cycle direction control. | 213 | 214 | #### Control buttons 215 | 216 | | Prop | Default | Type | Description | 217 | | :------------ |:---------------:| :---------------:| :-----| 218 | | showsButtons | true | `bool` | Set to `true` make control buttons visible. | 219 | | buttonWrapperStyle | `{backgroundColor: 'transparent', flexDirection: 'row', position: 'absolute', top: 0, left: 0, flex: 1, paddingHorizontal: 10, paddingVertical: 10, justifyContent: 'space-between', alignItems: 'center'}` | `style` | Custom styles. | 220 | | nextButton | `` | `element` | Allow custom the next button. | 221 | | prevButton | `` | `element` | Allow custom the prev button. | 222 | 223 | #### Props of Children 224 | 225 | | Prop | Default | Type | Description | 226 | | :------------ |:---------------:| :---------------:| :-----| 227 | | style | {...} | `style` | Custom styles will merge with the default styles. | 228 | | title | {...} | `element` | If this parameter is not specified, will not render the title. | 229 | 230 | #### Basic props of `` 231 | 232 | | Prop | Default | Type | Description | 233 | | :------------ |:---------------:| :---------------:| :-----| 234 | | horizontal | true | `bool` | If `true`, the scroll view's children are arranged horizontally in a row instead of vertically in a column. | 235 | | pagingEnabled | true | `bool` | If true, the scroll view stops on multiples of the scroll view's size when scrolling. This can be used for horizontal pagination. | 236 | | showsHorizontalScrollIndicator | false | `bool` | Set to `true` if you want to show horizontal scroll bar. | 237 | | showsVerticalScrollIndicator | false | `bool` | Set to `true` if you want to show vertical scroll bar. | 238 | | bounces | false | `bool` | If `true`, the scroll view bounces when it reaches the end of the content if the content is larger then the scroll view along the axis of the scroll direction. If `false`, it disables all bouncing even if the alwaysBounce* props are true. | 239 | | scrollsToTop | false | `bool` | If true, the scroll view scrolls to top when the status bar is tapped. | 240 | | removeClippedSubviews | true | `bool` | If true, offscreen child views (whose overflow value is hidden) are removed from their native backing superview when offscreen. This canimprove scrolling performance on long lists. | 241 | | automaticallyAdjustContentInsets | false | `bool` | Set to `true` if you need adjust content insets automation. | 242 | 243 | > @see: http://facebook.github.io/react-native/docs/scrollview.html 244 | 245 | #### Supported ScrollResponder 246 | 247 | | Prop | Params | Type | Description | 248 | | :------------ |:---------------:| :---------------:| :-----| 249 | | onMomentumScrollBegin | `e` / `state` / `context` | `function` | When animation begins after letting up | 250 | | onMomentumScrollEnd | `e` / `state` / `context` | `function` | Makes no sense why this occurs first during bounce | 251 | | onTouchStartCapture | `e` / `state` / `context` | `function` | Immediately after `onMomentumScrollEnd` | 252 | | onTouchStart | `e` / `state` / `context` | `function` | Same, but bubble phase | 253 | | onTouchEnd | `e` / `state` / `context` | `function` | You could hold the touch start for a long time | 254 | | onResponderRelease | `e` / `state` / `context` | `function` | When lifting up - you could pause forever before * lifting | 255 | 256 | > Note: each ScrollResponder be injected with two params: `state` and `context`, you can get `state` and `context`(ref to swiper's `this`) from params, for example: 257 | 258 | ```jsx 259 | var swiper = React.createClass({ 260 | _onMomentumScrollEnd: function (e, state, context) { 261 | console.log(state, context.state) 262 | }, 263 | render: function() { 264 | return ( 265 | 269 | ) 270 | } 271 | }) 272 | ``` 273 | 274 | > More ScrollResponder info, see: https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollResponder.js 275 | 276 | ### Examples 277 | 278 | @TODO, see code in [examples](https://github.com/leecade/react-native-swiper/tree/master/examples/examples) first. 279 | 280 | ### Development 281 | 282 | ```bash 283 | $ npm start 284 | ``` 285 | 286 | ## Contribution 287 | 288 | - [@leecade](mailto:leecade@163.com) The main author of the original version . 289 | 290 | ## Questions 291 | 292 | Feel free to [create an issue](https://github.com/sunnylqm/react-native-swiper2/issues/new) 293 | 294 | > Inspired by [nolimits4web/Swiper](https://github.com/nolimits4web/swiper/) & Design material from [Dribbble](https://dribbble.com/) & made with ♥. 295 | -------------------------------------------------------------------------------- /src/Swiper.ios.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | react-native-swiper 4 | 5 | @author leecade 6 | 7 | react-native-swiper2 8 | 9 | @author sunnylqm 10 | */ 11 | import React from 'react'; 12 | import { 13 | StyleSheet, 14 | Text, 15 | View, 16 | ScrollView, 17 | TouchableOpacity, 18 | Dimensions, 19 | Platform, 20 | } from 'react-native' 21 | 22 | // Using bare setTimeout, setInterval, setImmediate 23 | // and requestAnimationFrame calls is very dangerous 24 | // because if you forget to cancel the request before 25 | // the component is unmounted, you risk the callback 26 | // throwing an exception. 27 | import TimerMixin from 'react-timer-mixin' 28 | 29 | let { width, height } = Dimensions.get('window') 30 | 31 | /** 32 | * Default styles 33 | * @type {StyleSheetPropType} 34 | */ 35 | let styles = StyleSheet.create({ 36 | container: { 37 | backgroundColor: 'transparent', 38 | position: 'relative', 39 | }, 40 | 41 | wrapper: { 42 | backgroundColor: 'transparent', 43 | }, 44 | 45 | slide: { 46 | backgroundColor: 'transparent', 47 | }, 48 | 49 | pagination_x: { 50 | position: 'absolute', 51 | bottom: 25, 52 | left: 0, 53 | right: 0, 54 | flexDirection: 'row', 55 | flex: 1, 56 | justifyContent: 'center', 57 | alignItems: 'center', 58 | backgroundColor:'transparent', 59 | }, 60 | 61 | pagination_y: { 62 | position: 'absolute', 63 | right: 15, 64 | top: 0, 65 | bottom: 0, 66 | flexDirection: 'column', 67 | flex: 1, 68 | justifyContent: 'center', 69 | alignItems: 'center', 70 | backgroundColor:'transparent', 71 | }, 72 | 73 | title: { 74 | height: 30, 75 | justifyContent: 'center', 76 | position: 'absolute', 77 | paddingLeft: 10, 78 | bottom: -30, 79 | left: 0, 80 | flexWrap: 'nowrap', 81 | width: 250, 82 | backgroundColor: 'transparent', 83 | }, 84 | 85 | buttonWrapper: { 86 | backgroundColor: 'transparent', 87 | flexDirection: 'row', 88 | position: 'absolute', 89 | top: 0, 90 | left: 0, 91 | flex: 1, 92 | paddingHorizontal: 10, 93 | paddingVertical: 10, 94 | justifyContent: 'space-between', 95 | alignItems: 'center' 96 | }, 97 | 98 | buttonText: { 99 | fontSize: 50, 100 | color: '#007aff', 101 | fontFamily: 'Arial', 102 | }, 103 | }) 104 | 105 | export default React.createClass({ 106 | 107 | /** 108 | * Props Validation 109 | * @type {Object} 110 | */ 111 | propTypes: { 112 | horizontal : React.PropTypes.bool, 113 | children : React.PropTypes.node.isRequired, 114 | style : View.propTypes.style, 115 | pagingEnabled : React.PropTypes.bool, 116 | showsHorizontalScrollIndicator : React.PropTypes.bool, 117 | showsVerticalScrollIndicator : React.PropTypes.bool, 118 | bounces : React.PropTypes.bool, 119 | scrollsToTop : React.PropTypes.bool, 120 | removeClippedSubviews : React.PropTypes.bool, 121 | automaticallyAdjustContentInsets : React.PropTypes.bool, 122 | showsPagination : React.PropTypes.bool, 123 | showsButtons : React.PropTypes.bool, 124 | loop : React.PropTypes.bool, 125 | autoplay : React.PropTypes.bool, 126 | autoplayTimeout : React.PropTypes.number, 127 | autoplayDirection : React.PropTypes.bool, 128 | index : React.PropTypes.number, 129 | renderPagination : React.PropTypes.func, 130 | onPageChanged : React.PropTypes.func, 131 | }, 132 | 133 | mixins: [TimerMixin], 134 | 135 | /** 136 | * Default props 137 | * @return {object} props 138 | * @see http://facebook.github.io/react-native/docs/scrollview.html 139 | */ 140 | getDefaultProps() { 141 | return { 142 | horizontal : true, 143 | pagingEnabled : true, 144 | showsHorizontalScrollIndicator : false, 145 | showsVerticalScrollIndicator : false, 146 | bounces : false, 147 | scrollsToTop : false, 148 | removeClippedSubviews : true, 149 | automaticallyAdjustContentInsets : false, 150 | showsPagination : true, 151 | showsButtons : false, 152 | loop : true, 153 | autoplay : false, 154 | autoplayTimeout : 2.5, 155 | autoplayDirection : true, 156 | index : 0, 157 | } 158 | }, 159 | 160 | /** 161 | * Init states 162 | * @return {object} states 163 | */ 164 | getInitialState() { 165 | let props = this.props 166 | 167 | let initState = { 168 | isScrolling: false, 169 | autoplayEnd: false, 170 | } 171 | 172 | initState.total = props.children 173 | ? (props.children.length || 1) 174 | : 0 175 | 176 | initState.index = initState.total > 1 177 | ? Math.min(props.index, initState.total - 1) 178 | : 0 179 | 180 | // Default: horizontal 181 | initState.dir = props.horizontal == false ? 'y' : 'x' 182 | initState.width = props.width || width 183 | initState.height = props.height || height 184 | initState.offset = {} 185 | 186 | if(initState.total > 1) { 187 | let setup = props.loop ? 1 : initState.index 188 | initState.offset[initState.dir] = initState.dir == 'y' 189 | ? initState.height * setup 190 | : initState.width * setup 191 | } 192 | 193 | return initState 194 | }, 195 | 196 | /** 197 | * autoplay timer 198 | * @type {null} 199 | */ 200 | autoplayTimer: null, 201 | 202 | componentWillMount() { 203 | this.props = this.injectState(this.props) 204 | }, 205 | 206 | componentDidMount() { 207 | this.autoplay() 208 | }, 209 | 210 | /** 211 | * Automatic rolling 212 | */ 213 | autoplay() { 214 | if(!this.props.autoplay 215 | || this.state.isScrolling 216 | || this.state.autoplayEnd) return 217 | 218 | clearTimeout(this.autoplayTimer) 219 | 220 | this.autoplayTimer = this.setTimeout(() => { 221 | if(!this.props.loop && (this.props.autoplayDirection 222 | ? this.state.index == this.state.total - 1 223 | : this.state.index == 0)) return this.setState({ 224 | autoplayEnd: true 225 | }) 226 | this.scrollTo(this.props.autoplayDirection ? 1 : -1) 227 | }, this.props.autoplayTimeout * 1000) 228 | }, 229 | 230 | /** 231 | * Scroll begin handle 232 | * @param {object} e native event 233 | */ 234 | onScrollBegin(e) { 235 | // update scroll state 236 | this.setState({ 237 | isScrolling: true 238 | }) 239 | 240 | this.setTimeout(() => { 241 | this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e, this.state, this) 242 | }) 243 | }, 244 | 245 | /** 246 | * Scroll end handle 247 | * @param {object} e native event 248 | */ 249 | onScrollEnd(e) { 250 | 251 | // update scroll state 252 | this.setState({ 253 | isScrolling: false 254 | }) 255 | 256 | this.updateIndex(e.nativeEvent.contentOffset, this.state.dir) 257 | 258 | // Note: `this.setState` is async, so I call the `onMomentumScrollEnd` 259 | // in setTimeout to ensure synchronous update `index` 260 | this.setTimeout(() => { 261 | this.autoplay() 262 | 263 | // if `onMomentumScrollEnd` registered will be called here 264 | this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e, this.state, this) 265 | }) 266 | }, 267 | 268 | /** 269 | * Update index after scroll 270 | * @param {object} offset content offset 271 | * @param {string} dir 'x' || 'y' 272 | */ 273 | updateIndex(offset, dir) { 274 | 275 | let state = this.state 276 | let index = state.index 277 | let diff = offset[dir] - state.offset[dir] 278 | let step = dir == 'x' ? state.width : state.height 279 | 280 | // Do nothing if offset no change. 281 | if(!diff) return 282 | 283 | // Note: if touch very very quickly and continuous, 284 | // the variation of `index` more than 1. 285 | index = index + diff / step 286 | 287 | if(this.props.loop) { 288 | if(index <= -1) { 289 | index = state.total - 1 290 | offset[dir] = step * state.total 291 | } 292 | else if(index >= state.total) { 293 | index = 0 294 | offset[dir] = step 295 | } 296 | } 297 | 298 | this.setState({ 299 | index: index, 300 | offset: offset, 301 | }) 302 | 303 | this.props.onPageChanged && this.props.onPageChanged(index) 304 | }, 305 | 306 | /** 307 | * Scroll by index 308 | * @param {number} index offset index 309 | */ 310 | scrollTo(index) { 311 | if(this.state.isScrolling) return 312 | let state = this.state 313 | let diff = (this.props.loop ? 1 : 0) + index + this.state.index 314 | let x = 0 315 | let y = 0 316 | if(state.dir == 'x') x = diff * state.width 317 | if(state.dir == 'y') y = diff * state.height 318 | this.refs.scrollView && this.refs.scrollView.scrollTo({ x, y, animated: true }) 319 | 320 | // update scroll state 321 | this.setState({ 322 | isScrolling: true, 323 | autoplayEnd: false, 324 | }) 325 | }, 326 | 327 | /** 328 | * Render pagination 329 | * @return {object} react-dom 330 | */ 331 | renderPagination() { 332 | 333 | // By default, dots only show when `total` > 2 334 | if(this.state.total <= 1) return null 335 | 336 | let dots = [] 337 | let ActiveDot = this.props.activeDot || ; 347 | let Dot = this.props.dot || ; 357 | for(let i = 0; i < this.state.total; i++) { 358 | dots.push(i === this.state.index 359 | ? 360 | React.cloneElement(ActiveDot, {key: i}) 361 | : 362 | React.cloneElement(Dot, {key: i}) 363 | ) 364 | } 365 | 366 | return ( 367 | 368 | {dots} 369 | 370 | ) 371 | }, 372 | 373 | renderTitle() { 374 | let child = this.props.children[this.state.index] 375 | let title = child && child.props.title 376 | return title 377 | ? ( 378 | 379 | {this.props.children[this.state.index].props.title} 380 | 381 | ) 382 | : null 383 | }, 384 | 385 | renderButtons() { 386 | 387 | let nextButton = this.props.nextButton || 388 | 389 | let prevButton = this.props.prevButton || 390 | 391 | return ( 392 | 393 | !(!this.props.loop && this.state.index == 0) && this.scrollTo.call(this, -1)}> 394 | 395 | {prevButton} 396 | 397 | 398 | !(!this.props.loop && this.state.index == this.state.total - 1) && this.scrollTo.call(this, 1)}> 399 | 400 | {nextButton} 401 | 402 | 403 | 404 | ) 405 | }, 406 | 407 | renderScrollView(pages) { 408 | return ( 409 | 416 | {pages} 417 | 418 | ); 419 | }, 420 | 421 | /** 422 | * Inject state to ScrollResponder 423 | * @param {object} props origin props 424 | * @return {object} props injected props 425 | */ 426 | injectState(props) { 427 | /* const scrollResponders = [ 428 | 'onMomentumScrollBegin', 429 | 'onTouchStartCapture', 430 | 'onTouchStart', 431 | 'onTouchEnd', 432 | 'onResponderRelease', 433 | 'onPageChanged', 434 | ]*/ 435 | 436 | for(let prop in props) { 437 | // if(~scrollResponders.indexOf(prop) 438 | if(typeof props[prop] === 'function' 439 | && prop !== 'onMomentumScrollEnd' 440 | && prop !== 'renderPagination' 441 | && prop !== 'onScrollBeginDrag' 442 | && prop !== 'onPageChanged' 443 | ) { 444 | let originResponder = props[prop] 445 | props[prop] = (e) => originResponder(e, this.state, this) 446 | } 447 | } 448 | 449 | return props 450 | }, 451 | 452 | /** 453 | * Default render 454 | * @return {object} react-dom 455 | */ 456 | render() { 457 | let state = this.state 458 | let props = this.props 459 | let children = props.children 460 | let index = state.index 461 | let total = state.total 462 | let loop = props.loop 463 | let dir = state.dir 464 | let key = 0 465 | 466 | let pages = [] 467 | let pageStyle = [{width: state.width, height: state.height}, styles.slide] 468 | 469 | // For make infinite at least total > 1 470 | if(total > 1) { 471 | 472 | // Re-design a loop model for avoid img flickering 473 | pages = Object.keys(children) 474 | if(loop) { 475 | pages.unshift(total - 1) 476 | pages.push(0) 477 | } 478 | 479 | pages = pages.map((page, i) => 480 | {children[page]} 481 | ) 482 | } 483 | else pages = {children} 484 | 485 | return ( 486 | 490 | {this.renderScrollView(pages)} 491 | {props.showsPagination && (props.renderPagination 492 | ? this.props.renderPagination(state.index, state.total, this) 493 | : this.renderPagination())} 494 | {this.renderTitle()} 495 | {this.props.showsButtons && this.renderButtons()} 496 | 497 | ) 498 | } 499 | }) 500 | --------------------------------------------------------------------------------