問題概要
在小米手機(測試機爲小米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。還需要大量兼容性測試,重點測試小米其他型號手機