问题概要
在小米手机(测试机为小米4LTE)上,对一个TextView/Button设置OnTouchListener,长按View抬起时,并没有收到ACTION_UP时间,而是收到了ACTION_CANCEL事件。
理论
查阅资料,发现如下理论:当控件收到前驱事件(什么叫前驱事件?一个从DOWN一直到UP的所有事件组合称为完整的手势,中间的任意一次事件对于下一个事件而言就是它的前驱事件)之后,后面的事件如果被父控件拦截,那么当前控件就会收到一个CANCEL事件,并且把这个事件会传递给它的子事件。(注意:这里如果在控件的onInterceptTouchEvent中拦截掉CANCEL事件是无效的,它仍然会把这个事件传给它的子控件)之后这个手势所有的事件将全部拦截,也就是说这个事件对于当前控件和它的子控件而言已经结束了。按照该理论,MUI在系统级将长按View的最后一个ACTION_UP事件拦截并消费掉,并发出ACTION_CANCEL事件,并将这个事件传递给设置了OnTouchListener的View上。
实践Demo
我们先不讨论项目中遇到的问题,先按照上述理论做了个Demo
如下:
private void testTouchMUI() {
tvTouch.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("zxg","event x:"+event.getX()+",event y:"+event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("zxg", "action down");
break;
case MotionEvent.ACTION_MOVE:
Log.i("zxg", "action move");
break;
case MotionEvent.ACTION_UP:
Log.i("zxg", "action up");
break;
case MotionEvent.ACTION_CANCEL:
Log.i("zxg", "action cancel");
break;
}
return true;
}
});
}
小米4 LTE 测试结果
点击快速抬起
10-23 09:35:45.468 11218-11218/com.saic.saic_ui I/zxg: event x:234.0,event y:98.0
action down
10-23 09:35:45.543 11218-11218/com.saic.saic_ui I/zxg: event x:234.0,event y:98.0
action up
可以看到收到UP事件,是一次完整的点击事件
按下后滑动并抬起
10-23 09:37:31.083 11218-11218/com.saic.saic_ui I/zxg: event x:423.0,event y:191.0
action down
10-23 09:37:31.111 11218-11218/com.saic.saic_ui I/zxg: event x:423.0,event y:191.0
action move
...
10-23 09:37:31.447 11218-11218/com.saic.saic_ui I/zxg: event x:285.66003,event y:178.0
action move
10-23 09:37:31.461 11218-11218/com.saic.saic_ui I/zxg: event x:285.66003,event y:178.0
action up
可以看到收到多次move事件后最终也收到了UP事件
长按抬起
10-23 09:39:30.423 11218-11218/com.saic.saic_ui I/zxg: event x:232.0,event y:148.0
action down
10-23 09:39:30.592 11218-11218/com.saic.saic_ui I/zxg: event x:232.0,event y:148.0
action move
10-23 09:39:31.244 11218-11218/com.saic.saic_ui I/zxg: event x:322.0,event y:1296.0
action cancel
奇怪的事件发生了,最终并没有收到UP事件,而是CANCEL事件
我们先来看下其他机型情况,再分析小米手机长按的log
三星S10测试结果
点击快速抬起
与小米4log一致,不赘述
按下后滑动并抬起
与小米4log一致,不赘述
长按抬起
10-23 09:46:06.722 31076-31076/com.saic.saic_ui I/zxg: event x:625.24023,event y:160.44531
action down
10-23 09:46:06.747 31076-31076/com.saic.saic_ui I/zxg: event x:624.22046,event y:161.00195
action move
10-23 09:46:06.763 31076-31076/com.saic.saic_ui I/zxg: event x:623.9219,event y:161.00195
action move
10-23 09:46:06.780 31076-31076/com.saic.saic_ui I/zxg: event x:623.13086,event y:161.5586
action move
10-23 09:46:06.813 31076-31076/com.saic.saic_ui I/zxg: event x:622.6035,event y:161.5586
action move
10-23 09:46:06.913 31076-31076/com.saic.saic_ui I/zxg: event x:622.33984,event y:162.67188
action move
10-23 09:46:06.930 31076-31076/com.saic.saic_ui I/zxg: event x:622.8672,event y:162.67188
action move
10-23 09:46:07.412 31076-31076/com.saic.saic_ui I/zxg: event x:622.8672,event y:162.67188
action up
可以看到长按后抬起最终收到了ACTION_UP事件。下面我们分析下这两份长按日志如下区别:
-
小米日志event事件的坐标值只保留了小数点后1位且在这个过程中未变化过。而三星手机精确度比较高保留到了小数点后4位。换句话说三星手机较为灵敏,可以更细微感知手指滑动。
-
小米手机最终的CANCEL事件event坐标值突然增大,与之前的move事件DOWM及MOVE事件差距较大,推测:DOWN及MOVE事件坐标是以为左上角为(0,0)点,而最终CANCEL事件坐标是以屏幕左上角作为(0,0)点。这种推测也佐证了是MUI系统级拦截UP事件,并发出CANCEL事件。
-
使用三星手机很难做到长按抬起,事件坐标点一直不变化,因为其灵敏度很高,所以假如三星手机长按抬起坐标点不变化是否也是收到CANCEL事件,我们就不得而知了。
结论:
对于小米手机,无论是灵敏度低导致或系统拦截UP事件导致,最终的结果是长按不起收不到UP事件,那么针对小米手机,就要在CANCEL事件中执行与UP事件一样的处理。
具体问题
自研IM长按录音抬起发送语音的功能,在小米4中,由于长按抬起,一直走CANCEL事件,导致一直取消录音,没有走到UP事件中发送语音的逻辑。针对该问题,我们做如下修改:
event?.action == MotionEvent.ACTION_CANCEL -> {
Log.i("zxg", "cancel ACTION_CANCEL")
var location = IntArray(2)
btn_speak.getLocationOnScreen(location)
if (RomUtils.isMiui()) {
//如果是小米手机UP一瞬间系统给到的event的坐标体系并不是以btn_speak左上角为(0,0)的坐标值
//而是以整个屏幕左上角为(0,0)的坐标值,原因:猜测MUI系统层面拦截UP事件,btn_speak作为子控件,会收到CANCEL事件
//参考:https://blog.csdn.net/bornonew/article/details/90897001
//参考:https://blog.csdn.net/qq_23934247/article/details/88711079
//所以此时我们要获取btn_speak相对整个屏幕的绝对坐标值并判断event事件的左边是否在控件内执行UP事件中的发送录音逻辑
Log.info(TAG,"speak view start x:"+location[0]+",speak view start y:"+location[1])
IMLog.info(TAG, "boundary x:" + (location[0] + btn_speak.width) + ",boundary y:" + (location[1] + btn_speak.height))
Log.info(TAG,"event x:"+event?.x+",event y"+event?.y)
if ((event?.x > location[0] && event?.x < location[0] + btn_speak.width) && (event?.y > location[1] && event?.y < location[1] + btn_speak.height)) {
IMLog.info(TAG, "mui adapte need recrod")
//录音逻辑
} else {
//取消录音逻辑
}
} else {
//取消录音逻辑
}
}
可以看到,我们并没有直接执行录音逻辑,而是要判断event事件坐标点是否落在录音按钮内,因为需要顾及其他功能逻辑,例如上滑取消录音的逻辑
由于最终的CANCEL event事件坐标点是以整个屏幕左上角为(0,0)点,所以我们需要通过getLocationOnScreen()获取录音按钮左上角的绝对坐标(以屏幕左上角为(0,0)点的坐标),并且根据按钮的宽高计算出录音按钮的坐标范围,以此作为标准判断event事件坐标点是否落在按钮内
目前该改动已通过小米4/小米4 note/小米9测试,其他品牌尝试了三星、vivo。还需要大量兼容性测试,重点测试小米其他型号手机