React-Native ListView拖拽交换Item

Posted wangdi1224

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React-Native ListView拖拽交换Item相关的知识,希望对你有一定的参考价值。

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



对,就是这样~


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


布局

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

render()
        return(
            <View style=styles.container>
                <NavigationBar
                    title="首页内容展示顺序"
                    isBackBtnOnLeft=true
                    leftBtnIcon="arrow-back"
                    leftBtnPress=this._handleBack.bind(this)
                />
                this.names.map((item, i)=>
                    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>
        );
    

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

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',
    ,

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


item可点击突出显示

如果不了解react-native中的手势,建议简单去了解下, 直通车在这里还有 这个。一旦需要自己实现手势,我们需要实现这几个方法。
      onStartShouldSetPanResponder: (evt, gestureState) => true, //开启手势响应
      onMoveShouldSetPanResponder: (evt, gestureState) => true,  //开启移动手势响应

      onPanResponderGrant: (evt, gestureState) =>               //手指触碰屏幕那一刻触发
        
      ,
      onPanResponderMove: (evt, gestureState) =>                //手指在屏幕上移动触发
        
      ,
      onPanResponderTerminationRequest: (evt, gestureState) => true,   //当有其他不同手势出现,响应是否中止当前的手势
      onPanResponderRelease: (evt, gestureState) =>            //手指离开屏幕触发
        
      ,
      onPanResponderTerminate: (evt, gestureState) =>          //当前手势中止触发
        
      ,

简单介绍了下几个函数的意义,所以很明显,要实现item点击突出显示,我们需要在onPanRespondedGrant这里做事情。贴代码来解释,
onPanResponderGrant: (evt, gestureState) => 
                const pageY, locationY = evt.nativeEvent;   //1
                this.index = this._getIdByPosition(pageY);    //2
                this.preY = pageY - locationY;                //3
                //get the taped item and highlight it
                let item = this.items[this.index];            //4
                item.setNativeProps(                         //5
                    style: 
                        shadowColor: "#000",                  //6
                        shadowOpacity: 0.3,                   //6
                        shadowRadius: 5,                      //6
                        shadowOffset: height: 0, width: 2,  //6
                        elevation: 5                          //7
                    
                );
            ,

1. evt参数有个nativeEvent对象,其中包含了一系列的参数,包括点击的位置,有几个手指点击屏幕等等。pageY是相对于根节点的位置,locationY是相对于元素自己。 2. 通过这个pageY我们需要计算出这个点上是对应的哪一个item,由于我的布局简单,写个函数来计算了下,
_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;
    

3. this.preY保存当前正确点击item的位置,为了后面移动item。 4. 有了this.index,我们就可以获取到点击的是哪一个DOM了。 5. 所以这一步就是直接修改DOM的属性,将其突出显示 6. iOS中阴影属性 7. Android中阴影设置

可移动item

这一步应该也可以想到我们需要在onPanResponderMove里操作。让其移动就是不断的将evt.nativeEvent中位置信息去赋值给item的top属性,这个比较简单,
onPanResponderMove: (evt, gestureState) => 
                let top = this.preY + gestureState.dy;
                let item = this.items[this.index];
                item.setNativeProps(
                    style: top: top
                );
            ,

可交换item

这个是最核心的部分了,思路是这样的,当我们点击某个item并且开始移动它的时候,我们还需要计算下,当前这个手指移动到的位置有没有进入别的Item范围,如果有,OK,我们将进入到的那个item位置放到我们手上拿着的这个item的位置。因为有了之前的函数——通过位置计算id,我们可以很快的求出是否这个位置返回的id和我们手上这个item的id一样。
 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);  //获取当前的位置上item的id
                if(collideIndex !== this.index && collideIndex !== -1)           //判断是否和手上的item的id一样
                    let collideItem = this.items[collideIndex];
                    collideItem.setNativeProps(
                        style: top: this._getTopValueYById(this.index)         //将collideItem的位置移动到手上的item的位置
                    );
                    //swap two values
                    [this.items[this.index], this.items[collideIndex]] = [this.items[collideIndex], this.items[this.index]];
                    this.index = collideIndex;
                
            ,

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

抬起手指恢复正确的位置

抬起手指时,我们需要做两件事: 1.将手上拿起的item的属性恢复原样,2. 将其摆到正确的位置上。 第一个设置属性很简单,当初怎么改的,就怎么改回去,用setNativeProps。第二个也简单,因为我们在移动和交换过程中,始终保持id对应正确的item,所以我们只要有了id就可以计算出正确的位置。
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)
                );
            ,

忘了在之前贴一下根据id计算位置的函数了,
_getTopValueYById(id)
        const height = px2dp(49);
        return (id + 1) * height;
    

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

Anything Else?Finish it?

咱们的数据结构呢?这个只是界面作出了改动了,我们的数据还需要做出相应的变化,这里简单起见,我在构造函数中,添加了this.order=[ ],当开始map时,我们就将各个item的名字push进去,所以这个数组的顺序就代表着这个list的顺序。
this.names.map((item, i)=>
                    this.order.push(item);  //add code at here
                    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>
                    );
                )

当开始交换位置时,这个order也需要交换。
//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]];  //add code at here
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的主要内容,如果未能解决你的问题,请参考以下文章

React-Native ListView拖拽交换Item

react-native:ListView,如何从顶部推送新行

ListView 部分数据(react-native)

React-Native ListView加载图片淡入淡出效果的组件

可拖拽listview基本使用技巧(DragSortListView)

可拖拽listview基本使用技巧(DragSortListView)