native react 拖拽动作

2018-12-19 14:29:32 iOSTianNan 阅读数 1459

1.PanResnponder相关Api

    componentWillMount() {
        this.panResponder = PanResponder.create({

            /***************** 要求成为响应者 *****************/
            // 单机手势是否可以成为响应者
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            // 移动手势是否可以成为响应者
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            // 拦截子组件的单击手势传递,是否拦截
            onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
            // 拦截子组件的移动手势传递,是否拦截
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,


            /***************** 响应者事件回调处理 *****************/
            // 单击手势监听回调
            onPanResponderGrant: (e, gestureState) => {
                console.log('onPanResponderGrant==>' + '单击手势申请成功,开始处理手势')
                this._onPanResponderGrant(e)
            },
            // 移动手势监听回调
            onPanResponderMove: (e, gestureState) => {
                console.log('onPanResponderMove==>' + '移动手势申请成功,开始处理手势' + `${gestureState}`)
                this._onPanResponderMove(e, gestureState);
            },
            // 手势动作结束回调
            onPanResponderEnd: (evt, gestureState) => {
                console.log('onPanResponderEnd==>' + '手势操作完成了,用户离开')
                this._onPanResponderEnd(evt)
            },
            // 手势释放, 响应者释放回调
            onPanResponderRelease: (e) => {
                // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
                // 一般来说这意味着一个手势操作已经成功完成。
                console.log('onPanResponderRelease==>' + '放开了触摸,手势结束')
            },
            // 手势申请失败,未成为响应者的回调
            onResponderReject: (e) => {
                // 申请失败,其他组件未释放响应者
                console.log('onResponderReject==>' + '响应者申请失败')
            },

            // 当前手势被强制取消的回调
            onPanResponderTerminate: (e) => {
                // 另一个组件已经成为了新的响应者,所以当前手势将被取消
                console.log('onPanResponderTerminate==>' + '由于某些原因(系统等),所以当前手势将被取消')
            },
            onShouldBlockNativeResponder: (evt, gestureState) => {
                // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
                // 默认返回true。目前暂时只支持android。
                return true;
            },
        })
    }

一般使用手势的话,以上Api基本可以实现

在这里插入图片描述

2. 使用绝对布局方式,完成拖拽小球的实现

render() {
        return (
            <View style={{flex: 1}}>

                {this.renderNomalNavigationBar('触摸响应页')}

                <TouchableOpacity
                    onPress={() => this.enterPenresnponderShow()}
                    style={{backgroundColor: '#00ffee', width: pxdp.width, height: pxdp.fixHeight(50)}}
                />
                <View style={{
                    backgroundColor: this.state.isHeightShow ? '#ffddff' : '#ff0fa0',
                    borderColor: '#ff0',
                    borderWidth: 1,
                    borderRadius: pxdp.fixWidth(25),
                    width: pxdp.fixWidth(50),
                    height: pxdp.fixWidth(50),

                    position: 'absolute',
                    left: this.state.marginLeft,
                    top: this.state.marginTop,
                }} {...this.panResponder.panHandlers}
                />
            </View>


        )
    }


    /**************************************** 逻辑处理 ****************************************/
    /*
    * 备注: 拖动小圆点的实现方法 - 使用绝对定位
    * 对小圆点设置绝对布局,
    * 通过触发开始的pageXY与moveXY的差值
    * 来变更top,left的大小,
    * position一定要为 absolute
    * */

    // 单点手势开始
    _onPanResponderGrant(e) {

        //1. 开始触发时,获取触摸点绝对位置
        this.touchX = e.nativeEvent.locationX;
        this.touchY = e.nativeEvent.locationY;

    }

    // 移动手势处理
    _onPanResponderMove(evt, g) {
        console.log("        ");
        console.log(evt.nativeEvent);
        console.log('=================')
        console.log(g);

        //2. 根据触发点计算真实的左侧,顶侧位移变化
        let realMarginLeft = g.moveX - this.touchX;
        let realMarginTop = g.moveY - this.touchY;

        this.setState({
            marginLeft: realMarginLeft,
            marginTop: realMarginTop
        })


    }

    // 手势结束
    _onPanResponderEnd(evt) {
        let pageX = evt.nativeEvent.pageX;

        this.setState({
            isHeightShow: false,
        });

        if (pageX <= pxdp.width / 4) {
            pageX = 0;
            this.setState({
                marginLeft: pageX,
            })
        }
        if (pageX >= pxdp.width * 3 / 4) {
            pageX = pxdp.width - pxdp.fixWidth(50);
            this.setState({
                marginLeft: pageX,
            })
        }
    }

以上是通过绝对布局的方式来实现了一个拖拽的小球,但不是所有地方都应该使用绝对布局来实现拖拽,有些地方不应该使用绝对布局,并且也难以实现,失去了Flex弹性盒子模型的优势

3.通过相对布局,实现列表的拖拽

在这里插入图片描述

 /**************************************** 渲染 ****************************************/
    render() {
        return (
            <View style={{
                width: pxdp.width, height: this.cellHeight, borderWidth: 1, borderColor: '#090',
                borderRadius: 10, top: this.state.topOY, backgroundColor: '#00ddea'
            }} {...this.panResponder.panHandlers}>
                <Text>{this.props.title}</Text>
            </View>
        )
    }

    /**************************************** 逻辑 ****************************************/


    //拖拽触发
    _onPanResponderGrant(e, g) {
        console.log('触发了: 点击Grant')
        // console.log(e.nativeEvent.locationY);
        // console.log(e.nativeEvent.pageY);

        //1. 获取触摸点的绝对位置
        this.touchY = e.nativeEvent.pageY;
    }

    //拖拽移动
    _onPanResponderMove(e, g) {
        console.log('触发了: 移动Move');
        // console.log(e.nativeEvent);
        // console.log(g);

        //2. 计算top真正的移动距离

        //2.1 获取移动过程中,在Y轴上移动了多少(该值不能直接作为top变量,否则不能实现拖拽精准移动)
        let changeY = g.moveY - this.touchY;
        //2.2 根据本次的Y轴移动及上次Y轴移动,计算本次真正的Y轴移动变化量
        let realChangeY = changeY - this.lastChangeY;
        //2.3 计算移动中, 触摸点真正的移动了多少距离
        this.realChangeY = this.realChangeY + realChangeY;
        //2.4 同理,top相对触摸点,也移动了同样的距离
        this.setState({
            topOY: this.realChangeY
        });
        //2.5 记录上次移动的距离
        this.lastChangeY = changeY;
    }

    //结束拖拽
    _onPanResponderRelease(e, g) {
        console.log('释放了: 离开屏幕Release')
        //3. 结束触发后, 清除本次交互时,记录的Y轴移动大小,避免影响下次再次交互
        this.lastChangeY = 0;
    }

总结一下

PanResponder的交互过程中,会返回我们nativeEvent及gestureState对象,里面有很多参数以便于我们用于修改组件属性
引用官方的描述如下

nativeEvent
changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
identifier - 触摸点的 ID
locationX - 触摸点相对于父元素的横坐标
locationY - 触摸点相对于父元素的纵坐标
pageX - 触摸点相对于根元素的横坐标
pageY - 触摸点相对于根元素的纵坐标
target - 触摸点所在的元素 ID
timestamp - 触摸事件的时间戳,可用于移动速度的计算
touches - 当前屏幕上的所有触摸点的集合


-=-=-=-=-=-=-=-=

gestureState对象有如下的字段:

stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
moveX - 最近一次移动时的屏幕横坐标
moveY - 最近一次移动时的屏幕纵坐标
x0 - 当响应器产生时的屏幕坐标
y0 - 当响应器产生时的屏幕坐标
dx - 从触摸操作开始时的累计横向路程
dy - 从触摸操作开始时的累计纵向路程
vx - 当前的横向移动速度
vy - 当前的纵向移动速度
numberActiveTouches - 当前在屏幕上的有效触摸点的数量

常用的有pageXY, 是相对于屏幕的触摸点
locationXY是相对于组件触摸点位置
moveXY则是每次移动过程中反馈出的上一次移动距离值
由于FLex弹性盒子模型的使用, 我们不太能用坐标点来改变组件的位置,因此,我们可以改变组件的top/left等属性
但是,由于直接使用moveXY等值去修改top/left等属性,会造成在刚触摸移动的时候,top/left等属性得到了触摸点的左边,会造成一定程度位置混乱偏移,因此,需要手动来计算top/left的变化情况, 而绝对布局下,这个问题则相对来说比较容易处理

直接使用会出现如下问题

在这里插入图片描述

2016-11-28 11:05:57 w337198302 阅读数 8262

在高仿“掘金”客户端的那个项目中,你会发现在打开和关闭“首页展示标签”中,我并没有实现可拖拽换位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)
    }
});

2018-06-10 13:38:01 wqzone 阅读数 2812
先上效果图,意思意思。其实原理很简单,没有想的那么难。

大家在改造的时候,请注意 this.offset 的值,因为它关系到查找目标box的index(原理:手势释放时,所在的坐标值来推算出目标box的Index),本文代码可读性还需要改造,代码写的有点乱。


借鉴了:
https://blog.csdn.net/nfq6612/article/details/78675515(原文中有几处bug,运行不出来他说的效果)




'use strict';


import React, { Component } from 'react';

import {
    Image,
    StyleSheet,
    LayoutAnimation,
    Text,
    TouchableHighlight,
    TouchableOpacity,
    TouchableWithoutFeedback,
    Dimensions,
    PanResponder,
    View,
    Alert,
    NativeModules,
    YellowBox
} from 'react-native';
import tools from "../../common/tools";
import {Icon} from 'native-base';


//获取屏幕宽高
let { width, height } = Dimensions.get('window');
let boxWidth = (width - 10) / 4 - 10;
let boxHeight = (width - 10) / 8 - 10;
const { UIManager } = NativeModules;


UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
YellowBox.ignoreWarnings(['Warning: isMounted(...) is deprecated', 'Module RCTImageLoader']);


export default class CustomTab extends Component {


    static navigationOptions = ({navigation})=>({
        headerTitle: '栏目设置',
        gesturesEnabled:false,
        headerRight: <View />
    });


    constructor(props) {
        super(props);
        let {items,sourceItems} = this.props;
        this._width = width / 4;
        this.topIndex = 0;
        this.leftIndex = 0;
        this.index = 0;
        this.finalTopIndex = 0;
        this.finalLeftIndex = 0;
        this.finalIndex = 0;
        this.prev_left = 0;
        this.prev_top = 0;
        this.left = 0;
        this.top = 0;


        this.barHeight=40;//qzwang
        this._marginTop=10;//qzwang
        this._borderTopWidth=1;//qzwang
        this.offset = this.barHeight + this._marginTop + this._borderTopWidth; //偏移


        //this.boxsPlanHeight =  (this._width / 2) +  boxHeight;
this.items = [{
                id: 0,
                name: '首页',
                ordernum: 0
            },{
                id: 1,
                name: '成交价',
                ordernum: 0
            },{
                id: 2,
                name: '库存',
                ordernum: 0
            },{
                id: 3,
                name: '快讯',
                ordernum: 0
            },{
                id: 4,
                name: '分析',
                ordernum: 0
            },{
                id: 5,
                name: '视频',
                ordernum: 0
            },{
                id: 6,
                name: '钢厂',
                ordernum: 0
            }
        ];
        this.sourceItems=[{
                id: 7,
                name: '期货',
                ordernum: 0
            },{
                id: 8,
                name: '设置',
                ordernum: 0
            },{
                id: 9,
                name: '内容',
                ordernum: 0
            }

        ];

        this.state = {};
    }
    componentWillReceiveProps(nextProps){
    }


    componentWillMount() {
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return gestureState.dx !== 0 || gestureState.dy !== 0;
            },
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            onPanResponderGrant: (evt, gestureState) => {
                const { pageX, pageY } = evt.nativeEvent;
                this.topIndex = Math.floor((pageY - this.offset) / (this._width / 2)) - 1;//qzwang 这里应该减法减1
                this.leftIndex = Math.floor(pageX / this._width);
                this.index = this.topIndex * 4 + this.leftIndex;
                this.prev_left = this._width * this.leftIndex;
                this.prev_top = this._width / 2 * this.topIndex;


            },
            onPanResponderMove: (evt, gestureState) => {
                if (this.index >= 0 && this.index < this.items.length ) {
                    this.left = this.prev_left + gestureState.dx;
                    this.top = this.prev_top + gestureState.dy;
                    let box = this.refs[this.items[this.index].id];
                    box.setNativeProps({
                        style: { top: this.top, left: this.left }
                    });
                }
            },
            onPanResponderTerminationRequest: (evt, gestureState) => true,
            onPanResponderRelease: (evt, gestureState) => {
                if(this.index <0 || this.index >= this.items.length){
                    this.forceUpdate();
                    return;
                }
                    const { pageX, pageY } = evt.nativeEvent;
                    this.finalTopIndex = Math.floor((pageY - this.offset) / (this._width / 2)) -1;
                    this.finalLeftIndex = Math.floor(pageX / this._width);
                    let index = this.finalTopIndex * 4 + this.finalLeftIndex;
                    this.prev_left = this._width * this.finalTopIndex;
                    this.prev_top = this._width / 2 * this.finalTopIndex;


                    if (index >= 0 && index < this.items.length && this.items[index]) {
                        if (index > this.index) {
                            this.moveBack(this.index,index);
                        } else if (index < this.index) {
                            this.moveForward(this.index,index);
                        }else{
                            this.delteBox(index);//删除
                        }
                    } else {
                        //移出范围,则重新回到原始位置
                        let box1 = this.refs[this.items[this.index].id];
                        let top1 = Math.floor(this.index / 4) * (this._width / 2);
                        let left1 = (this.index % 4) * this._width;
                        LayoutAnimation.linear();


                        box1.setNativeProps({
                            style: {
                                top: top1,
                                left: left1,
                                zIndex:9999
                            }
                        });
                        LayoutAnimation.configureNext(LayoutAnimation.Presets.spring); //系统自带
                    }
            },
            onShouldBlockNativeResponder: (event, gestureState) => true
        });
    }


    moveForward = function(sourceIndex,targetIndex){
        //往前移动
        for (let i = sourceIndex; i > targetIndex; i--) {
            let box2 = this.refs[this.items[i - 1].id];
            let top2 = Math.floor(i / 4) * (this._width / 2);
            let left2 = (i % 4) * this._width;
            //LayoutAnimation.linear();
            LayoutAnimation.configureNext(
                LayoutAnimation.create(
                    200,
                    LayoutAnimation.Types.linear,
                    LayoutAnimation.Properties.scaleXY
                )
            );
            box2.setNativeProps({
                style: {
                    top: top2,
                    left: left2
                }
            });
        }
        let box1 = this.refs[this.items[sourceIndex].id];
        let top1 = Math.floor(targetIndex / 4) * (this._width / 2);
        let left1 = (targetIndex % 4) * this._width;


        box1.setNativeProps({
            style: {
                top: top1,
                left: left1
            }
        });
        let temp = this.items[sourceIndex];
        for (let i = sourceIndex; i > targetIndex; i--) {
            this.items[i] = this.items[i - 1];
        }
        this.items[targetIndex] = temp;
    }
    moveBack = function(sourceIndex,targetIndex){
        for (let i = sourceIndex; i < targetIndex; i++) {
            let box2 = this.refs[this.items[i + 1].id];
            let top2 = Math.floor(i / 4) * (this._width / 2);
            let left2 = (i % 4) * this._width;
            //LayoutAnimation.linear();
            LayoutAnimation.configureNext(
                LayoutAnimation.create(
                    200,
                    LayoutAnimation.Types.linear,
                    LayoutAnimation.Properties.scaleXY
                )
            );
            box2.setNativeProps({
                style: {
                    top: top2,
                    left: left2
                }
            });
        }
        let box1 = this.refs[this.items[sourceIndex].id];
        let top1 = Math.floor(targetIndex / 4) * (this._width / 2);
        let left1 = (targetIndex % 4) * this._width;


        box1.setNativeProps({
            style: {
                top: top1,
                left: left1
            }
        });
        let temp = this.items[sourceIndex];
        for (let i = sourceIndex; i < targetIndex; i++) {
            this.items[i] = this.items[i + 1];
        }
        this.items[targetIndex] = temp;
    }
    delteBox = function(index){
        if(this.items.length==1)
        {
            tools.showToast("必须保留至少一个栏目");
            return;
        }
        if( index <0 || index >= this.items.length ){
            tools.showToast("应用出错");
            return;
        }
        //1、移除
        let item = this.items[index];
        this.items.removeItem(o=>o.id==item.id);
        //2、增加
        this.sourceItems.removeItem(o=>o.id==item.id);
        this.sourceItems.push(item);
        //3、重新刷新
        this.forceUpdate();
    }
    onAddTab=function(item){
        this.items.push(item);
        this.sourceItems.removeItem(o=>o.id==item.id);
        this.forceUpdate();
    }
    renderSource=function(){
        return this.sourceItems.map((item, index) => {
            let top =  Math.floor(index / 4) * (this._width / 2);
            let left = (index % 4) * this._width;


            return (
                <TouchableOpacity
                    onPress={()=>{ this.onAddTab(item) }}
                    key={'' + item.id}
                    style={[componentStyles.touchBox, { top, left }]}
                >
                    <View
                        style={{
                            alignItems: 'center',
                            justifyContent: 'center',
                            width: (width - 10) / 4 - 10,
                            height: (width - 10) / 8 - 10,
                            backgroundColor: '#f3f3f3',
                            borderWidth: 1,
                            borderRadius: 5,
                            borderColor: '#f3f3f3'}}>
                        <Text style={{color: '#5c5c5c'}}>{item.name}</Text>
                    </View>
                </TouchableOpacity>
            );
        });
    }
    renderBoxs=function(){
        return this.items.map((item, index) => {
            let top = Math.floor(index / 4) * (this._width / 2);
            let left = (index % 4) * this._width;
            return (
                <View
                    ref={'' + item.id}
                    {...this._panResponder.panHandlers}
                    key={'' + item.id}
                    style={[componentStyles.touchBox, { top, left }]}
                >
                    <TouchableOpacity onPress={()=>{ this.delteBox(item.id) }} style={componentStyles.closeButton}  underlayColor={'#f9f3f9'}>
                        <Icon name='ios-close' style={{ color:'#ffffff',textAlign:'center', fontSize:15 }} />
                    </TouchableOpacity>
                    <View
                        style={{
                            alignItems: 'center',
                            justifyContent: 'center',
                            width: (width - 10) / 4 - 10,
                            height: (width - 10) / 8 - 10,
                            backgroundColor: '#f3f3f3',
                            borderWidth: 1,
                            borderRadius: 5,
                            borderColor: '#f3f3f3'}}>
                        <Text style={{ color: index > 0 ? '#5c5c5c' : '#ff0000' }}>{item.name}</Text>
                    </View>
                </View>
            );
        });
    }
    render() {
        const boxes = this.renderBoxs();
        const sourceBoxs = this.renderSource();


        let {style,ref,...props} = this.props;
        if(!style){
            style={};
        }
        if(!(style instanceof Array)){
            style=[style];
        }


        return (
            <View style={[componentStyles.container,...style]}>
                <View style={[componentStyles.crossBar,{height: this.barHeight,borderBottomWidth:this._borderTopWidth}]}>
                    <Text>拖拽排序,轻触添加或删除栏目</Text>
                </View>
                <View style={[componentStyles.plan,{marginTop: this._marginTop}]}>
                    {boxes}
                </View>
                <View style={componentStyles.sourcePlan}>
                    <View style={[componentStyles.crossBar,{height: this.barHeight,borderBottomWidth:this._borderTopWidth}]}>
                        <Text>全部栏目</Text>
                    </View>
                    <View style={[componentStyles.sourceBoxs,{marginTop: this._marginTop}]}>
                        {sourceBoxs}
                    </View>
                </View>
            </View>
        );
    }
}


const componentStyles = StyleSheet.create({
    touchBox: {
        width: boxWidth,
        height: boxHeight,
        backgroundColor: '#fff',
        position: 'absolute',
        left: 0,
        top: 0,
        zIndex:888
    },
    container:{
        flex: 1,
        flexDirection: 'column',
        backgroundColor: '#fff'
    },
    crossBar:{
        flexDirection: 'row',
        backgroundColor: '#fff',
        paddingLeft:10,
        alignItems:'center',
        borderBottomColor:'#969696'
    },
    plan:{


        width: width,
        marginLeft: 10,
        marginBottom: 10,
        marginRight: 10
    },
    sourcePlan:{
        flex:1,
        top: 200,
    },
    sourceBoxs:{
        flexDirection: 'column',
        width: width,
        marginLeft: 10,
        marginBottom: 10,
        marginRight: 10,
        flex:1
    },
    closeButton:{
        height:20,
        width:20,
        backgroundColor:'#CDC9C9',
        borderRadius:10,
        position:'absolute',
        top:1,
        right:1,
        zIndex:9999999,
    },
    bottomBar:{
        height:50
    }
});
2018-08-08 07:52:42 weixin_34309435 阅读数 232

2019.2: 优化拖拽不移动时自动恢复,现在这个插件应该没有任何问题。新加一个实战演示例子,后面有时间会对这个例子进行加动画,删除时item向下到待选的item动画,和待选到item。还有滑动时自动向下滑动动画。


最近由于业务需求需要实现一个功能需要实现图片的上传和排序和删除,在网上搜索了几款发现都需要固定列数,感觉不太友好,所以自己实现了一个可以不需要设定列数的排序,而且布局高度实现自适应

源码链接

效果图对比(固定列数和自适应流布局)

动态图

实现

其实拖拽排序在大多数编程语言里已经有很多中三方插件可以使用,实现方法都差不多,而且例如Android和iOS或者现在的React-Native他们逻辑几乎是可以共用,你会写一个语言的拖拽排序,其他的都差不多。

梳理一下步骤
    1. 开始触发: 长按或触摸到达一定时间时触发开始排序,这时可以进行把被单机的item放大、透明、抖动动画。
    1. 开始滑动:
    • (1) 被拖拽的item随着手指的滑动而滑动
    • (2) 被拖动的item滑动到第x个时,item到x之间的item进行左滑右滑一个位置的动画。
    1. 松开手指:
    • (1) 被拖拽的这个item通过四舍五入进入相应的位置。
    • (2) 数据进行替换并重绘加布局矫正。

tip: 滑动逻辑,例如当你把index=1拖到index=3,不是将1和3替换(0,3,2,1,4),而是(0,3,1,2,4)这才是拖拽后结果,只将被拖拽的一个替换到要去的位置,其他的向前和向后移动

主要代码
// 触摸事件的监听
this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => this.props.sortable,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => {
                this.isMovePanResponder = false
                return false
            },
            //  接管触摸加滑动事件
            onMoveShouldSetPanResponder: (evt, gestureState) => this.isMovePanResponder,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.isMovePanResponder,

            onPanResponderGrant: (evt, gestureState) => {},
            onPanResponderMove: (evt, gestureState) => this.moveTouch(evt,gestureState),
            onPanResponderRelease: (evt, gestureState) => this.endTouch(evt),

            onPanResponderTerminationRequest: (evt, gestureState) => false,
            onShouldBlockNativeResponder: (evt, gestureState) => false,
        })

//这里使用长按触发开发拖拽事件,其实开始是使用触摸一定时间后触发事件的,但和View的单机事件有冲突不好解决,所以选择了长按触发事件
startTouch(touchIndex) {
      // 接管滑动
        this.isMovePanResponder = true
        //if (this.measureTimeOut) clearTimeout(this.measureTimeOut)

        if (sortRefs.has(touchIndex)) {
            if (this.props.onDragStart) {
                this.props.onDragStart(touchIndex)
            }
            //变大和加透明
            Animated.timing(
                this.state.dataSource[touchIndex].scaleValue,
                {
                    toValue: maxScale,
                    duration: scaleDuration,
                }
            ).start(()=>{
              //  备份被触摸的事件
                this.touchCurItem = {
                    ref: sortRefs.get(touchIndex),
                    index: touchIndex,
                    //  记录之前的位置
                    originLeft: this.state.dataSource[touchIndex].originLeft,
                    originTop: this.state.dataSource[touchIndex].originTop,
                    moveToIndex: touchIndex,
                }
            })
        }
    }

//滑动
moveTouch (nativeEvent,gestureState) {
        if (this.touchCurItem) {

            let dx = gestureState.dx
            let dy = gestureState.dy

            const rowNum = parseInt(this.props.parentWidth/this.itemWidth);
            const maxWidth = this.props.parentWidth-this.itemWidth
            const maxHeight = this.itemHeight*Math.ceil(this.state.dataSource.length/rowNum) - this.itemHeight

            //出界后取最大或最小值防止出界
            if (this.touchCurItem.originLeft + dx < 0) {
                dx = -this.touchCurItem.originLeft
            } else if (this.touchCurItem.originLeft + dx > maxWidth) {
                dx = maxWidth - this.touchCurItem.originLeft
            }
            if (this.touchCurItem.originTop + dy < 0) {
                dy = -this.touchCurItem.originTop
            } else if (this.touchCurItem.originTop + dy > maxHeight) {
                dy = maxHeight - this.touchCurItem.originTop
            }


            let left = this.touchCurItem.originLeft + dx
            let top = this.touchCurItem.originTop + dy
           //置于最上层
            this.touchCurItem.ref.setNativeProps({
                style: {
                    zIndex: touchZIndex,
                }
            })

            //滑动时刷新布局,这里直接刷新Animated的数字就可以进行局部刷新了
this.state.dataSource[this.touchCurItem.index].position.setValue({
                x: left,
                y: top,
            })


            let moveToIndex = 0
            let moveXNum = dx/this.itemWidth
            let moveYNum = dy/this.itemHeight
            if (moveXNum > 0) {
                moveXNum = parseInt(moveXNum+0.5)
            } else if (moveXNum < 0) {
                moveXNum = parseInt(moveXNum-0.5)
            }
            if (moveYNum > 0) {
                moveYNum = parseInt(moveYNum+0.5)
            } else if (moveYNum < 0) {
                moveYNum = parseInt(moveYNum-0.5)
            }

            moveToIndex = this.touchCurItem.index+moveXNum+moveYNum*rowNum
            
            if (moveToIndex > this.state.dataSource.length-1) moveToIndex = this.state.dataSource.length-1

          // 其他item向左和向右滑动
            if (this.touchCurItem.moveToIndex != moveToIndex ) {
                this.touchCurItem.moveToIndex = moveToIndex

                this.state.dataSource.forEach((item,index)=>{

                    let nextItem = null
                    if (index > this.touchCurItem.index && index <= moveToIndex) {
                        nextItem = this.state.dataSource[index-1]

                    } else if (index >= moveToIndex && index < this.touchCurItem.index) {
                        nextItem = this.state.dataSource[index+1]

                    } else if (index != this.touchCurItem.index &&
                        (item.position.x._value != item.originLeft ||
                            item.position.y._value != item.originTop)) {
                        nextItem = this.state.dataSource[index]

                        //有时前一个或者后一个数据有个动画差的原因无法回到正确位置,这里进行矫正
                    } else if ((this.touchCurItem.index-moveToIndex > 0 && moveToIndex == index+1) ||
                        (this.touchCurItem.index-moveToIndex < 0 && moveToIndex == index-1)) {
                        nextItem = this.state.dataSource[index]
                    }

                    //需要滑动的就进行滑动动画
                    if (nextItem != null) {
                        Animated.timing(
                            item.position,
                            {
                                toValue: {x: parseInt(nextItem.originLeft+0.5),y: parseInt(nextItem.originTop+0.5)},
                                duration: slideDuration,
                                easing: Easing.out(Easing.quad),
                            }
                        ).start()
                    }

                })
            }



        }
    }

//触摸事件
    endTouch (nativeEvent) {
        
        //clear
        if (this.measureTimeOut) clearTimeout(this.measureTimeOut)

        if (this.touchCurItem) {
            if (this.props.onDragEnd) {
                this.props.onDragEnd(this.touchCurItem.index,this.touchCurItem.moveToIndex)
            }
            //this.state.dataSource[this.touchCurItem.index].scaleValue.setValue(1)
            Animated.timing(
                this.state.dataSource[this.touchCurItem.index].scaleValue,
                {
                    toValue: 1,
                    duration: scaleDuration,
                }
            ).start()
            this.touchCurItem.ref.setNativeProps({
                style: {
                    zIndex: defaultZIndex,
                }
            })
            this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex)
            this.touchCurItem = null
        }
    }

//刷新数据
    changePosition(startIndex,endIndex) {

        if (startIndex == endIndex) {
            const curItem = this.state.dataSource[startIndex]
            this.state.dataSource[startIndex].position.setValue({
                x: parseInt(curItem.originLeft+0.5),
                y: parseInt(curItem.originTop+0.5),
            })
            return;
        }

        let isCommon = true
        if (startIndex > endIndex) {
            isCommon = false
            let tempIndex = startIndex
            startIndex = endIndex
            endIndex = tempIndex
        }

        const newDataSource = [...this.state.dataSource].map((item,index)=>{
            let newIndex = null
            if (isCommon) {
                if (endIndex > index && index >= startIndex) {
                    newIndex = index+1
                } else if (endIndex == index) {
                    newIndex = startIndex
                }
            } else {
                if (endIndex >= index && index > startIndex) {
                    newIndex = index-1
                } else if (startIndex == index) {
                    newIndex = endIndex
                }
            }

            if (newIndex != null) {
                const newItem = {...this.state.dataSource[newIndex]}
                newItem.originLeft = item.originLeft
                newItem.originTop = item.originTop
                newItem.position = new Animated.ValueXY({
                    x: parseInt(item.originLeft+0.5),
                    y: parseInt(item.originTop+0.5),
                })
                item = newItem
            }

            return item
        })

        this.setState({
            dataSource: newDataSource
        },()=>{
            if (this.props.onDataChange) {
                this.props.onDataChange(this.getOriginalData())
            }
            //防止RN不绘制开头和结尾
            const startItem = this.state.dataSource[startIndex]
            this.state.dataSource[startIndex].position.setValue({
                x: parseInt(startItem.originLeft+0.5),
                y: parseInt(startItem.originTop+0.5),
            })
            const endItem = this.state.dataSource[endIndex]
            this.state.dataSource[endIndex].position.setValue({
                x: parseInt(endItem.originLeft+0.5),
                y: parseInt(endItem.originTop+0.5),
            })
        })

    }

复制代码

后续会加上添加和删除Item渐变动画


源码链接

2018-03-26 14:03:16 money9sun 阅读数 4279

      React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的UI框架 React 在原生移动应用平台的衍生产物,目前支持iOS和安卓两大平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。(百度百科)

      小编有幸参与了公司APP的开发,该APP采用混合式开发,本人负责RN的几个功能的开发,其中最复杂的就是应用中心的功能,如下图所示。模块分为上下两部分,下面是所有应用,上面是下方勾选的应用,可以上下左右滑动(图中只有一行,后面改成了两行,故可以上下或者斜着滑动,偷懒就不截图了)

         所以一共要实现接收数据、选择应用、调整顺序、提交这个几个功能,下面重点讲述调整顺序的代码,其他的代码可能会掠过。

1、获取数据

定一个几个变量,用于保存我的应用和所有应用,获取数据调用了原生的方法,大家用RN的获取数据的方法是一样的。

 

在构造器里面的变量
this.myapps = [];
this.application= [];

 

//获取数据
dsfectchData(){
    RNBridgeManager.queryAppList((error, result) => {
        if(error){
            console.log("---q--->error:" + error);
        }else{
            var myapp = [];
            for(var i=0;i<result.length;i++){
                if(result[i].myAppList == 1){
                    myapp.push(result[i]);
                    console.log( i + " : " + result[i].appId);
                }
            }
            this.myapps = myapp;   //保存我的应用数据
            console.log("this.myapps" + this.myapps.length);
            this.application = result; // 保存所有数据
        }
    });
}



2、展示数据

重点来看我的应用的数据展示方式,如下图第一个红色标记的是将数据放入到一个数组中,这个数据记录
顺序;第二个红色标记用于和上一个标记中的order数组一起,用于排序和重新展示;第三个红色标记的
部分是每个可拖动的图标的定位,当然首先要将定位设定为:position = 'absolute'。绿色底标注
的为可拖动的核心,即触摸操作。


 
<View style={{flexDirection: 'row',height:25,backgroundColor: '#fff',alignItems: 'center',paddingTop:10,marginTop:px2pt(10)}}>
    <Image
        source={{uri:this.state.navbar}}
        style={{width:3,height:15,marginLeft:px2pt(10)}}
    />
    <Text style={styles.itemTitle}>我的应用</Text>
    <Text style={styles.itemTitleDes}>(按住拖动调整顺序)</Text>
</View>
<View style={styles.containerMy}>
    {this.myapps.map((item, i)=>{
        this.order.push(item);
        return (
            <View
                {...this._panResponder.panHandlers}
                ref={(ref) => this.items[i] = ref}
                key={i}
                style={[styles.item, {top: (parseInt(i/4))*90 + 10,left:(i%4)*this.widthSreen}]}>
                <Image
                    source={{uri:this.state.shortcut_icon_bg}}
                    style={styles.thumbnail}
                />
                <Image
                    source={this._getApplicationIcons(item.appId)}
                    style={styles.thumbnailInside}
                />
                <TextActivity text={item.appName} width={containerWidth}/>
                <TouchableWithoutFeedback onPress={this._pressIcon.bind(this,this.myapps,i,1)} style={styles.buttonRightTop}>
                    <Image
                        source={{uri:this.state.shortcut_icon_del}}
                        style={styles.imageRightTop}
                    />
                </TouchableWithoutFeedback>
            </View>
        );
    })}
</View>
3、滑动操作部分,此部分依次为

1)点击时判定是哪个图标(根据坐标)

2)判定移动位置是否已经和其他的图标有重合,如果有,则交换两个图标的位置

3)释放后的显示结果


componentWillMount(){
    this._panResponder = PanResponder.create({
        onStartShouldSetPanResponder: (evt, gestureState) => true,
        onMoveShouldSetPanResponder: (evt, gestureState) => true,
        onPanResponderGrant: (evt, gestureState) => {
            const {pageX,pageY, locationX,locationY} = evt.nativeEvent;
            console.log("--eric-->:pageX:" + pageX + "--pageY:" + pageY + "--locationX:" + locationX + "--locationY:" + locationY);
            this.index = this._getIdByPosition(pageX,pageY);
            //逻辑应该是点击的一瞬间,判定是第几个 (parseInt(i/4))*90,left:(i%4)*this.widthSreen}
            this.preX = (this.index%4)*this.widthSreen;
            this.preY = (parseInt(this.index/4))*90 + 10;
            //get the taped item and highlight it
            let item = this.items[this.index];
            if(item!=null){
                item.setNativeProps({
                    style: {
                        shadowColor: "#000",
                        shadowOpacity: 0.3,
                        shadowRadius: 5,
                        shadowOffset: {height: 0, width: 2},
                        elevation: 5
                    }
                });
            }
        },
        // 移动判定是否进入了某个图标的领域,如果进入则交换
         onPanResponderMove: (evt, gestureState) => {
            let left = this.preX + gestureState.dx;
            let top = this.preY + gestureState.dy;
            console.log("--eric-->top:" + top + ":" + gestureState.dy);
            let item = this.items[this.index];
            if(item!=null) {
                item.setNativeProps({
                    style: {left: left,top: top}
                });
            }
            let collideIndex = this._getIdByPosition(evt.nativeEvent.pageX,evt.nativeEvent.pageY);
            if(collideIndex !== this.index && collideIndex !== -1) {
                let collideItem = this.items[collideIndex];
                if(collideItem!=null){
                    collideItem.setNativeProps({
                        style: {left: this._getLeftValueYById(this.index),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
            if(item!=null){
                item.setNativeProps({
                    style: {...shadowStyle, left: this._getLeftValueYById(this.index),top: this._getTopValueYById(this.index)}
                });
            }
            console.log(this.order);
        },
        onPanResponderTerminate: (evt, gestureState) => {
            // Another component has become the responder, so this gesture
            // should be cancelled
        }
    });
}

 完整代码

https://github.com/EricLoveMia/ApplicationCenterOfRN.git