探索 pointerIndex out of range

最近 App 里报了一个奇怪的 Exception。为什么说奇怪呢,因为它发生的概率不是很大,但每天总有那么几个。这种非必现的问题,解决起来最是麻烦。查了下代码,发现是 event.getY() 报错,我刚看到的反应是:纳尼,这里都能报错?网上查找了下解决方案,大致都是在 onTouchEvent 里面或外面包一层 try…catch。能解决问题,可惜不够优雅。 于是花了些时间研究下。

1
2
3
4
5
6
7
java.lang.IllegalArgumentException: pointerIndex out of range
at android.view.MotionEvent.nativeGetAxisValue(Native Method)
at android.view.MotionEvent.getY(MotionEvent.java:1994)
at com.kay.example.DemoView.onTouchEvent(HomeView.java:184)
at android.view.View.dispatchTouchEvent(View.java:7714)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2210)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1945)

探索

这种非必现的问题,第一步要找到能重现问题的场景。于是我就拼命地戳那个报错的 View,在不懈努力之下,还真重现了几次。异常的栈跟上面是一致的,看来 event.getY() 真能出错。
如果 event.getY() 这种基础的方法都能出错,那么系统的控件是如何防止这个错误的呢。
查看了 NestedScrollView 的源码,发现它的 onTouchEvent 的处理果然是有门道的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// NestedScrollView#onTouchEvent
// ...
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
// ...
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
float y = event.getY(activePointerIndex);
// ...

在报错的 View 中,是直接通过 event.getY() 来获取到纵坐标的值。而 NestedScrollView 先保存了原先 MotionEvent 的 PointerId,再通过 PointerId 查找到 Index,并判断是否有效,最终通过 event.getY(activePointerIndex) 获取纵坐标的值。
查看源码发现,event.getY() 相当于 event.getY(0),也就是说两者的差别在于对 Index 的获取。那么 Index 和 Id 两者有什么区别呢?

Index vs ID

以下这段来自 Making Sense of Multitouch

At a higher level, touchscreen data from a snapshot in time may not be immediately useful since touch gestures involve motion over time spanning many motion events. A pointer index does not necessarily match up across complex events, it only indicates the data’s position within the MotionEvent. However this is not work that your app has to do itself. Each pointer also has an ID mapping that stays persistent across touch events. You can retrieve this ID for each pointer using MotionEvent.getPointerId(index) and find an index for a pointer ID using MotionEvent.findPointerIndex(id).

简单的说,Index 只是表示存储在 MotionEvent 中数据的位置,在事件中不一定保持一致。而 ID 在 Touch 事件中是保持一致的。因此我们需要先保存 PointerId 然后再通过它来找到对应的 Index 来获取相应的坐标数据。

看来得多看看系统源码,能少爬多少坑。

参考