├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── screencasts ├── horizontal.gif └── vertical.gif /.gitignore: -------------------------------------------------------------------------------- 1 | *.[aod] 2 | *.DS_Store 3 | .DS_Store 4 | *Thumbs.db 5 | *.iml 6 | .gradle 7 | .idea 8 | node_modules 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .DS_Store 3 | *Thumbs.db 4 | .gradle 5 | .idea 6 | *.iml 7 | npm-debug.log 8 | node_modules 9 | screencasts 10 | /android/build 11 | /ios/**/*xcuserdata* 12 | /ios/**/*xcshareddata* 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015-2016 YunJiang.Fang <42550564@qq.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Card Swiper (remobile) 2 | A react-native card swiper write in js 3 | 4 | ## Installation 5 | ```sh 6 | npm install @remobile/react-native-card-swiper --save 7 | ``` 8 | 9 | ## Usage 10 | 11 | ### Example 12 | ```js 13 | 'use strict'; 14 | 15 | var React = require('react'); 16 | var ReactNative = require('react-native'); 17 | var { 18 | StyleSheet, 19 | View, 20 | Text, 21 | } = ReactNative; 22 | var CardSwiper = require('@remobile/react-native-card-swiper'); 23 | 24 | module.exports = React.createClass({ 25 | getDefaultProps: function() { 26 | return { 27 | vertical: false, 28 | }; 29 | }, 30 | renderRow(obj, index) { 31 | return ( 32 | 33 | {obj} 34 | 35 | ) 36 | }, 37 | onPressRow(obj, index) { 38 | console.log('onPressRow', obj, index); 39 | }, 40 | onChange(obj, index) { 41 | console.log('onChange', obj, index); 42 | }, 43 | render() { 44 | const {vertical} = this.props; 45 | return ( 46 | 47 | 57 | 58 | ); 59 | } 60 | }); 61 | 62 | 63 | var styles = StyleSheet.create({ 64 | container: { 65 | flex: 1, 66 | paddingTop: 100, 67 | }, 68 | panel: { 69 | backgroundColor: 'green', 70 | flex: 1, 71 | alignItems: 'center', 72 | justifyContent: 'center', 73 | }, 74 | }); 75 | ``` 76 | 77 | ## Screencasts 78 | 79 | ![vertical](https://github.com/remobile/react-native-card-swiper/blob/master/screencasts/vertical.gif) 80 | ![horizontal](https://github.com/remobile/react-native-card-swiper/blob/master/screencasts/horizontal.gif) 81 | 82 | #### Props 83 | - `list: PropTypes.list` card data list 84 | - `index: PropTypes.number` card initial index: default(0) 85 | - `width: PropTypes.number.required` card item width 86 | - `height: PropTypes.number.required` card item height 87 | - `loop: propTypes.boolean` swiper is loop: default(false) 88 | - `vertical: propTypes.boolean` swiper derection is vertical: default(false) 89 | - `renderRow: PropTypes.func [args: data, index]` row render function 90 | - `onPress: PropTypes.func [args: data, index]` row press callback function 91 | - `onChange: PropTypes.func [args: data, index]` row change callback function 92 | 93 | #### Method 94 | - `scrollTo(index)` scroll to index card 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | const ReactNative = require('react-native'); 5 | const { 6 | View, 7 | TouchableOpacity, 8 | ScrollView, 9 | Animated, 10 | InteractionManager, 11 | } = ReactNative; 12 | const _ = require('lodash'); 13 | const TimerMixin = require('react-timer-mixin'); 14 | 15 | module.exports = React.createClass({ 16 | mixins: [TimerMixin], 17 | getDefaultProps () { 18 | return { 19 | index: 0, 20 | vertical: false, 21 | loop: false, 22 | ratio: 0.872, 23 | }; 24 | }, 25 | getInitialState () { 26 | const { list, width, height, vertical } = this.props; 27 | const size = vertical ? height : width; 28 | this.blockSize = size * 0.708; 29 | this.moveDistance = size * 0.733; 30 | this.offset = this.moveDistance - ((size - this.moveDistance) / 2); 31 | 32 | this.count = list.length; 33 | this.list = [list[this.count - 2], list[this.count - 1], ...list, list[0], list[1]]; 34 | 35 | const scaleArr = []; 36 | const translateArr = []; 37 | const opacityArr = []; 38 | for (let i = 0; i < this.count + 4; i++) { 39 | scaleArr.push(new Animated.Value(1)); 40 | translateArr.push(new Animated.Value(0)); 41 | opacityArr.push(new Animated.Value(0)); 42 | } 43 | return { scaleArr, translateArr, opacityArr }; 44 | }, 45 | componentWillReceiveProps (nextProps) { 46 | const { list, width, height, vertical } = nextProps; 47 | const { list: _list, width: _width, height: _height, vertical: _vertical } = this.props; 48 | if (width !== _width || height !== _height || vertical !== _vertical) { 49 | const size = vertical ? height : width; 50 | this.blockSize = size * 0.708; 51 | this.moveDistance = size * 0.733; 52 | this.offset = this.moveDistance - ((size - this.moveDistance) / 2); 53 | } 54 | 55 | if (this.count !== _list.length) { 56 | this.count = list.length; 57 | 58 | const scaleArr = []; 59 | const translateArr = []; 60 | for (let i = 0; i < this.count + 4; i++) { 61 | scaleArr.push(new Animated.Value(1)); 62 | translateArr.push(new Animated.Value(0)); 63 | } 64 | this.setState({ scaleArr, translateArr }); 65 | } 66 | 67 | if (!_.isEqual(list, _list)) { 68 | this.list = [list[this.count - 2], list[this.count - 1], ...list, list[0], list[1]]; 69 | } 70 | }, 71 | componentDidMount () { 72 | const { vertical, index, loop } = this.props; 73 | this.setTimeout(() => { 74 | this.assistScroll.scrollTo({ [vertical ? 'y' : 'x']: (this.moveDistance * (loop ? index + 1 : index) || 1), animated: false }); 75 | this.setTimeout(() => { 76 | this.setState({ initialized: true }); 77 | }, 100); 78 | }, 100); 79 | }, 80 | getShowViews () { 81 | const { loop, width, height, vertical } = this.props; 82 | const { opacityArr, scaleArr, translateArr } = this.state; 83 | return this.list.map((o, i) => { 84 | if (!loop && (i < 1 || i >= this.list.length - 3)) { 85 | return ; 86 | } 87 | const margin = (this.moveDistance - this.blockSize) / 2; 88 | return ( 89 | 90 | 91 | 92 | {this.props.renderRow(this.list[i + (loop ? 0 : 1)], loop ? (i + 1) % this.count : i - 1)} 93 | 94 | 95 | 96 | ); 97 | }); 98 | }, 99 | getAssistViews () { 100 | const { list, loop, width, height, vertical } = this.props; 101 | const count = this.count + (loop ? 2 : 0); 102 | const margin = (this.moveDistance - this.blockSize) / 2; 103 | const views = []; 104 | for (let i = 0; i < count; i++) { 105 | views.push( 106 | 107 | 108 | this.props.onPress(list[loop ? (i + 2) % this.count : i], loop ? (i + 2) % this.count : i)}> 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | return views; 116 | }, 117 | scrollTo (index) { 118 | const { vertical } = this.props; 119 | this.scrollTargetIndex = index; 120 | this.assistScroll.scrollTo({ [vertical ? 'y' : 'x']: this.moveDistance * index, animated: true }); 121 | }, 122 | onScroll (e) { 123 | const { loop, vertical } = this.props; 124 | if (this.mainScroll && this.assistScroll) { 125 | const val = e.nativeEvent.contentOffset[vertical ? 'y' : 'x']; 126 | if (loop && Math.abs(val - ((this.count + 1) * this.moveDistance)) < 0.5) { 127 | this.mainScroll.scrollTo({ [vertical ? 'y' : 'x']: this.moveDistance + this.offset, animated: false }); 128 | this.assistScroll.scrollTo({ [vertical ? 'y' : 'x']: this.moveDistance, animated: false }); 129 | } else if (loop && Math.abs(val) < 0.1) { 130 | this.mainScroll.scrollTo({ [vertical ? 'y' : 'x']: this.moveDistance * this.count + this.offset, animated: false }); 131 | this.assistScroll.scrollTo({ [vertical ? 'y' : 'x']: this.moveDistance * this.count, animated: false }); 132 | } else { 133 | this.mainScroll.scrollTo({ [vertical ? 'y' : 'x']: val + this.offset, animated: false }); 134 | } 135 | const currentPageFloat = val / this.moveDistance; 136 | this.cardAnimated(currentPageFloat); 137 | } 138 | }, 139 | cardAnimated (currentPageFloat) { 140 | const { loop, list, width, height, vertical, ratio, onChange } = this.props; 141 | const { scaleArr, translateArr, opacityArr } = this.state; 142 | const index = loop ? (Math.round(currentPageFloat) + 2) % this.count : Math.round(currentPageFloat); 143 | if (this.lastChangeIndex !== index) { 144 | if (this.scrollTargetIndex == null) { 145 | onChange && onChange(list[index], index); 146 | } else if (this.scrollTargetIndex === index) { 147 | this.scrollTargetIndex = null; 148 | } 149 | this.lastChangeIndex = index; 150 | } 151 | 152 | for (let i = 0; i < this.count + 4; i++) { 153 | let r = 0; 154 | const currentPageInt = parseInt(currentPageFloat); 155 | if (i == 2) { 156 | r = Math.abs(currentPageFloat - (this.count + 1)) < 0.1 ? 1 : 0; 157 | } 158 | if (i == this.count + 1) { 159 | r = Math.abs(currentPageFloat) < 0.1 ? 1 : 0; 160 | } 161 | if (i - 1 == currentPageInt) { 162 | r = 1 - currentPageFloat % 1; 163 | } else if (i - 1 == currentPageInt + 1) { 164 | r = currentPageFloat % 1; 165 | } 166 | const scale = ratio + ((1 - ratio) * r); 167 | const translate = (vertical ? width : height) * (1 - scale) / 8; 168 | opacityArr[i].setValue(Math.pow(scale, 5)); 169 | scaleArr[i].setValue(scale); 170 | translateArr[i].setValue(translate); 171 | } 172 | }, 173 | render () { 174 | const { width, height, vertical } = this.props; 175 | const totalWidth = (vertical ? width : this.moveDistance) * this.list.length; 176 | const totalHeight = (vertical ? this.moveDistance : height) * this.list.length; 177 | return ( 178 | 179 | { this.mainScroll = ref; }} 183 | showsHorizontalScrollIndicator={false} 184 | > 185 | {this.state.initialized ? this.getShowViews() : } 186 | 187 | 188 | 189 | { this.assistScroll = ref; }} 194 | onScroll={e => this.onScroll(e)} 195 | scrollEventThrottle={16} 196 | showsHorizontalScrollIndicator={false} 197 | showsVerticalScrollIndicator={false} 198 | > 199 | {this.getAssistViews()} 200 | 201 | 202 | ); 203 | }, 204 | }); 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remobile/react-native-card-swiper", 3 | "version": "1.0.5", 4 | "description": "A react-native card swiper write in js", 5 | "main": "index.js", 6 | "author": { 7 | "name": "YunJiang.Fang", 8 | "email": "42550564@qq.com" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "react-native", 13 | "react-component", 14 | "ios", 15 | "android", 16 | "card", 17 | "list", 18 | "swiper", 19 | "remobile", 20 | "mobile" 21 | ], 22 | "homepage": "https://github.com/remobile/react-native-card-swiper", 23 | "bugs": { 24 | "url": "https://github.com/remobile/react-native-card-swiper/issues" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/remobile/react-native-card-swiper.git" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /screencasts/horizontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remobile/react-native-card-swiper/7ab37dca6ccacb02606b803705ec31a34b6c060c/screencasts/horizontal.gif -------------------------------------------------------------------------------- /screencasts/vertical.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remobile/react-native-card-swiper/7ab37dca6ccacb02606b803705ec31a34b6c060c/screencasts/vertical.gif --------------------------------------------------------------------------------