React-Native ListView拖拽交换Item

react

在高仿“掘金”客户端的那个项目中,你会发现在打开和关闭“首页展示标签”中,我并没有实现可拖拽换位item的效果。不过在自己新写的Gank.io项目中,将这一功能实现了一把,在此记录一下。

先上效果图http://blog.csdn.net/w337198302/article/details/53374506




对,就是这样~


在实现这个效果前,我的思路是这样的,布局->item可点击突出显示->可移动item->可交换item->抬起手指恢复正确的位置。下面一一解释。


布局

忘了说了,由于这个界面的item的元素较少,并且为了方便起见,我并没有采用ListView控件去实现这个list,而是使用数组map返回一个个itemView。

[javascript] view

plain copy

  1. render(){  

  2.         return(  

  3.             <View style={styles.container}>  

  4.                 <NavigationBar  

  5.                     title="首页内容展示顺序"  

  6.                     isBackBtnOnLeft={true}  

  7.                     leftBtnIcon="arrow-back"  

  8.                     leftBtnPress={this._handleBack.bind(this)}  

  9.                 />  

  10.                 {this.names.map((item, i)=>{  

  11.                     return (  

  12.                         <View  

  13.                             {...this._panResponder.panHandlers}  

  14.                             ref={(ref) => this.items[i] = ref}  

  15.                             key={i}  

  16.                             style={[styles.item, {top: (i+1)*49}]}>  

  17.                             <Icon name="ios-menu" size={px2dp(25)} color="#ccc"/>  

  18.                             <Text style={styles.itemTitle}>{item}</Text>  

  19.                         </View>  

  20.                     );  

  21.                 })}  

  22.             </View>  

  23.         );  

  24.     }  


前面NavigationBar部分不用看,自己封装的组件,通过map函数,可以依次遍历每个数组元素(this.names = ['Android','iOS','前端','拓展资源','休息视频'];)。因为我们需要后面能直接控制每个DOM(后面会直接操控它的样式),所以需要添加ref属性,不熟悉或者不明白ref这个prop的,可以参考这里。还需要注意的地方是,因为我们的item是可以拖拽移动的,能直接操控它们位置属性的就是绝对和相对布局,提供了top,left,right,bottom这些个props。贴一下item的stylesheet。

[javascript] view

plain copy

  1. item: {  

  2.         flexDirection: 'row',  

  3.         height: px2dp(49),  

  4.         width: theme.screenWidth,  

  5.         alignItems: 'center',  

  6.         backgroundColor: '#fff',  

  7.         paddingLeft: px2dp(20),  

  8.         borderBottomColor: theme.segment.color,  

  9.         borderBottomWidth: theme.segment.width,  

  10.         position: 'absolute',  

  11.     },  

不用在意其他的props,最关键的最起作用的就是position属性,一旦设置,该View的位置就不会受控于flexbox的布局了,直接浮动受控于top,left这几个参数。对于{...this._panResponder.panHandlers} 这个属性,就会谈到React-native中的手势,也就是我们下一个内容。


item可点击突出显示

如果不了解react-native中的手势,建议简单去了解下,直通车在这里还有这个。一旦需要自己实现手势,我们需要实现这几个方法。

[javascript] view

plain copy

  1. onStartShouldSetPanResponder: (evt, gestureState) => true, //开启手势响应  

  2. onMoveShouldSetPanResponder: (evt, gestureState) => true,  //开启移动手势响应  

  3.   

  4. onPanResponderGrant: (evt, gestureState) => {              //手指触碰屏幕那一刻触发  

  5.     

  6. },  

  7. onPanResponderMove: (evt, gestureState) => {               //手指在屏幕上移动触发  

  8.     

  9. },  

  10. onPanResponderTerminationRequest: (evt, gestureState) => true,   //当有其他不同手势出现,响应是否中止当前的手势  

  11. onPanResponderRelease: (evt, gestureState) => {           //手指离开屏幕触发  

  12.     

  13. },  

  14. onPanResponderTerminate: (evt, gestureState) => {         //当前手势中止触发  

  15.     

  16. },  


简单介绍了下几个函数的意义,所以很明显,要实现item点击突出显示,我们需要在onPanRespondedGrant这里做事情。贴代码来解释,

[javascript] view

plain copy

  1. onPanResponderGrant: (evt, gestureState) => {  

  2.                 const {pageY, locationY} = evt.nativeEvent;   //1  

  3.                 this.index = this._getIdByPosition(pageY);    //2  

  4.                 this.preY = pageY - locationY;                //3  

  5.                 //get the taped item and highlight it  

  6.                 let item = this.items[this.index];            //4  

  7.                 item.setNativeProps({                         //5  

  8.                     style: {  

  9.                         shadowColor: "#000",                  //6  

  10.                         shadowOpacity: 0.3,                   //6  

  11.                         shadowRadius: 5,                      //6  

  12.                         shadowOffset: {height: 0, width: 2},  //6  

  13.                         elevation: 5                          //7  

  14.                     }  

  15.                 });  

  16.             },  


1. evt参数有个nativeEvent对象,其中包含了一系列的参数,包括点击的位置,有几个手指点击屏幕等等。pageY是相对于根节点的位置,locationY是相对于元素自己。

2. 通过这个pageY我们需要计算出这个点上是对应的哪一个item,由于我的布局简单,写个函数来计算了下,

[javascript] view

plain copy

  1. _getIdByPosition(pageY){  

  2.         var id = -1;  

  3.         const height = px2dp(49);  

  4.   

  5.         if(pageY >= height && pageY < height*2)  

  6.             id = 0;  

  7.         else if(pageY >= height*2 && pageY < height*3)  

  8.             id = 1;  

  9.         else if(pageY >= height*3 && pageY < height*4)  

  10.             id = 2;  

  11.         else if(pageY >= height*4 && pageY < height*5)  

  12.             id = 3;  

  13.         else if(pageY >= height*5 && pageY < height*6)  

  14.             id = 4;  

  15.   

  16.         return id;  

  17.     }  


3. this.preY保存当前正确点击item的位置,为了后面移动item。

4. 有了this.index,我们就可以获取到点击的是哪一个DOM了。

5. 所以这一步就是直接修改DOM的属性,将其突出显示

6. iOS中阴影属性

7. Android中阴影设置


可移动item

这一步应该也可以想到我们需要在onPanResponderMove里操作。让其移动就是不断的将evt.nativeEvent中位置信息去赋值给item的top属性,这个比较简单,

[javascript] view

plain copy

  1. onPanResponderMove: (evt, gestureState) => {  

  2.                 let top = this.preY + gestureState.dy;  

  3.                 let item = this.items[this.index];  

  4.                 item.setNativeProps({  

  5.                     style: {top: top}  

  6.                 });  

  7.             },  


可交换item

这个是最核心的部分了,思路是这样的,当我们点击某个item并且开始移动它的时候,我们还需要计算下,当前这个手指移动到的位置有没有进入别的Item范围,如果有,OK,我们将进入到的那个item位置放到我们手上拿着的这个item的位置。因为有了之前的函数——通过位置计算id,我们可以很快的求出是否这个位置返回的id和我们手上这个item的id一样。

[javascript] view

plain copy

  1. onPanResponderMove: (evt, gestureState) => {  

  2.                let top = this.preY + gestureState.dy;  

  3.                let item = this.items[this.index];  

  4.                item.setNativeProps({  

  5.                    style: {top: top}  

  6.                });  

  7.   

  8.                let collideIndex = this._getIdByPosition(evt.nativeEvent.pageY);  //获取当前的位置上item的id  

  9.                if(collideIndex !== this.index && collideIndex !== -1) {          //判断是否和手上的item的id一样  

  10.                    let collideItem = this.items[collideIndex];  

  11.                    collideItem.setNativeProps({  

  12.                        style: {top: this._getTopValueYById(this.index)}         //将collideItem的位置移动到手上的item的位置  

  13.                    });  

  14.                    //swap two values  

  15.                    [this.items[this.index], this.items[collideIndex]] = [this.items[collideIndex], this.items[this.index]];  

  16.                    this.index = collideIndex;  

  17.                }  

  18.            },  


在swap two value这里,我们还需要做一件很重要的事,当位置此时发生交换时,对应的item的id值我们需要进行一下交换,不然下一次再碰撞检测时,collideItem移动到的位置始终都是我们手上拿的item的初始位置。PS:这里我用的ES6的语法交换两个数的数值。


抬起手指恢复正确的位置

抬起手指时,我们需要做两件事: 1.将手上拿起的item的属性恢复原样,2. 将其摆到正确的位置上。

第一个设置属性很简单,当初怎么改的,就怎么改回去,用setNativeProps。第二个也简单,因为我们在移动和交换过程中,始终保持id对应正确的item,所以我们只要有了id就可以计算出正确的位置。

[javascript] view

plain copy

  1. onPanResponderRelease: (evt, gestureState) => {  

  2.                 const shadowStyle = {  

  3.                     shadowColor: "#000",  

  4.                     shadowOpacity: 0,  

  5.                     shadowRadius: 0,  

  6.                     shadowOffset: {height: 0, width: 0,},  

  7.                     elevation: 0  

  8.                 };  

  9.                 let item = this.items[this.index];  

  10.                 //go back the correct position  

  11.                 item.setNativeProps({  

  12.                     style: {...shadowStyle, top: this._getTopValueYById(this.index)}  

  13.                 });  

  14.             },  


忘了在之前贴一下根据id计算位置的函数了,

[javascript] view

plain copy

  1. _getTopValueYById(id){  

  2.         const height = px2dp(49);  

  3.         return (id + 1) * height;  

  4.     }  


因为我的NavigationBar也是行高49,所以id为0的第一item位置就应该1*49。这样就容易理解这个代码了吧。


Anything Else?Finish it?

咱们的数据结构呢?这个只是界面作出了改动了,我们的数据还需要做出相应的变化,这里简单起见,我在构造函数中,添加了this.order=[ ],当开始map时,我们就将各个item的名字push进去,所以这个数组的顺序就代表着这个list的顺序。

[javascript] view

plain copy

  1. {this.names.map((item, i)=>{  

  2.                     this.order.push(item);  //add code at here  

  3.                     return (  

  4.                         <View  

  5.                             {...this._panResponder.panHandlers}  

  6.                             ref={(ref) => this.items[i] = ref}  

  7.                             key={i}  

  8.                             style={[styles.item, {top: (i+1)*49}]}>  

  9.                             <Icon name="ios-menu" size={px2dp(25)} color="#ccc"/>  

  10.                             <Text style={styles.itemTitle}>{item}</Text>  

  11.                         </View>  

  12.                     );  

  13.                 })}  


当开始交换位置时,这个order也需要交换。

[javascript] view

plain copy

  1. //swap two values  

  2. [this.items[this.index], this.items[collideIndex]] = [this.items[collideIndex], this.items[this.index]];  

  3. [this.order[this.index], this.order[collideIndex]] = [this.order[collideIndex], this.order[this.index]];  //add code at here  

  4. this.index = collideIndex;  


OK,至此,大功告成,完成。完整代码最后贴出来。


关于新项目

目前正在做这个新项目,因为上一个“掘金”项目,毕竟api不公开,偷偷获取数据流别人不怪罪已经很感谢了,而且有的数据获取不到,所以做不了一个完整的react-native项目,最近在用gank.io的公开api在做一个全新的项目,从界面设计到代码架构(Redux架构)都是一次全新的体验,毕竟上一个项目是第一个,还是摸索,这一次将会更加熟练,会重新规范代码结构和命名。


所以欢迎大家可以关注我的新项目,PS:这个项目仍然处在开发阶段,当完成时,会再一次博客记录这次开发旅程~


完整代码

有些代码是自己封装的,不用理会

/**

* Created by wangdi on 27/11/16.

*/

'use strict';

import React, {Component, PropTypes} from 'react';

import {StyleSheet, View, Text, PanResponder} from 'react-native';

import Icon from 'react-native-vector-icons/Ionicons';

import BackPageComponent from '../BackPageComponent';

import NavigationBar from '../../components/NavigationBar';

import px2dp from '../../utils/px2dp';

import theme from '../../constants/theme';

export default class OrderContentPage extends BackPageComponent{

constructor(props){

super(props);

this.names = ['Android','iOS','前端','拓展资源','休息视频'];

this.items = [];

this.order = [];

}

render(){

return(

<View style={styles.container}>

<NavigationBar

title="首页内容展示顺序"

isBackBtnOnLeft={true}

leftBtnIcon="arrow-back"

leftBtnPress={this._handleBack.bind(this)}

/>

{this.names.map((item, i)=>{

this.order.push(item);

return (

<View

{...this._panResponder.panHandlers}

ref={(ref) => this.items[i] = ref}

key={i}

style={[styles.item, {top: (i+1)*49}]}>

<Icon name="ios-menu" size={px2dp(25)} color="#ccc"/>

<Text style={styles.itemTitle}>{item}</Text>

</View>

);

})}

</View>

);

}

componentWillMount(){

this._panResponder = PanResponder.create({

onStartShouldSetPanResponder: (evt, gestureState) => true,

onMoveShouldSetPanResponder: (evt, gestureState) => true,

onPanResponderGrant: (evt, gestureState) => {

const {pageY, locationY} = evt.nativeEvent;

this.index = this._getIdByPosition(pageY);

this.preY = pageY - locationY;

//get the taped item and highlight it

let item = this.items[this.index];

item.setNativeProps({

style: {

shadowColor: "#000",

shadowOpacity: 0.3,

shadowRadius: 5,

shadowOffset: {height: 0, width: 2},

elevation: 5

}

});

},

onPanResponderMove: (evt, gestureState) => {

let top = this.preY + gestureState.dy;

let item = this.items[this.index];

item.setNativeProps({

style: {top: top}

});

let collideIndex = this._getIdByPosition(evt.nativeEvent.pageY);

if(collideIndex !== this.index && collideIndex !== -1) {

let collideItem = this.items[collideIndex];

collideItem.setNativeProps({

style: {top: this._getTopValueYById(this.index)}

});

//swap two values

[this.items[this.index], this.items[collideIndex]] = [this.items[collideIndex], this.items[this.index]];

[this.order[this.index], this.order[collideIndex]] = [this.order[collideIndex], this.order[this.index]];

this.index = collideIndex;

}

},

onPanResponderTerminationRequest: (evt, gestureState) => true,

onPanResponderRelease: (evt, gestureState) => {

const shadowStyle = {

shadowColor: "#000",

shadowOpacity: 0,

shadowRadius: 0,

shadowOffset: {height: 0, width: 0,},

elevation: 0

};

let item = this.items[this.index];

//go back the correct position

item.setNativeProps({

style: {...shadowStyle, top: this._getTopValueYById(this.index)}

});

console.log(this.order);

},

onPanResponderTerminate: (evt, gestureState) => {

// Another component has become the responder, so this gesture

// should be cancelled

}

});

}

_getIdByPosition(pageY){

var id = -1;

const height = px2dp(49);

if(pageY >= height && pageY < height*2)

id = 0;

else if(pageY >= height*2 && pageY < height*3)

id = 1;

else if(pageY >= height*3 && pageY < height*4)

id = 2;

else if(pageY >= height*4 && pageY < height*5)

id = 3;

else if(pageY >= height*5 && pageY < height*6)

id = 4;

return id;

}

_getTopValueYById(id){

const height = px2dp(49);

return (id + 1) * height;

}

}

const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: theme.pageBackgroundColor

},

item: {

flexDirection: 'row',

height: px2dp(49),

width: theme.screenWidth,

alignItems: 'center',

backgroundColor: '#fff',

paddingLeft: px2dp(20),

borderBottomColor: theme.segment.color,

borderBottomWidth: theme.segment.width,

position: 'absolute',

},

itemTitle: {

fontSize: px2dp(15),

color: '#000',

marginLeft: px2dp(20)

}

});


以上是 React-Native ListView拖拽交换Item 的全部内容, 来源链接: utcz.com/z/382529.html

回到顶部