- 思維導圖
- View事件分發機制
- 點擊事件傳遞規則
- 事件分發的原始碼決議
- View的滑動沖突
- 滑動沖突的解決方式
- 參看文章
思維導圖
View事件分發機制
點擊事件傳遞規則
所謂點擊事件的事件分發,就是對MotionEvent事件的分發程序,傳遞給某一個View.
在事件傳遞中有三個方法是貫穿始終的
dispatchTouchEvent():進行事件的分發 如果事件能傳遞到View那么此方法一定會被呼叫,回傳值受當前View的onTouchEvent()和下級View的dispatchTouchEvent()的影響. 表示是否消耗當前事件.onInterceptTouchEvent(): 判斷是否攔截事件,如果當前View攔截了某個事件,那么在同一個事件序列中, 此方法不會被再次呼叫, 回傳結果表示是否攔截當前事件.onTouchEvent(): 在dispatchTouchEvent中被呼叫. 用來處理點擊事件, 回傳結果表示是否消耗當前事件,如果不消耗, 則在同一個事件序列中, 當前View無法再次接收到事件.
如果把原始碼中的各種判斷去掉, 只留最核心的代碼, 那么就如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false; //表示這個事件最終的處理結果
if (onInterceptTouchEvent(event)){
//事件被攔截自己處理
consume = onTouchEvent(event);
}else{
//事件被分發到子view的dispatchTouchEvent()中
consume = child.dispatchTouchEvent(event);
}
return consume;
}
OnTouchListener() , onTouchEvent() , OnClickListener() 的優先級
上面說了普遍情況下的事件分發. 如果這個View同時還添加了OnTouchListener和OnClickListener. 此時的優先級如下:
OnTouchListener –> onTouchEvent –> OnClickListener
而onTouchEvent()能否被最終呼叫取決于設定了OnTouchListener()中的onTouch()的回傳值, 如果onTouch()回傳的結果是false,那么onTouchEvent()會被呼叫. 如果回傳true那么onTouchEvent()不會被呼叫.
而最后被呼叫的OnClickListener()方法是在onTouchEvent()被呼叫的. 所以如果onTouchEvent()方法如果執行, 那么對應的添加的onClickLisener()才會被呼叫. 所以如果在OnTouchListener()中的onTouch()回傳true那么onTouchEvent()不會被呼叫,內部呼叫OnClickListener也就更無法被呼叫.
一個事件的傳遞程序遵循如下
Activity -> Window ->decor view->ViewGroup-> View
如果事件一直不攔截,傳遞到了最里層的View而最里層的View的onTouchEvent()也回傳false不消費, 那么事件就會向上級的onTouchEvent()傳遞,如果還回傳false就依次傳遞.
對于事件機制的規則:
- 事件序列是指按下到抬起之間發生的一系列事件.
- 默認一個事件序列只能被一個View攔截并消耗. (例外:采用非常規,在onTouchEvent強行傳遞給其他View. 不推薦)
- 如果View決定攔截,那么這個事件序列只能由它自己處理. 并且它的
onInterceptTouchEvent()不會再被呼叫 - 如果View不在
ACTION_DOWN事件時回傳true, 那么同一個事件序列都不會再交給它來處理.并且事件會重新傳遞到父元素的onTouchEvent()再次呼叫方法. - 如果View不消耗除
ACTION_DOWN以外的事件,那么這個點擊事件會消失,而父元素的onTouchEvent()不會被呼叫,并且當前View可以持續收到后續的事件,最終這些消失的事件會傳遞到activity處理. ViewGroup默認不攔截任何事件, 原始碼中ViewGroup的onInterceptTouchEvent()默認回傳falseView沒有onInterceptTouchEvent(), 因為它沒有子View,所以直接呼叫onTouchEvent()View的onTouchEvent默認都會消耗事件回傳true. 除非它不可點擊的(需要clickable,longClickable同時為false). View的longClickable默認都為false. 而clickable需要區分控制元件, 如Button默認為true,TextView默認為false.View的enable屬性不影響onTouchEvent的默認回傳值, 哪怕一個View是disable狀態. 只要它的clickable或者longClickable有一個為true. 那么它的onTouchEvent()就回傳true.onClick會發生的前提是當前View為可點擊, 并且他收到了down和up事件.- 事件傳遞的程序是由外向內的. 通過
requestDisallowInterceptTouchEvent()可以在子元素中干預父元素的事件分發程序,但是ACTION_DOWN事件除外.
事件分發的原始碼決議
1.Activity對點擊事件的分發程序
事件的起始Activity的dispatchTouchEvent()進行分發,具體的作業交由內部的Window來完成. window會將事件傳遞給decor view. 而decor view一般是當前界面的底層容器(平常setContentView中傳遞的布局),可通過Activity.getWindow.getDecorView()獲得.
window是怎樣將事件傳遞給ViewGroup的? 首先Window類為一個抽象類,而類中的呼叫的分發方法也為抽象方法. 所以需要找到實作類. Window的唯一實作類PhoneWindow. 這個類會在被實體化的時候會被重構.
PhoneWindow#superDispatchTouchEvent(ev)方法中將事件傳遞給了DecorView.
decorView就是掛載我們的layout布局的頂級View,繼承FrameLayout.
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
這個方法可以獲取到Activity所設定的View. 所以之間的關系很清楚了. 事件先交給最頂級的DecorView然后交由我們設定的View.
2.ViewGroup對事件的處理
在父元素中判斷子元素是否能接收點擊事件的主要由兩個因素衡量: 子元素是否在播放影片和點擊事件的坐標是否落在子元素的區域內.dispatchTransformedTouchEvent()實際就是呼叫子View的dispatchTouchEvent. 而在事件從孩子到父元素(子View在onTouchEvent回傳false). 其實也是呼叫了dispatchTransformedTouchEvent(). 區別就在于向內傳遞引數3是傳入的不是空值, 向外傳傳入的是null.看下面代碼:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
3.View對事件的處理
- View對事件的判斷首先是檢測是否有
onTouchListener如果有那么就呼叫其中的onTouch()方法. - 然后執行
onTouchEvent()這里有個判斷條件,如果在onTouchListener()回傳true. 那么if中的條件判斷第一個就不會成立,也就不會再呼叫onTouchEvent()方法.如下:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
- 然后在
onTouchEvent()中首先是查看View處理不可用狀態. 這里需要注意一下, 雖然View不可用但是如果點擊標記或者長按點擊標記都是true. 那么事件也會被消費.如下
|
if ((viewFlags & ENABLED_MASK) == DISABLED) {
.....
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
- 如果View設定有代理, 那么還會執行
TouchDelegate的onTouchEvent() - 然后對點擊狀態進行處理.首先如果點擊和長按有一個為true那么就會消費事件. 即
onTouchEvent()回傳true. 然后在ACTION_UP中會觸發performClick()如果設定了onClickListener()那么就會在此處進行判斷并呼叫onClick(). - 上面一直說的LONG_CLICKABLE與CLICKABLE. 長按標記默認為false. 點擊標記和View是否是可點擊View有關系. 如button可點擊默認就是true. 否者反之. 在使用時可以通過
setClickable和setLongClickable對View的這兩個標記進行修改. 這里還要注意還有一種賦值方式. 如果設定了setOnClickListener()或者setOnLongClickListener()監聽的話那么會自動將其對應的屬性置為true.
View的滑動沖突
滑動沖突的解決方式
外部攔截法
是指點擊事件都是需要先經過父容器的攔截處理, 如果父容器需要此事件就攔截,不需要就下放. 外部攔截需要重寫父容器的onInterceptTouchEvent方法
簡述一下: 如果使用這樣攔截法. 那么首先
ACTION_DOWN這個事件,父容器必須回傳false, 即不攔截ACTION_DOWN事件, 因為一旦父容器攔截了這個事件, 那么后續的ACTION_MOVE,ACTION_UP事件都會交由父容器來處理了. 這個時候這個事件序列剩余部分無法傳遞給子元素了.ACTION_MOVE這個事件,就可以根據實際的需求來決定是否需要攔截. 如果需要攔截就回傳true.否則false.ACTION_UP這個事件必須回傳false, 因為ACTION_UP事件本身沒有太多意義.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if("父容器的點擊事件"){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = x;
return intercepted;
}
內部攔截法
是指父容器不攔截任何事件, 所有的事件都需要傳遞給子元素, 如果子元素需要此事件就直接消費. 否則就交由父容器進行處理, 由于這種方法和Android中的事件分發機制不一致, 需要配合requestDisallowInterceptTouchEvent()方式才能正常作業. 需要重寫子元素的dispatchTouchEvent
這種攔截法的使用規則:
子View中的dispatchTouchEvent()進行復寫.
ACTION_DOWN事件中: 讓父容器拒絕攔截所有事件, 呼叫parent.requestDisallowInterceptTouchEvent(true)ACTION_MOVE事件中: 進行條件的攔截判斷, 如果在某一種場景需要攔截,那么就呼叫方法允許父容器攔截事件.return時, 呼叫super.dispatchTouchEvent(event)
父容器的onInterceptTouchEvent()進行ACTION_DOWN回傳false, 其余都是回傳true的復寫.
說明一點, 為什么父容器不連Action_down一并的用true復寫. 因為ACTION_DOWN這個事件是不受INTERCEPT_FLAG這個標記影響的的, 就是不管攔截標記是否是何值, 按下事件必然會執行, 所以如果這里回傳true, 那么就代表著, 這個事件序列的后續部分將由父容器進行處理, 而子容器無法收到這個事件.
@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:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if("父容器的點擊事件"){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
案例參考:SwipeRefreshLayout+RecyclerView滑動沖突解決
參看文章
《Android 開發藝術探索》書集
《Android 開發藝術探索》 03-View的事件體系
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/1311.html
標籤:其他
