├── package.json ├── .gitignore ├── LICENSE ├── README.md ├── example └── example.js └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-swipe-left", 3 | "version": "0.1.2", 4 | "description": "a RN swipe-left component for listView.(左滑解决方案)", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/yzsolo/react-native-swipe-left.git" 15 | }, 16 | "author": "yzsolo125@gmail.com", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/yzsolo/react-native-swipe-left/issues" 20 | }, 21 | "homepage": "https://github.com/yzsolo/react-native-swipe-left#readme" 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aresy.z 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-swipe-left 2 | a RN swipe-left component for listView.(左滑解决方案) 3 | 4 | ### IOS && ANDROID 5 | IOS | Android 6 | -----|------- 7 | ![swipeleft ios preview](http://imgur.com/KswnF0X.gif) | ![swipeleft android preview](http://imgur.com/6FyHjft.gif) 8 | 9 | IOS | Android 10 | -----|------- 11 | ![swipeleft ios preview](http://imgur.com/e7FKKUs.gif) | ![swipeleft android preview](http://i.imgur.com/oXOMKME.gif) 12 | 13 | ### Features (特性) 14 | RESOLVE | 解决 15 | ----------------- | ----- 16 | the Opposite effect between two rows |(row之间的互斥收回) 17 | button configurable(one or more, text/image, bgcolor, width,callback etc)|左边按钮的可配置化(可配置多按钮,文字/图片,背景色,宽度,回调) 18 | pressable in single row |单个row内的按钮或链接可点击 19 | optional animation type, timing/spring |可选择滚动动画类型,timing/spring 20 | 21 | 22 | ### Installation 23 | ``` 24 | npm install --save react-native-swipe-left 25 | ``` 26 | 27 | ### Usage example 28 | see the example/example.js for a more detailed example. 29 | ```javascript 30 | // 1, settings in your constructor 31 | constructor(props) { 32 | this._dataRow = {}; 33 | this.openRowId = ''; 34 | this.state = { 35 | scrollEnable: true, 36 | hasIdOpen: false 37 | }; 38 | } 39 | 40 | // 2, set scrollEnabled 41 | 42 | 43 | // 3, set your button`s setting 44 | let rightBtn = [{ 45 | id: 1, 46 | text: 'button', 47 | width: 80, 48 | bgColor: 'red', 49 | underlayColor: '#ffffff', 50 | onPress: ()=>{alert('delete1!');}, 51 | }, { 52 | id: 2, 53 | image: 'your uri', 54 | width: 80, 55 | bgColor: null, 56 | onPress: ()=>{alert('delete2!')} 57 | }, { 58 | id: 3, 59 | text: 'button', 60 | width: 80, 61 | bgColor: 'yellow', 62 | onPress: ()=>{alert('delete3!');}, 63 | }] 64 | 65 | 66 | // 4, in your renderRow function(a is sectionId, b is rowId) 67 | let id = '' + a + b; 68 | this._dataRow[id] = row} 71 | id={id} 72 | data={data} 73 | rightBtn={rightBtn}> 74 | {children node} 75 | 76 | ``` 77 | 78 | 79 | ### Props 80 | 81 | ##### component: 82 | Prop | Type | Optional | Default | Description 83 | --------------- | ------ | --------- | ---------- | ----------- 84 | root | current component | require | | current component 85 | ref | function | require | | it is row`s identity card 86 | id | string | require | | identity card 87 | rightBtn | array | require | | your buttons, one or more 88 | 89 | ##### row: 90 | Prop | Type | Optional | Default | Description 91 | --------------- | ------ | --------- | ---------- | ----------- 92 | boxbgColor | string | Yes | '#eeeeee' | when you swipe the row a lot ,you`ll see this color 93 | rowbgColor | string | Yes | '#ffffff' | row`s bgColor 94 | animationType | string | Yes | 'timing' | animation type 95 | duration | number | Yes | 150 | The animation process time 96 | 97 | ##### button: 98 | Prop | Type | Optional | Default | Description 99 | --------------- | ------ | --------- | ---------- | ----------- 100 | id | number | require | | deal with the 'key' problem 101 | text/image | string | Yes | | use text or a image 102 | width | number | Yes | | the width of button 103 | bgColor | string | Yes | | backgroundColor of button 104 | onPress | function| Yes | | the callback when you press a button 105 | underlayColor | string | Yes | | the underlayColor of TouchableHighlight 106 | 107 | ### Note: 108 | 从组件本身来讲,已经完成了ios/android端能够流畅左滑的工作([优化过程](https://github.com/yzsolo/yzsolo.github.io/issues/22 "优化过程")),但还可以继续优化体验,除了上面特性的可配置化,后期会增加更多灵活的配置,如:是否选择互斥,或是类似qq那样单次滑动只做一件事,滑出或滑进。 109 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | AppRegistry, 4 | ListView, 5 | TouchableHighlight, 6 | ScrollView, 7 | StyleSheet, 8 | PanResponder, 9 | Image, 10 | Text, 11 | View, 12 | RefreshControl, 13 | } from 'react-native'; 14 | 15 | import Platform from 'Platform'; 16 | 17 | //swipt 18 | import SwipeitemView from 'react-native-swipe-left'; 19 | 20 | //ios第三方下拉组件 21 | import PullDownRefreshView from './components/PullDown'; 22 | 23 | class AresRn extends Component { 24 | 25 | constructor(props) { 26 | super(props); 27 | var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 28 | this._dataRow = {}; 29 | this.openRowId = ''; 30 | this.state = { 31 | dataSource: ds.cloneWithRows(['row 1', 'row 2', 'row 3', 'row 4', 'row 5', 'row 6', 'row 7', 'row 8', 'row 9', 'row 10', 'row 11', 'row 12', 'row 13', 'row 14', 'row 15']), 32 | isShowToTop: false, 33 | isReFresh: false, 34 | scrollEnable: true, 35 | hasIdOpen: false 36 | }; 37 | } 38 | 39 | _listView() { 40 | 41 | /* 42 | * android,ios都使用原生下拉刷新组件: 43 | */ 44 | return ( 45 | { 53 | return 54 | }} 55 | refreshControl={ 56 | 61 | }/> 62 | ); 63 | } 64 | 65 | PullDownRefresh() { 66 | let self = this; 67 | this.setState({ 68 | isReFresh: true 69 | }); 70 | 71 | setTimeout(function() { 72 | self.setState({ 73 | isReFresh: false 74 | }) 75 | }, 2000); 76 | } 77 | 78 | render() { 79 | let listView = this._listView(); 80 | return ( 81 | 82 | 83 | 消息列表 84 | 85 | {listView} 86 | {this.state.isShowToTop?:null} 87 | 88 | ); 89 | } 90 | 91 | rowRender(data, a, b) { 92 | let rightBtn = this._rightButtons(); 93 | let id = '' + a + b; 94 | return ( 95 | this._dataRow[id] = row} 98 | id={id} 99 | data={data} 100 | rightBtn={rightBtn}> 101 | 102 | 103 | { 106 | alert('hi!'); 107 | }}> 108 | hello world 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | 116 | _rightButtons() { 117 | return [{ 118 | id: 1, 119 | text: 'button', 120 | width: 80, 121 | bgColor: 'red', 122 | underlayColor: '#ffffff', 123 | onPress: ()=>{alert('delete1!');}, 124 | }, { 125 | id: 2, 126 | image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAABYCAYAAABxlTA0AAAEBUlEQVR4Ae2dz24bZRRHPR53EG9BlFXAaovwioh3cJCQ4iSKAgosEF3QLurswEkUsN8BVGiXMazLBtGQhdtESlet5NKi/BGSQyLZiUJVmMvvohnJshzjsefz3BnfI53N5HPGOqqcNPP9SUmBiGyYhbNwFd6FW/AJbMAmZF7CP+EzuAe3vLFr3muz0IYpCUZ5cwu+DVfgT17AsGh633PFu4c1ToEn4RewTqOj7t1zMqmBM3AebkGXosOFv8IFmElCYAd+DOskj7r33pw4Bk7Dj+A+yWffe6/puATOwR2KHzswJznwFfg1fEXx5RUswyvSAk/AGiWHh3BCSuA8PKXkcQrzUQa2YAm6lGxK0Bp1YBveofHhDrRHFdiBVRo/qtAxHdiGmzS+bELbVGALfk8KN7BMBF4nxWcj7MAfQJcUH5ebhBV4CraoE6XFbYYN/BrcI+Uy9rjRMIHLUOlNZdDA78C/odIbbpQLGjgNH5LSL49gOkjgZVKCstxv4NfhISlBOeR2/QT+nJRBufl/gR14RMqgcDunV+BFUoZlsVfgGgnhn6Pf6a+vbtH53DSd5a93lb/GY3isIGqXBb4qJu7Bczqff48j9iWP5dcI4mq3wBUSAv+r5HAXX35K7kmDLoO/xmN4LL9GEJXOwBZ8TkLwPhba4/aMzGPPZ98lQbyAVnvgayQIBGODj5fF9fbARRoxFysfemHMy/eKgGJ74PuJDlxcogi47we2YTMuHwcx+thoQpsDvwlJAxshy4HnNLAx5jjwqgY2xjoHvicxMP8QhAGuiQx8jwM/kBjYux7gmsjADzjwUw1sjKccuKGBjdHgwC0NbIxmikyigSkFX5JRNPCxBjZGK2X+78D6Q+4xGUV/TftZAxvjFw78jQY2xrcc+LYGNsZtDpzXwMaY4cBTGtgYU/4joxMNHDon/iMj9geRfw8uLgW4Ji7wj+1PlW/oE43QudEe+C0NHDrZzqlTL0RPkwqAe3rsT6cSM3WKrUQ60W/1Mw4zfNzjP/h7RT0hsGJo+qr5qaoxmdJ6TdQEbA7x32TrwvTwYQvepOzo4tZ0CYFZFnURTISLYNibNCjKLcMLEXUhooCltLqUlk3DR9Qvyk7QxeBszth2BrqdgW7IEYDyMDueOD3/86HUoDNMYPaNrpt/KtxkIqxtvWagSz6KC98Pe2O6DVKMbUzHWvAuKdzAMrk5aJXGlyq0dXvbGG9v65sZsw2av4MZ3WI8vluM6yb5BgLrMQ8SDyop60ElozlqZ5fixy7M6WFR4XNg+rAokzrwE/iM5PGb996cpBzYtwC3Bfxaty3gwD6jTsJSBEdOlhJ65KQemipF/9jfAlzrcuzvGfQ586496Tj2tyDt2N9/Ac1/sDfK46TXAAAAAElFTkSuQmCC', 127 | width: 80, 128 | bgColor: null, 129 | onPress: ()=>{alert('delete2!')} 130 | }, { 131 | id: 3, 132 | text: 'button', 133 | width: 80, 134 | bgColor: 'yellow', 135 | onPress: ()=>{alert('delete3!');}, 136 | }] 137 | } 138 | 139 | } 140 | 141 | const styles = StyleSheet.create({ 142 | listview:{ 143 | flex: 1, 144 | backgroundColor: '#eeeeee' 145 | }, 146 | header: { 147 | height : 50, 148 | paddingTop:15, 149 | justifyContent:'center', 150 | alignItems:'center', 151 | backgroundColor : '#099fde' 152 | }, 153 | headerText: { 154 | color: '#ffffff' 155 | }, 156 | deletebtn: { 157 | flex: 1, 158 | width: 80, 159 | height: 74, 160 | backgroundColor: 'red', 161 | alignItems: 'center', 162 | justifyContent: 'center' 163 | }, 164 | btntext: { 165 | color: '#ffffff' 166 | }, 167 | deleteBut: { 168 | flex: 1, 169 | justifyContent: 'center', 170 | alignItems: 'center', 171 | }, 172 | }); 173 | 174 | AppRegistry.registerComponent('AresRn', () => AresRn); 175 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a swipe left component. (消息列表左滑解决方案) 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import React, { Component } from 'react'; 8 | import { 9 | View, 10 | Text, 11 | Image, 12 | StyleSheet, 13 | PanResponder, 14 | Animated, 15 | TouchableHighlight, 16 | } from 'react-native'; 17 | 18 | export default class Swipes extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | let _width = _width || this._getBtnBoxWidth(); 23 | this.state = { 24 | isOpen: false, 25 | height: 0, 26 | RowTranslateX: new Animated.Value(0), 27 | BtnTranslateX: new Animated.Value(0) 28 | }; 29 | 30 | } 31 | 32 | _closeRow = () => { 33 | let _width = _width || this._getBtnBoxWidth(); 34 | this._setIsOpenState(false); 35 | this._setHasIdOpenState(false); 36 | 37 | this.moving(this.state.RowTranslateX, 0); 38 | this.moving(this.state.BtnTranslateX, 0); 39 | } 40 | 41 | _getBtnBoxWidth() { 42 | let arr = []; 43 | 44 | this.props.rightBtn.map(function(item){ 45 | return arr.push(item.width); 46 | }) 47 | 48 | return arr.reduce(function(pre, cur) { 49 | return pre + cur; 50 | }); 51 | 52 | } 53 | 54 | componentWillMount() { 55 | 56 | this._panResponder = PanResponder.create({ 57 | onStartShouldSetPanResponder: (evt, gestureState) => { 58 | return (this.state.isOpen || this.props.root.state.hasIdOpen)? true : false; 59 | }, 60 | onStartShouldSetPanResponderCapture: (evt, gestureState) => { 61 | return (this.state.isOpen || this.props.root.state.hasIdOpen)? true : false; 62 | }, 63 | onMoveShouldSetPanResponder: (evt, gestureState) => {return Math.abs(gestureState.dx) > 0;}, 64 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => {return Math.abs(gestureState.dx) > 0;}, 65 | onPanResponderMove: (evt, gestureState) => {this._onPanResponderMove(evt, gestureState)}, 66 | onPanResponderRelease: (evt, gestureState) => {this._onPanResponderRelease(evt, gestureState)}, 67 | onPanResponderTerminate: (evt, gestureState) => {this._onPanResponderTerminate(evt, gestureState)}, 68 | }) 69 | 70 | } 71 | 72 | _onPanResponderMove(evt, gestureState) { 73 | let dx; 74 | let _width = _width || this._getBtnBoxWidth(); 75 | let right = -_width; 76 | 77 | if(Math.abs(gestureState.dx)>5) { 78 | this.disallowScroll(); 79 | } 80 | 81 | this.isRowMove(); 82 | 83 | if(!this.state.isOpen) { 84 | 85 | dx = gestureState.dx; 86 | 87 | if(dx < -10) { 88 | dx += 10; 89 | 90 | if(dx >= right) { 91 | this.setState({ 92 | BtnTranslateX: new Animated.Value(dx) 93 | }); 94 | } else { 95 | this.setState({ 96 | BtnTranslateX: new Animated.Value(right) 97 | }); 98 | } 99 | 100 | if(dx < 50) { 101 | this.setState({ 102 | RowTranslateX: new Animated.Value(dx) 103 | }); 104 | } 105 | 106 | } 107 | 108 | } else { 109 | dx = right + gestureState.dx; 110 | 111 | if(dx>right) { 112 | this.setState({ 113 | BtnTranslateX: new Animated.Value(dx) 114 | }) 115 | } 116 | 117 | if(dx < 50) { 118 | this.setState({ 119 | RowTranslateX: new Animated.Value(dx) 120 | }) 121 | } 122 | 123 | } 124 | 125 | } 126 | 127 | _onPanResponderRelease(evt, gestureState) { 128 | let toValue; 129 | let isOpen; 130 | let dx; 131 | let _width = _width || this._getBtnBoxWidth(); 132 | let right = -_width; 133 | let range; 134 | 135 | if(this.state.isOpen || this.props.root.state.hasIdOpen) { 136 | dx = right + gestureState.dx; 137 | range = right + 40 138 | } else { 139 | dx = gestureState.dx; 140 | range = -40; 141 | } 142 | 143 | if(dx event => { 195 | item.onPress(event, rowId) 196 | } 197 | 198 | moving(k, v) { 199 | let type = this.props.animationType; 200 | let duration = this.props.duration 201 | if(type === 'timing'){ 202 | Animated.timing(k, { 203 | toValue: v, 204 | duration: duration 205 | }).start(); 206 | } else if(type === 'spring') { 207 | Animated.spring(k, { 208 | toValue: v, 209 | duration: duration 210 | }).start(); 211 | } 212 | 213 | } 214 | 215 | isRowOpen() { 216 | let root = this.props.root; 217 | let id = this.props.id; 218 | if(!root.openRowId) 219 | root.openRowId = id; 220 | } 221 | 222 | isRowMove() { 223 | let root = this.props.root; 224 | let id = this.props.id; 225 | if(root.openRowId && root.openRowId !== id && root._dataRow[root.openRowId]) { 226 | root._dataRow[root.openRowId]._closeRow(); 227 | root.openRowId = ''; 228 | } 229 | } 230 | 231 | isTerminate() { 232 | let root = this.props.root; 233 | let id = this.props.id; 234 | if(root.openRowId && root.openRowId !== id && root._dataRow[root.openRowId]) { 235 | root._dataRow[root.openRowId]._closeRow(); 236 | } 237 | root.openRowId = id; 238 | } 239 | 240 | closeRow() { 241 | let root = this.props.root; 242 | if(root.openRowId && root._dataRow[root.openRowId]) { 243 | root._dataRow[root.openRowId]._closeRow(); 244 | } 245 | } 246 | 247 | allowScroll() { 248 | let root = this.props.root; 249 | 250 | /* 接入原生下拉 */ 251 | root.setState({ 252 | scrollEnable: true 253 | }) 254 | 255 | /* 接入第三方下拉 */ 256 | /* 指定到像相应的scrollView对象 */ 257 | // root.refs.listview.refs.listviewscroll.setState({ 258 | // scrollEnabled: true 259 | // }); 260 | } 261 | 262 | disallowScroll() { 263 | let root = this.props.root; 264 | 265 | /* 接入原生下拉 */ 266 | root.setState({ 267 | scrollEnable: false 268 | }) 269 | 270 | /* 接入第三方下拉 */ 271 | /* 指定到像相应的scrollView对象 */ 272 | // root.refs.listview.refs.listviewscroll.setState({ 273 | // scrollEnabled: false 274 | // }); 275 | } 276 | 277 | render() { 278 | let _width = _width || this._getBtnBoxWidth(); 279 | const rowId = this.props.id 280 | return ( 281 | {this.setState({height:e.nativeEvent.layout.height})}}> 282 | 283 | 284 | 291 | {this.props.children} 292 | 293 | 294 | 301 | {this.props.rightBtn.map((item)=>{ 302 | 303 | return 304 | {item.text? 305 | {item.text} 306 | : 307 | item.image? 308 | 309 | : 310 | null} 311 | 312 | })} 313 | 314 | 315 | 316 | ); 317 | } 318 | 319 | } 320 | 321 | Swipes.defaultProps = { 322 | boxbgColor: '#eeeeee', 323 | rowbgColor: '#ffffff', 324 | animationType: 'timing', 325 | duration: 150, 326 | } 327 | 328 | let styles = StyleSheet.create({ 329 | containerBox: { 330 | flex: 1, 331 | flexDirection: 'row', 332 | justifyContent:'center', 333 | overflow: 'hidden', 334 | position: 'relative', 335 | }, 336 | container: { 337 | flex: 1, 338 | // borderBottomWidth: 1, 339 | // borderBottomColor: '#eeeeee', 340 | overflow: 'hidden', 341 | }, 342 | deletebtnbox: { 343 | flex: 1, 344 | position: 'absolute', 345 | top: 0, 346 | bottom: 0, 347 | right: 0, 348 | flexDirection: 'row', 349 | alignItems: 'center', 350 | justifyContent:'center', 351 | }, 352 | deletebtn: { 353 | alignItems: 'center', 354 | justifyContent: 'center' 355 | } 356 | }) 357 | --------------------------------------------------------------------------------