react native的touch系列以及手势的事件真的是大有文章,涉及了很多原生事件以及JS事件拦截的很多机制。
这篇文章主要就是给大家在应用层面说明一些具体的深坑。
这个坑的发现是源于要做一个需求:点击长按—>(做点事情),手指滑动离开按钮(没有离开屏幕)则取消这个事情,移回来则继续这个事情(注意这里没有间断)
从简单来看,貌似可以用touch*系列组件的 onLongpress事件以及onpressin和onpressout事件,当然还有onpress事件。不熟悉的童鞋可以参考这篇文章:http://www.lcode.org/%E3%80%90react-native%E5%BC%80%E5%8F%91%E3%80%91react-native%E6%8E%A7%E4%BB%B6%E4%B9%8Btouchable%E7%B3%BB%E5%88%97%E7%BB%84%E4%BB%B6%E8%AF%A6%E8%A7%A318/
这是文章里给出的事件触发顺序,其实这里也是一个坑,但是作为入门,这篇文章也算对得起小白了。
咱们来看一下进阶的:https://gold.xitu.io/entry/55fa202960b28497519db23f
通过一个稍微严谨一点的测试就能非常简单的证明了上面链接里文章写孰对孰错:
press,pressIn,pressOut,longPress事件的触发条件和触发顺序:
1.快速点击,会按顺序触发pressin,pressout,press事件
2.如果同时绑定了pressIn, pressOut和press事件,那么当pressIn事件触发之后,如果用户的手指在绑定的组件中释放,那么接着会连续触发pressOut和press事件,此时的顺序是pressIn -> pressOut -> press。而如果用户的手指滑到了绑定组件之外才释放,那么此时将会不触发press事件,只会触发pressOut事件,此时的顺序是pressIn -> pressOut。后一种情况就是前面所说的中途取消,如果我们将回调函数绑定给press事件,那么后一种情况中回调函数并不会被触发,相当于"被取消"。
3.如果绑定了longPress事件,那么当用户的手指在当前组件抬起,会按顺序触发pressin,longpress,pressout,press事件;如果用户的手指没有在当前组件抬起,那么会按顺序触发pressin,longpress,pressout事件。
4.longPress事件的触发时间大概是在pressIn事件发生300-400ms之后。
看来两位大神的测试都不够严谨。(当然也许我的也是错的,大家还是要自己测一下,这个坑属于迷幻坑,拼人品的)
表面上看起来,这4个组件足够实现我开篇提到的那个需求了,控制好pressin,以及pressout,和press就能实现,但是PM又要求在手指离开组件的时候要有UI变化,额,这个.......要实时判断手指有没有离开组件,这个需求,依靠touch*系列组件估计是搞不定了,而上一篇讲到的JS手势事件拦截,也仅仅是能处理事件event,大家仔细看
目测都提供了这么详细的对象属性和事件数据,难道还不够用?文档最下面的这几个字引起了我的怀疑:
然后在开头写了这么一句:它提供了一个对触摸响应系统响应器的可预测的包装。对于每一个处理函数,它在原生事件之外提供了一个新的gestureState对象。很好奇的(其实是强迫症)我点了这个触摸响应系统,然后回到了刚刚的第一页....(一万个草泥马)。书读百遍,其义自现,我们得好好揣摩一下。可预测包装???(what?)我还是去翻翻英文文档吧,看看是不是翻译坑,3分钟后,我又回来了....(刚刚的草泥马应该现在飘)英文文档跟中文的完全就是两个版本,而且.....在这里英文版的更加烂(烂的彻底)!大家可以前去观摩一下:https://facebook.github.io/react-native/docs/handling-touches.html。不要奇怪为什么最后两个超链接点不开,或者是你点开了(科学上网)发现特么链接指向的怎么是FB的code主页。中文文档在这一刻终于显得那么的人性化和高大上~!
好吧,看看有没有大神有研究过这玩意,要是不小心掉坑了,还不一定能爬出来。然后我就看到了这个:另外需要注意,因为 RN 的异步通信和执行机制,前面描述的所有回调函数都是在 JS 线程中,并不是 Native 的 UI 线程,而 Native 平台的 Touch 事件都是在 UI 线程中。所以在 JS 中通过 Touch 或者手势实现动画,可能会延迟的问题。(http://www.race604.com/react-native-touch-event/ 最后一段)在这里,我灵感乍现!!!
原来这里就是使用了我在上一篇提到的JS层拦截事件机制,将事件在JS层拦截处理掉,不传回原生层,减少通信以及内存各种开销;(和原生通信只能使用异步这个坑我到现在都还没填上只能祈祷原生健健康康长命百岁)
恩,确实不错,本着排雷填坑的初衷,我很谨慎的把传回的参数都打印出来,并且测试了N次(N<100)参照文档的使用说明以及例子:
我将e,以及gestureState打印出来:
很枯燥的数据
{ dispatchConfig: { registrationName: 'onResponderMove' }, ----------------------------move事件触发的
I/ReactNativeJS(18892): dispatchMarker: '.r[1]{TOP_LEVEL}[0].$1.0.0.3.0.1.1.0.1.0',
I/ReactNativeJS(18892): nativeEvent: ---------------原生事件
I/ReactNativeJS(18892): { identifier: 0,
I/ReactNativeJS(18892): timeStamp: 174716835,
I/ReactNativeJS(18892): locationY: 1.0174967050552368,
I/ReactNativeJS(18892): locationX: 60.88374710083008,
I/ReactNativeJS(18892): pageY: 588.350830078125,
I/ReactNativeJS(18892): target: 1007,
I/ReactNativeJS(18892): pageX: 125.5504150390625,
I/ReactNativeJS(18892): changedTouches: [ [Circular] ],
I/ReactNativeJS(18892): touches: [ [Circular] ] },
I/ReactNativeJS(18892): type: undefined,
I/ReactNativeJS(18892): target: 1007,
I/ReactNativeJS(18892): currentTarget: 999,
I/ReactNativeJS(18892): eventPhase: undefined,
I/ReactNativeJS(18892): bubbles: undefined,
I/ReactNativeJS(18892): cancelable: undefined,
I/ReactNativeJS(18892): timeStamp: 174716835,
I/ReactNativeJS(18892): defaultPrevented: undefined,
I/ReactNativeJS(18892): isTrusted: undefined,
I/ReactNativeJS(18892): touchHistory:
I/ReactNativeJS(18892): { touchBank:
I/ReactNativeJS(18892): [ { touchActive: true,
I/ReactNativeJS(18892): startTimeStamp: 174716828,
I/ReactNativeJS(18892): startPageX: 125.5504150390625,
I/ReactNativeJS(18892): startPageY: 588.350830078125,
I/ReactNativeJS(18892): currentPageX: 125.5504150390625,
I/ReactNativeJS(18892): currentPageY: 588.350830078125,
I/ReactNativeJS(18892): currentTimeStamp: 174716835,
I/ReactNativeJS(18892): previousPageX: 125.5504150390625,
I/ReactNativeJS(18892): previousPageY: 588.350830078125,
I/ReactNativeJS(18892): previousTimeStamp: 174716828 } ],
I/ReactNativeJS(18892): numberActiveTouches: 1,
I/ReactNativeJS(18892): indexOfSingleActiveTouch: 0,
I/ReactNativeJS(18892): mostRecentTimeStamp: 174716835 },
I/ReactNativeJS(18892): isDefaultPrevented: [Function],
I/ReactNativeJS(18892): isPropagationStopped: [Function],
I/ReactNativeJS(18892): _dispatchListeners: [Function: onResponderMove],
I/ReactNativeJS(18892): _dispatchIDs: '.r[1]{TOP_LEVEL}[0].$1.0.0.3.0.1.1.0.1.0' }
I/ReactNativeJS(18892): { stateID: 0.2133135343901813,
I/ReactNativeJS(18892): moveX: 125.5504150390625,
I/ReactNativeJS(18892): moveY: 588.350830078125,
I/ReactNativeJS(18892): x0: 125.5504150390625,
I/ReactNativeJS(18892): y0: 588.350830078125,
I/ReactNativeJS(18892): dx: 0,
I/ReactNativeJS(18892): dy: 0,
I/ReactNativeJS(18892): vx: 0,
I/ReactNativeJS(18892): vy: 0,
I/ReactNativeJS(18892): numberActiveTouches: 1,
I/ReactNativeJS(18892): _accountsForMovesUpTo: 174716835 }
由于日志太多,贴在这里有凑字数的嫌疑。我就直说了吧!
对应一下说明:nativeEvent
changedTouches- 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
identifier- 触摸点的ID
locationX- 触摸点相对于父元素的横坐标 --------------------巨坑之一:任何时候只要按下了此数值就再也不会变化了
locationY- 触摸点相对于父元素的纵坐标 ------------------------------同上;其实就是说这个只是第一次触摸的点,并不是每次移动后的点
pageX- 触摸点相对于根元素的横坐标 -------------------这里正好相反,是每次移动后的点;
pageY- 触摸点相对于根元素的纵坐标 -----------------------同上
target- 触摸点所在的元素ID
timestamp- 触摸事件的时间戳,可用于移动速度的计算
touches- 当前屏幕上的所有触摸点的集合
一个gestureState对象有如下的字段:
stateID- 触摸状态的ID。在屏幕上有至少一个触摸点的情况下,这个ID会一直有效。
moveX- 最近一次移动时的屏幕横坐标 -----巨坑之二:此数值是跟move事件的响应频率有关,就是每次响应前后移动的坐标,有兴趣的童鞋可以详细测算一下
moveY- 最近一次移动时的屏幕纵坐标 --------------同上
x0- 当响应器产生时的屏幕坐标 ----------------------按下时候的触摸点相对于屏幕的x坐标
y0- 当响应器产生时的屏幕坐标 ----------------------按下时候的触摸点相对于屏幕的y坐标
dx- 从触摸操作开始时的累计横向路程 -------------------- 注意,这里的累积路程也遵从数学的4象限坐标系的正负取值,并不是绝对值的累加
dy- 从触摸操作开始时的累计纵向路程 -----------------------同上
vx- 当前的横向移动速度 ------------------------按XY轴数学的4个象限有正负速度,原点为按下的那一点
vy- 当前的纵向移动速度 ------------------------同上
numberActiveTouches- 当前在屏幕上的有效触摸点的数量
呵呵,坑不是一般多。多测几次,我果然还是很明智啊!
现在可以来考虑如何实现了。直接用gestureState拦截到的对象属性。首先我们无法直接拿到触摸点相对于组件的坐标系坐标。So,只能从屏幕坐标来判断。那么首先就要获取到组件在屏幕上的坐标,这里可以用onlayout(x,y,width,height)简直就是为手势专门打造的,4个属性,少一个都不行。接着用native事件对象取到按下那一点相对于在组件内部的坐标(locationX,locationY).然后配合gestureState对象的dx,dy判断手指是否有离开组件。至此大功告成!(呵呵,又特么想多了,哪有那么容易)
根据手势的不同,做出UI的变化.......UI变化,UI变化(重要的事情说3遍!)特么的UI变化就要setState~!这个巨坑,栽了吧!setState就是重刷啊!别相信什么diff算法,重刷就是重刷!就跟网页的刷新一样。如果重刷范围波及到了你的触摸响应组件,呵呵了,后果你可以自己测试一下,嘿嘿
那怎么办呢?这个坑,绕不过去啊!那就填平他,这是一个个性坑,根据你要改变UI的范围不同要 用马$%^主义哲学的方法论#$#%$%^&^*&^*^*^*^& 具体问题具体分析:如果你要重刷的部位跟事件响应组件没关系,那就单独封装,在组件内部刷。如果很不幸你的响应组件在波及范围内,那就把响应组件封装一层,内部刷,外面的父组件以及其他组件就一个一个的控制好刷吧,这个真没有什么通用的办法,如果有,请速速告诉我。
已经凌晨5点半了,今天就说到这吧~~大家有什么更好的填坑之法,请务必告诉我,我已经被坑的生无所恋了。