panResponder详解及Demo

我们知道,react-native封装了一系列的组件例如来提供触摸事件的反馈,另外ButtonText等组件也提供了简单的点击方法来给组件快速添加触摸事件。但是这些组件都是针对某个特定的效果,因其是定制的,所以可自定义、调整的部分就会很少,至多是效果有个选项。那么,如果我们希望做复杂的效果,改怎么办呢?答案是panResopner

先看下panResponder的说明:

它可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。
它提供了一个对触摸响应系统响应器的可预测的包装。对于每一个处理函数,它在原生事件之外提供了一个新的gestureState对象。

触摸整体流程

简单来说,就是它会将触摸中发生的每次事件,每次状态的转换都通过api提供出来,供开发者做深入的开发和操作。这里的状态和过程包括:

  1. 要求成为响应者

    onStartShouldSetPanResponder: (evt, gestureState) => true,
    onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
    onMoveShouldSetPanResponder: (evt, gestureState) => true,
    onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
  2. 开始手势操作

    onPanResponderGrant: (evt, gestureState) => {}
  3. 触摸点移动

    onPanResponderMove: (evt, gestureState) => {} 
  4. 用户放开了所有的触摸点,且此时视图已经成为了响应者

    onPanResponderTerminationRequest: (evt, gestureState) => true,
    onPanResponderRelease: (evt, gestureState) => {}
  5. 另一个组件已经成为了新的响应者,所以当前手势将被取消

    onPanResponderTerminate: (evt, gestureState) => {}
  6. 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者(暂只支持android)

    onShouldBlockNativeResponder: (evt, gestureState) => {
        return true;   
    }

整体的流程图是:

触摸事件参数

从上面的函数就可以看到,触摸事件中,基本都包含这两个参数:

  1. nativeEvent

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

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

这里先给出定义,具体的每个参数的时候,会在讲触摸move的时候一起讲

给组件添加触摸事件

给一个组件添加触摸事件很简单,有两步:

  1. 创建一个panResponder
  2. 将panResponder的panHandles传给这个组件

看下代码:

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            redViewBgColor: 'red',
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({redViewBgColor: 'red'})
    }

    _highlight(){
        this.setState({redViewBgColor: 'blue'})
    }

    render() {
        return (
            
                backgroundColor: this.state.redViewBgColor}]}
                    {...this._panResponder.panHandlers}
                >
            
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    redView: {
        width: 100,
        height: 100,
        marginTop: 100,
        marginLeft: 100,
    },

});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);

onPanResponderGrant是当用户触摸到屏幕时,我们需要给用户一个反馈,让他知道,触摸已经起作用了。这里我们将view变蓝。

onPanResponderRelease是当用户触摸结束时,我们也要给个反馈,让用户知道触摸已经停止。这里我们将view变回红色。

之前记得看过,之所以native比web的效果用起来好很多,就是因为native的每个步骤,都会给用户反馈,让用户知道,他的行为已经有回应了。这也是我们在做前端包括交互设计的时候,非常需要注意的事情,要知道,用户是很不耐烦的,你一定要给用户一个不太需要思考的交互,不要让他去想诸如:‘我碰到这个按钮了吗?’,‘这个是这样用的吗?’类似的问题。感兴趣的可以看下一本书《Don’t make me think》。

效果如下:

触摸移动

nativeEvnet

locationX loactionY

我们试着在移动的时候,让view随着手指的移动而移动。这里主要需要处理onPanResponderMove函数,先试试用nativeEventlocationXlocationY来处理,代码如下:

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`locationX : ${evt.nativeEvent.locationX}   locationY : ${evt.nativeEvent.locationY}`);
                this.setState({
                        marginLeft: evt.nativeEvent.locationX,
                        marginTop: evt.nativeEvent.locationY,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            
                backgroundColor: this.state.backgroundColor,
                        marginTop: this.state.marginTop,
                        marginLeft: this.state.marginLeft,
                    }
                ]}
                    {...this._panResponder.panHandlers}
                >
            
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    redView: {
        width: 100,
        height: 100,
    },

});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);

在实际操作中,发现locationXlocationY的移动是非常跳跃的,和api上面的介绍并不一样。查了githubissues,发现很多人提locationXandroid上面不准确或者不变的bug,但这个问题没有人提,所以我就提了一个,地址是: When I use panResponder.nativeEvent.locationX and locationY, it changes queite strange

效果如下:

pageX pageY

ok、根据介绍,pageX和pageY是触摸点相对于根元素的横纵坐标,其效果如何呢? 我们将上述代码中的locationX和locationY替换为pageX和pageY.代码:

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`pageX : ${evt.nativeEvent.pageX}   pageY : ${evt.nativeEvent.pageY}`);
                this.setState({
                        marginLeft: evt.nativeEvent.pageX,
                        marginTop: pageY,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            
                backgroundColor: this.state.backgroundColor,
                        marginTop: this.state.marginTop,
                        marginLeft: this.state.marginLeft,
                    }
                ]}
                    {...this._panResponder.panHandlers}
                >
            
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    redView: {
        width: 100,
        height: 100,
    },

});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);

reload下。看下效果:

这里发现,移动开始的时候,pageX和pageY其实会有个跳跃,但是在移动的过程中,其变化比较稳定。这里打了下log,发现本来是(100,100)的View,在点击的时候,(pageX,pageY)为(176,156),研究了下发现是因为我的触摸点相对于view其实是有距离的,所以造成了开始的跳跃。

接下里,我们在这个redView的外围增加一个view,看下获得的pageX和pageY是什么,代码:

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`locationX : ${evt.nativeEvent.pageX}   locationY : ${evt.nativeEvent.pageY}`);
                this.setState({
                    marginLeft: evt.nativeEvent.pageX,
                    marginTop: evt.nativeEvent.pageY,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            
                height: 200,width: 200,backgroundColor:"grey"}}>
                    backgroundColor: this.state.backgroundColor,
                            marginTop: this.state.marginTop,
                            marginLeft: this.state.marginLeft,
                        }
                    ]}
                          {...this._panResponder.panHandlers}
                    >
                

            
        );
    }
}

效果:

这里可以看到,pageX和pageY确实是当前的屏幕坐标,和其父view没有关系

怎么解决跳跃的问题呢? 目前从nativeEvent中的参数来看,是没办法解决的,所以我们继续看下gesture中的参数。(后面会给出解决方案)

gesture

moveX moveY

这里先看下moveX moveY,定义是最近一次移动时的屏幕横坐标和纵坐标,从定义上讲,和nativeEvent中的pageX和pageY应该是一样的,我们看下demo。

代码:

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`gestureState.moveX : ${gestureState.moveX}   gestureState.moveY : ${gestureState.moveY}`);
                this.setState({
                    marginLeft: gestureState.moveX,
                    marginTop: gestureState.moveY,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            
                    backgroundColor: this.state.backgroundColor,
                            marginTop: this.state.marginTop,
                            marginLeft: this.state.marginLeft,
                        }
                    ]}
                          {...this._panResponder.panHandlers}
                    >
            
        );
    }
}

效果和pageX,pageY一样,这里就不贴出来截屏了。
在有parentView的情况下,效果也是没有区别。

x0 y0

x0和y0定义写的很清楚:

x0 - 当响应器产生时的屏幕坐标
y0 - 当响应器产生时的屏幕坐标

就是当响应器产生时,即可以理解为触摸开始时,触摸的屏幕坐标。所以一旦触摸产生,x0 和 y0其实不会变了,除非这次触摸release,产生下次触摸。

dx dy

dx - 从触摸操作开始时的累计横向路程
dy - 从触摸操作开始时的累计纵向路程

这个定义写的也蛮清楚的,就是本次触摸的累积横向路程和纵向路程。嘿、还记得上面我们遇到的问题吗?pageX、pageY 和 moveX、moveY都会有的问题,第一次如果不是点击左上角来移动,第一会有点跳跃,因为我们不知道当前的触摸点相对于view的坐标,所以坐标会偏移一些。
有了dx和dy,我们就可以想办法解决这个问题了。我们只要记住上次这个view的left和top,然后set的时候,增加移动的距离,就解决了移动跳跃的问题了,先上代码:

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        };
        this.lastX = this.state.marginLeft;
        this.lastY = this.state.marginTop;
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`gestureState.dx : ${gestureState.dx}   gestureState.dy : ${gestureState.dy}`);
                this.setState({
                    marginLeft: this.lastX + gestureState.dx,
                    marginTop: this.lastY + gestureState.dy,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
                this.lastX = this.state.marginLeft;
                this.lastY = this.state.marginTop;
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            
                    backgroundColor: this.state.backgroundColor,
                            marginTop: this.state.marginTop,
                            marginLeft: this.state.marginLeft,
                        }
                    ]}
                          {...this._panResponder.panHandlers}
                    >
            
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    redView: {
        width: 100,
        height: 100,
    },

});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);

效果:

vx vy

接下来的参数是vx和vy,这里不详细介绍,给出定义:

vx - 当前的横向移动速度
vy - 当前的纵向移动速度

控制触摸是否响应

上面一直在讲move的时候的用法, 下面看下如何控制是否响应

onStartShouldSetPanResponder

这个很简单了,返回true,怎变成响应器,否则不会响应.但是当我真的去demo 的时候,诡异的事情发生了,onStartShouldSetPanResponder return false,居然view还能移动。打断点看了下,原来是因为onMoveShouldSetPanResponder是true,那么其移动是正常的。所以onStartShouldSetPanResponder只控制当触摸开始时,不会执行onPanResponderGrant函数。但是如果发生了move,则还是会走onPanResponderGrant函数。

上面简介的时候,还有个onStartShouldSetPanResponderCapture没有介绍,这个我尝试了几种组合:

onStartShouldSetPanResponder onStartShouldSetPanResponderCapture 结果
true true work
true false work
false true work
false false not work

这说明,只要两者有一个true,则触摸都会进入onPanResponderGrant。网上搜索了下,也没有找到两者的区别。说实话,我对这里还是表示疑惑的,如果有读者知道原因,请指教。我目前怀疑是react-native的bug

onMoveShouldSetPanResponder

onMoveShouldSetPanResponder 从字面上理解,也是是否可以在移动中响应。我们先将其返回false,看下效果。居然可以移动…

查了github,貌似是个bug [Touchable] How to stopPropagation touch event

还是好多bug啊,一声叹息。。。(还是我太蠢没找到???)

多层响应

我们先尝试,在红色区域下面加一个灰色的区域,而且实现上面的触摸移动效果:
代码:

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
            backgroundColor1: 'grey',
            marginTop1: 100,
            marginLeft1: 100,
        };
        this.lastX = this.state.marginLeft;
        this.lastY = this.state.marginTop;
        this.lastX1 = this.state.marginLeft1;
        this.lastY1 = this.state.marginTop1;
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
                console.log('highlight');
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`gestureState.dx : ${gestureState.dx}   gestureState.dy : ${gestureState.dy}`);
                this.setState({
                    marginLeft: this.lastX + gestureState.dx,
                    marginTop: this.lastY + gestureState.dy,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
                this.lastX = this.state.marginLeft;
                this.lastY = this.state.marginTop;
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });

        this._panResponder1 = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight1();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`gestureState.dx : ${gestureState.dx}   gestureState.dy : ${gestureState.dy}`);
                this.setState({
                    marginLeft1: this.lastX1 + gestureState.dx,
                    marginTop1: this.lastY1 + gestureState.dy,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight1();
                this.lastX1 = this.state.marginLeft1;
                this.lastY1 = this.state.marginTop1;
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    _unhighlight1(){
        this.setState({
            backgroundColor1: 'grey',
        });
    }

    _highlight1(){
        this.setState({
            backgroundColor1: 'green',
        });
    }

    render() {
        return (
            
                backgroundColor: this.state.backgroundColor1,
                        marginTop: this.state.marginTop1,
                        marginLeft: this.state.marginLeft1,
                    }
                    ]}
                      {...this._panResponder1.panHandlers}
                >
                    backgroundColor: this.state.backgroundColor,
                            marginTop: this.state.marginTop,
                            marginLeft: this.state.marginLeft,
                        }
                    ]}
                          {...this._panResponder.panHandlers}
                    >
                
            
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    greyView: {
        width: 200,
        height: 200,
    },
    redView: {
        width: 100,
        height: 100,
    },
});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);

看下效果:

点击红色区域,其变蓝,然后移动开始之后,底部的灰色也会变绿,移动发现,整体移动,相当于移动下面的区域。松开手后,上面的view不变回红色,下面的view变回灰色.只有单独点上面的view,而不移动,松开后其才会转成变回红色。

这是为什么呢? 我们分别在两个view的move函数中打log,发现根本没有进入上面view的move函数。这时就要介绍下上面讲到的另一个函数了:onPanResponderTerminate,我们发现,移动的时候,进入了上面view的这个函数,说明它的控制权,被抢走了。rn的多层触摸事件,我理解是从底层挨个网上查询,看哪个view想要接收,一定被接收,则消息不再传递。

把红色view的onStartShouldSetPanResponderonMoveShouldSetPanResponder都返回false,发现点击红色区域的时候,灰色的区域也会有效果,和我们正常的理解也一样。效果如下:

多点触摸

如果是多点触摸呢?这里就要通过nativeEvent里面的touches去处理。记得,当前活跃的panResponder只有一个,所以需要在一个responder里面取处理两个触摸。

本篇文章到底结束,这里我们简单介绍了panResponder的触摸事件各个阶段的处理,以及参数的使用。还是有很多的问题,如果有新的消息,我会udpate这篇blog。

你可能感兴趣的:(react-native,react-native,panResopnd)