自我成长——react native手势触摸

1,559 阅读5分钟

简介

在诸多移动应用中,需要有许多手势识别的使用场景。比如当涂抹、上滑、下滑、侧滑、拖动等,都涉及到方方面面的功能交互。而在react native中,就有提供处理这些手势的API,下面就来一一介绍相关的响应方法。

PanResponder类

当我们在移动设备上进行手势交互时,所进行对操作会比web上更复杂。而PanResponder类可以将多点触摸操作协调成一个手势,使得一个单点触摸可以接受更多的触摸操作。

我们可以通过PanResponder.create({options})方法,去创建手势响应事件。

手势事件一览:

    // 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
    onStartShouldSetPanResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;   
    // 如果 View 不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?
    onMoveShouldSetPanResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
    // 如果某个父 View 想要在触摸操作开始时阻止子组件成为响应者,那就应该处理onStartShouldSetResponderCapture事件并返回 true 值。
    onMoveShouldSetPanResponderCapture?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
    onStartShouldSetPanResponderCapture?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
    
    // 手势开始响应
    onPanResponderGrant?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。
    onPanResponderReject?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // View 现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里。
    onPanResponderStart?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // 手势移动
    onPanResponderMove?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // 手势释放
    onPanResponderRelease?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // 手势结束
    onPanResponderEnd?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    
    // 另一个容器已经成为了新的响应者,当前手势将被取消。
    onPanResponderTerminate?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
    // 当别的想成为响应者,是否放权
    onPanResponderTerminationRequest?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
    // 当前组件是否应该阻止原生组件成为JS响应者
    onShouldBlockNativeResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;

1、在使用手势之前,首先应赋予相应的容器为触摸事件响应者

可以通过onStartShouldSetResponderonMoveShouldSetResponder去判断触摸开始时或触摸移动时,是否成为响应者。

举个🌰:

this._panResponder = PanResponder.create({
  onStartShouldSetPanResponder: this.handleStartPanResponder,
  onMoverShouldSetPanResponder: this.handleSetPanResponder,
})
...
handleStartPanResponder = (e:GestureResponderEvent) => {
  // 如果传进来给组件的参数为disabled,则不响应
  if(this.props.disabled){
    return false;
  }
  return true;
};
handleSetPanResponder = (e:GestureResponderEvent, gesture:PanResponderGestureState)=> {
   const { disabled } = this.props;
   const { dx, dy } = gesture;
    if (disabled) {
      return false;
    }
  // 如果横向移动距离和纵向移动距离都小于4,则不响应
    if(Math.abs(dx)<4 && Math.abs(dy)<4){
      return false;
    }
}
...
// 加上手势属性
<View {...this.panResponder.panHandlers}>
  ...
</View>

第一个参数传入的是原生事件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 - 当前在屏幕上的有效触摸点的数量

熟悉以上属性,可以帮助我们更流畅地写出交互逻辑~

2、设置完响应者之后,就会开始尝试成为响应者

此时可以通过onResponderGrantonResponderReject两个方法设置刚开始响应触摸事件、拒绝响应触摸事件的相应事件

 onPanResponderGrant: (evt, gestureState) => {
        // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
        // gestureState.{x,y} 现在会被设置为0
},
onResponderReject: (evt, gestureState) => {
        // 现在已有响应者,不会响应
},

3、成为响应者后,容器就可以响应触摸事件

此时就可以给容器添加

  • View.props.onResponderMove: (evt) => {} - 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。

  • View.props.onResponderRelease: (evt) => {} - 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。

  • View.props.onResponderTerminationRequest: (evt) => true - 有其他组件请求接替响应者,当前的 View 是否“放权”?返回 true 的话则释放响应者权力。

  • View.props.onResponderTerminate: (evt) => {} - 响应者权力已经交出。这可能是由于其他 View 通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如 iOS 上的控制中心或是通知中心)。

    最后贴一个官方对于PanResponder的🌰:github.com/facebook/re…

    const React = require('react');
    const {PanResponder, StyleSheet, View} = require('react-native');
    const RNTesterPage = require('../../components/RNTesterPage');
    ​
    import type {
      PanResponderInstance,
      GestureState,
    } from 'react-native/Libraries/Interaction/PanResponder';
    import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes';
    ​
    type CircleStyles = {
      backgroundColor?: string,
      left?: number,
      top?: number,
      ...
    };
    ​
    const CIRCLE_SIZE = 80;
    ​
    type Props = $ReadOnly<{||}>;
    type State = {|
      left: number,
      top: number,
      pressed: boolean,
    |};
    ​
    class PanResponderExample extends React.Component<Props, State> {
      _previousLeft: number = 20;
      _previousTop: number = 84;
      _circleStyles: {|style: CircleStyles|} = {style: {}};
      circle: ?React.ElementRef<typeof View> = null;
    ​
      state: State = {
        left: 20,
        top: 84,
        pressed: false,
      };
    ​
      _handleStartShouldSetPanResponder = (
        event: PressEvent,
        gestureState: GestureState,
      ): boolean => {
        // Should we become active when the user presses down on the circle?
        return true;
      };
    ​
      _handleMoveShouldSetPanResponder = (
        event: PressEvent,
        gestureState: GestureState,
      ): boolean => {
        // Should we become active when the user moves a touch over the circle?
        return true;
      };
    ​
      _handlePanResponderGrant = (
        event: PressEvent,
        gestureState: GestureState,
      ) => {
        this.setState({
          pressed: true,
        });
      };
    ​
      _handlePanResponderMove = (event: PressEvent, gestureState: GestureState) => {
        this.setState({
          left: this._previousLeft + gestureState.dx,
          top: this._previousTop + gestureState.dy,
        });
      };
    ​
      _handlePanResponderEnd = (event: PressEvent, gestureState: GestureState) => {
        this.setState({
          pressed: false,
        });
        this._previousLeft += gestureState.dx;
        this._previousTop += gestureState.dy;
      };
    ​
      _panResponder: PanResponderInstance = PanResponder.create({
        onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
        onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
        onPanResponderGrant: this._handlePanResponderGrant,
        onPanResponderMove: this._handlePanResponderMove,
        onPanResponderRelease: this._handlePanResponderEnd,
        onPanResponderTerminate: this._handlePanResponderEnd,
      });
    ​
      render(): React.Node {
        return (
          <RNTesterPage
            noSpacer={true}
            noScroll={true}
            title="Basic gesture handling">
            <View style={styles.container}>
              <View
                ref={circle => {
                  this.circle = circle;
                }}
                style={[
                  styles.circle,
                  {
                    transform: [
                      {translateX: this.state.left},
                      {translateY: this.state.top},
                    ],
                    backgroundColor: this.state.pressed ? 'blue' : 'green',
                  },
                ]}
                {...this._panResponder.panHandlers}
              />
            </View>
          </RNTesterPage>
        );
      }
    }
    ​
    const styles = StyleSheet.create({
      circle: {
        width: CIRCLE_SIZE,
        height: CIRCLE_SIZE,
        backgroundColor: 'green',
        borderRadius: CIRCLE_SIZE / 2,
        position: 'absolute',
        left: 0,
        top: 0,
      },
      container: {
        flex: 1,
        height: 500,
      },
    });
    ​
    ​