我们知道,react-native
封装了一系列的组件例如
来提供触摸事件的反馈,另外Button
、Text
等组件也提供了简单的点击方法来给组件快速添加触摸事件。但是这些组件都是针对某个特定的效果,因其是定制的,所以可自定义、调整的部分就会很少,至多是效果有个选项。那么,如果我们希望做复杂的效果,改怎么办呢?答案是panResopner
。
先看下panResponder
的说明:
它可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。
它提供了一个对触摸响应系统响应器的可预测的包装。对于每一个处理函数,它在原生事件之外提供了一个新的gestureState对象。
简单来说,就是它会将触摸中发生的每次事件,每次状态的转换都通过api提供出来,供开发者做深入的开发和操作。这里的状态和过程包括:
要求成为响应者
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
开始手势操作
onPanResponderGrant: (evt, gestureState) => {}
触摸点移动
onPanResponderMove: (evt, gestureState) => {}
用户放开了所有的触摸点,且此时视图已经成为了响应者
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {}
另一个组件已经成为了新的响应者,所以当前手势将被取消
onPanResponderTerminate: (evt, gestureState) => {}
返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者(暂只支持android)
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
}
整体的流程图是:
从上面的函数就可以看到,触摸事件中,基本都包含这两个参数:
nativeEvent
gestureState
这里先给出定义,具体的每个参数的时候,会在讲触摸move的时候一起讲
给一个组件添加触摸事件很简单,有两步:
看下代码:
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》。
效果如下:
我们试着在移动的时候,让view随着手指的移动而移动。这里主要需要处理onPanResponderMove函数,先试试用nativeEvent
的locationX
和locationY
来处理,代码如下:
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);
在实际操作中,发现locationX
和locationY
的移动是非常跳跃的,和api上面的介绍并不一样。查了github
的issues
,发现很多人提locationX
在android
上面不准确或者不变的bug,但这个问题没有人提,所以我就提了一个,地址是: When I use panResponder.nativeEvent.locationX and locationY, it changes queite strange
效果如下:
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中的参数。(后面会给出解决方案)
这里先看下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其实不会变了,除非这次触摸release,产生下次触摸。
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 - 当前的纵向移动速度
上面一直在讲move的时候的用法, 下面看下如何控制是否响应
这个很简单了,返回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
从字面上理解,也是是否可以在移动中响应。我们先将其返回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的onStartShouldSetPanResponder
和 onMoveShouldSetPanResponder
都返回false,发现点击红色区域的时候,灰色的区域也会有效果,和我们正常的理解也一样。效果如下:
如果是多点触摸呢?这里就要通过nativeEvent里面的touches去处理。记得,当前活跃的panResponder只有一个,所以需要在一个responder里面取处理两个触摸。
本篇文章到底结束,这里我们简单介绍了panResponder
的触摸事件各个阶段的处理,以及参数的使用。还是有很多的问题,如果有新的消息,我会udpate这篇blog。