本文主要講述:
- 自己對View事件機制的一些理解
- 在項目中遇到的一些坑,解決方案
- 收集了一些View的事件機制問題
事件的分發原理圖
對於一個root viewgroup來說,如果接受了一個點擊事件,那麼首先會調用他的dispatchTouchEvent方法。
如果這個viewgroup的onInterceptTouchEvent 返回true,那就代表要攔截這個事件。接下來這個事件就
給viewgroup自己處理了,從而viewgroup的onTouchEvent方法就會被調用。如果如果這個viewgroup的onInterceptTouchEvent
返回false就代表我不攔截這個事件,然後就把這個事件傳遞給自己的子元素,然後子元素的dispatchTouchEvent
就會被調用,就是這樣一個循環直到 事件被處理。
圖:
完整事件流程:
dispatchTouchEvent(true) -> onInterceptTouchEvent(true) -> onTouchEvent(true) - 事件結束
重要的事情說一遍:
也就是說在任何View或者ViewGrop中只要它想消費Touch事件,那就onInterceptTouchEvent(true),這樣它就不會把
事件傳下去給孩子view了,自己消費.
api 描述:
dispatchTouchEvent 分發事件
return false; //表示分發,默認false;
return true; // 表示不分發;onInterceptTouchEvent 攔截事件
當dispatchTouchEvent 確認分發,會啓動攔截事件;
return false; //表示不攔截,默認false;
return true; // 表示攔截;
注意:攔截是相當於它的孩子(也就是說不會攔截自己,如果攔截,則TouchEvent會傳到他自己,而它孩子就接收不)
不攔截會繼續往他的孩子遞歸是否onInterceptTouchEvent ;
- onTouchEvent 觸摸事件
return false; //表示不消費,默認false;
return true; // 表示消費;
當onInterceptTouchEvent 確認攔截,會問自己是否要消費TouchEvent,
如果攔截了又不消費則,Touch結束;
- invalidate 重新繪製
讓整個view失效,這樣view會被重新調用, 配合onDraw()使用;
下面是調用流程:
當invalidate時會重新調用draw方法,
draw會調用onDraw,而在draw內還會調用computeScroll(),
此時如果想讓computeScroll()循環被調用可以在computeScroll()內自己調用postInvaildate()重新繪製.
computeScroll() 源碼是空實現,具體實現由自己來寫
常見問題
1.view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者優先級如何?
答:
onTouchListener優先級最高,如果onTouch方法返回 false ,那onTouchEvent就被調用了,返回true 就不會被調用。至於onClick 優先級最低。2.點擊事件的傳遞順序如何?
答:
Activity-Window-View。從上到下依次傳遞,當然瞭如果你最低的那個view onTouchEvent返回false 那就說明他不想處理 那就再往上拋,都不處理的話
最終就還是讓Activity自己處理了。舉個例子,pm下發一個任務給leader,leader自己不做 給架構師a,小a也不做 給程序員b,b如果做了那就結束了這個任務。
b如果發現自己搞不定,那就找a做,a要是也搞不定 就會不斷向上發起請求,最終可能還是pm做。
//activity的dispatchTouchEvent 方法 一開始就是交給window去處理的
//win的superDispatchTouchEvent 返回true 那就直接結束了 這個函數了。返回false就意味
//這事件沒人處理,最終還是給activity的onTouchEvent 自己處理 這裏的getwindow 其實就是phonewindow
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//來看phonewindow的這個函數 直接把事件傳遞給了mDecor
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//devorview就是 我們的rootview了 就是那個framelayout 我們的setContentView裏面傳遞的那個layout
//就是這個decorview的 子view了
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
3.enable是否影響view的onTouchEvent返回值?
答:
不影響,只要clickable和longClickable有一個爲真,那麼onTouchEvent就返回true。4.滑動衝突問題如何解決 思路是什麼?
答:
讓誰消費滑動:
要解決滑動衝突 其實最主要的就是有一個核心思想。你到底想在一個事件序列中,讓哪個view 來響應你的滑動?比如 從上到下滑,是哪個view來處理這個事件,從左到右呢?
攔截內外滑動:
用業務需求 來想明白以後 剩下的 其實就很好做了。核心的方法 就是2個 外部攔截也就是父親攔截,另外就是內部攔截,也就是子view攔截法。 學會這2種 基本上所有的滑動衝突.
都是這2種的變種,而且核心代碼思想都一樣。外部攔截法:思路就是重寫父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因爲這和android自身的事件處理機制 邏輯是一模一樣的
父容器示例代碼:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
//down事件肯定不能攔截 攔截了後面的就收不到了
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (你的業務需求) {
//如果確定攔截了 就去自己的onTouchEvent裏 處理攔截之後的操作和效果 即可了
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//up事件 我們一般都是返回false的 一般父容器都不會攔截他。 因爲up是事件的最後一步。這裏返回true也沒啥意義
//唯一的意義就是因爲 父元素 up被攔截。導致子元素 收不到up事件,那子元素 就肯定沒有onClick事件觸發了,這裏的
//小細節 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
}
內部攔截法:內部攔截法稍微複雜一點,就是事件到來的時候,父容器不管,讓子元素自己來決定是否處理。如果消耗了 就最好,沒消耗 自然就轉給父容器處理了。
子元素代碼:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (如果父容器需要這個點擊事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}//否則的話 就交給自己本身view的onTouchEvent自動處理了
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
PS: 父親容器代碼也要修改一下,其實就是保證父親別攔截down:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
案列: 分發的例子
下面紅色框區域的結構是ScrollView, 它的孩子是一些TextView;
分析:
1.當點擊它任意一個孩子(TextView)時,如果ScrollView不進行onInterceptTouchEvent ,則它就不可以在菜單上進行左右滑動;
2.但是如果攔截了全部,則它的孩子又會消費不了TouchEvent;
解決方法:
只有左右移動的時候進行攔截,這樣父親就擁有了TouchEvent,可在菜單上繼續左右滑動,
而上下移動或靜止的時候就不攔截,這樣孩子又有了TouchEvent,那麼孩子就可以點擊了;
實例代碼:
/**
* 當滑動的時候,需要攔截TouchEvent時間,讓scrollView消化,否則會分發到孩子去;
* 當不滑動的停止的時候,不攔截,則會分發到孩子去,也就是TexView;
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
// 只有水平滑動時才攔截touch
case MotionEvent.ACTION_DOWN:
startX = (int) (ev.getRawX() + 0.5f);
startY = (int) (ev.getRawY() + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
int newX = (int) (ev.getRawX() + 0.5f);
int newY = (int) (ev.getRawY() + 0.5f);
int dx = Math.abs(startX - newX);
int dy = Math.abs(startY - newY);
if (dx > dy) {
// 水平滑動,只有水平滑動纔會攔截事件
return true;
}
startX = (int) ev.getRawX();// 初始化當前位置
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}