RN手势响应系统总结

响应者的生命周期

生命周期方法列表

View.props.onStartShouldSetResponderCapture: (evt) => true 
View.props.onMoveShouldSetResponderCapture: (evt) => true
View.props.onStartShouldSetResponder: (evt) => true
View.props.onMoveShouldSetResponder: (evt) => true
View.props.onResponderGrant: (evt) => {}
View.props.onResponderReject: (evt) => {}
View.props.onResponderMove: (evt) => {}
View.props.onResponderRelease: (evt) => {}
View.props.onResponderTerminationRequest: (evt) => true
View.props.onResponderTerminate: (evt) => {}

evt是一个合成事件,它包含以下结构:

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

生命周期方法详解

捕获 ShouldSet 事件处理

  • View.props.onStartShouldSetResponderCapture: (evt) => true
  • View.props.onMoveShouldSetResponderCapture: (evt) => true

“是否愿意成为响应者”系列方法:onStartShouldSetResponder与onMoveShouldSetResponder是以冒泡的形式调用的,即嵌套最深的节点最先调用。这意味着当多个 View 同时在*ShouldSetResponder中返回 true 时,最底层的 View 将优先“夺权”。在多数情况下这并没有什么问题,因为这样可以确保所有控件和按钮是可用的。

但是有些时候,某个父 View 会希望能先成为响应者。我们可以利用“捕获期”来解决这一需求。响应系统在从最底层的组件开始冒泡之前,会首先执行一个“捕获期”,在此期间会触发on*ShouldSetResponderCapture系列事件。因此,如果某个父 View 想要在触摸操作开始时阻止子组件成为响应者,那就应该处理onStartShouldSetResponderCapture事件并返回 true 值。

是否愿意成为响应者

  • View.props.onStartShouldSetResponder: (evt) => true 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
  • View.props.onMoveShouldSetResponder: (evt) => true 如果 View 不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?

尝试成为响应者

  • View.props.onResponderGrant: (evt) => {} View 现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里
  • View.props.onResponderReject: (evt) => {} 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。

开始响应触摸事件

  • View.props.onResponderMove: (evt) => {} 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。
  • View.props.onResponderRelease: (evt) => {} 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。
  • View.props.onResponderTerminationRequest: (evt) => true 有其他组件请求接替响应者,当前的 View 是否“放权”?返回 true 的话则释放响应者权力。
  • View.props.onResponderTerminate: (evt) => {} 响应者权力已经交出。这可能是由于其他 View 通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如 iOS 上的控制中心或是通知中心)。

封装手势

可以认为是对手势响应生命周期方法的模板封装,与gesture responder system 比起来,封装手势方法的抽象程度更高,使用起来也更加方便

PanResponder

响应事件

事件方法列表
  1. onMoveShouldSetPanResponder: (evt, gestureState) => {...}
  2. onMoveShouldSetPanResponderCapture: (evt, gestureState) => {...}
  3. onStartShouldSetPanResponder: (evt, gestureState) => {...}
  4. onStartShouldSetPanResponderCapture: (evt, gestureState) => {...}
  5. onPanResponderReject: (evt, gestureState) => {...}
  6. onPanResponderGrant: (evt, gestureState) => {...}
  7. onPanResponderStart: (evt, gestureState) => {...}
  8. onPanResponderEnd: (evt, gestureState) => {...}
  9. onPanResponderRelease: (evt, gestureState) => {...} //用户手指离开屏幕时,调用该方法
  10. onPanResponderMove: (evt, gestureState) => {...} //用户滑动手指时,调用该方法
  11. onPanResponderTerminate: (evt, gestureState) => {...}
  12. onPanResponderTerminationRequest: (evt, gestureState) => {...}
  13. onShouldBlockNativeResponder: (evt, gestureState) => {...}
说明:
  • 相比gesture responder syste的响应事件方法多了一个gestureState对象参数。

    例如:onPanResponderMove: (event, gestureState) => {}

  • 一个gestureState对象有如下的字段:

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

    • evt
      • 获取触摸的位置在被响应的 View 中的相对坐标
      • evt.nativeEvent.locationX 和 evt.nativeEvent.locationY(这个方法很实用)
    • gestureState
      • dx/dy:手势进行到现在的横向/纵向相对位移
      • vx/vy:此刻的横向/纵向速度
      • numberActiveTouches:responder上的触摸的个数

基本用法

  componentWillMount: function() {
    this._panResponder = PanResponder.create({
      // 要求成为响应者:
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

      onPanResponderGrant: (evt, gestureState) => {
        // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!

        // gestureState.{x,y} 现在会被设置为0
      },
      onPanResponderMove: (evt, gestureState) => {
        // 最近一次的移动距离为gestureState.move{X,Y}

        // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
        // 一般来说这意味着一个手势操作已经成功完成。
      },
      onPanResponderTerminate: (evt, gestureState) => {
        // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
        // 默认返回true。目前暂时只支持android。
        return true;
      },
    });
  },

  render: function() {
    return (
      <View {...this._panResponder.panHandlers} />
    );
  },
示例:
import {
    View,
    StyleSheet,
    PanResponder
} from 'react-native';

class QQAndGameHome extends PureComponent {
    static contextTypes = {
        router: PropTypes.object,
        store: PropTypes.object
    }

    constructor(props) {
        super(props);
        this.state = {
            top: JDDevice.getRpx(100),
            left: JDDevice.getRpx(100),
            bg: 'gray',
        };

        this.panResponder = PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onMoveShouldSetPanResponderCapture: () => true,
            onPanResponderGrant: () => {
                this._top = this.state.top;
                this._left = this.state.left;
                this.setState({bg: 'red'});
            },
            onPanResponderMove: (evt, gs) => {
                console.log(gs.dx+' '+gs.dy);
                this.setState({
                    top: this._top+gs.dy,
                    left: this._left+gs.dx
                });
            },

            onPanResponderRelease: (evt, gs)=>{
                this.setState({
                    bg: 'gray',
                    top: this._top+gs.dy,
                    left: this._left+gs.dx
                })
            }

        });
    }
    
    render() {
        return(
            <View
                {...this.panResponder.panHandlers}
                style={[myStyles.rect,{
                    "backgroundColor": this.state.bg,
                    "top": this.state.top,
                    "left": this.state.left
                    }]}
            >
            ...
            </View>
        )
    }
    
}


const myStyles = StyleSheet.create({
    rect: {
        position: 'absolute',
        width: JDDevice.getRpx(100),
        height: JDDevice.getRpx(100),
        borderColor: 'black'
    }
});

TouchableHighlight 与 Touchable 系列组件

响应系统用起来可能比较复杂。所以我们提供了一个抽象的Touchable实现,用来做“可触控”的组件。

TouchableHighlight

  • 本组件用于封装视图,使其可以正确响应触摸操作
  • 按下时自带透明度降低蒙层颜色变化反馈等效果
  • 底层实现上,实际会创建一个新的视图到视图层级中
  • TouchableHighlight只支持一个子节点,(不能没有子节点也不能多于一个),如果你希望包含多个子组件,可以用一个View来包装它们
  • hitSlop:这一属性定义了按钮的外延范围。这一范围也会使pressRetentionOffset变得更大。 注意: 触摸范围不会超过父视图的边界,也不会影响原先和本组件层叠的视图(保留原先的触摸优先级)。
  • onLayout:当加载或者布局改变的时候被调用,参数为:{nativeEvent: {layout: {x, y, width, height}}}
  • onLongPressonPress

TouchableOpacity

  • 本组件用于封装视图,使其可以正确响应触摸操作
  • 按下时自带透明度降低效果
  • 不透明度的变化是通过把子元素封装在一个Animated.View中来实现的,这个动画视图会被添加到视图层级中,少数情况下有可能会影响到布局
  • 此组件与TouchableHighlight的区别在于并没有额外的颜色变化,更适于一般场景

TouchableWithoutFeedback

  • TouchableHighlightTouchableOpacity的区别是按下时没有任何视觉上的反馈

TouchableNativeFeedback

  • 本组件用于封装视图,使其可以正确响应触摸操作
  • 仅限Android平台
  • 在Android设备上,这个组件利用原生状态来渲染触摸的反馈。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容