View的事件分發(fā)機制
事件分發(fā)主要由三個方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)
三個方法的關系:
<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
</pre>
對于一個根ViewGroup來說,點擊事件產(chǎn)生后帐偎,首先會傳遞給他,這時他dispatchTouchEvent就會被調(diào)用碾褂,如果這個ViewGroup的onInterceptTouchEvent的返回值為true表示它要攔截當前事件雀瓢,事件就會交給這個ViewGroup的onTouchEvent處理,如果onInterceptTouchEvent的返回值為false表示不攔截當前事件契邀,當前事件會繼續(xù)傳遞給他的子元素偶房,子元素的dispatchTouchEvent會被調(diào)用趁曼,如此反復直到事件被最終處理。
當一個View需要處理事件時棕洋,如果設置了OnTouchListener,那么OnTouchListener中的OnTouchEvent會被調(diào)用挡闰,如果返回true,那么OnTouchEvent方法不會被調(diào)用掰盘,否則會被調(diào)用摄悯。如果當前設置的有OnclickListener,它的onClick方法在onTouchEvent里被調(diào)用。
當一個點擊事件產(chǎn)生后愧捕,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity射众,在傳遞給Window,最后Window傳遞給頂級的View晃财,頂級的Viw接收到事件后按照事件分發(fā)的機制去分發(fā)事件。
事件分發(fā)的結論
- 同一個事件序列是從手指接觸屏幕的那一刻起典蜕,到手指離開屏幕的那一刻結束断盛,在這個過程中產(chǎn)生的一些列的事件,這個事件以down事件開始愉舔,中間含有數(shù)量不定的move事件钢猛,最終以up事件結束。
- 正常情況下轩缤,一個事件序列知只能被一個View攔截且消耗
- 一旦一個View攔截了某事件命迈,那么同一個事件序列的所有事件都會直接交給他處理,因此同一個事件序列的事件不能分別由兩個View同時處理火的,但是通過特殊手段可以做到壶愤,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
- 某個View一旦開始處理事件馏鹤,如果它不消耗ACTION_DOWN事件(onTouchEvent 返回了false)征椒,那么同一事件序列中的其他事件都不會再交給他處理,會調(diào)用父元素的onTouchEvent
- 如果View不消費除ACTION_DOWN以外的其他事件湃累,那么這個點擊事件會消失勃救,此時父元素的onTouchEvent并不會被調(diào)用碍讨,并且當前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點擊事件會傳遞給Activity處理 蒙秒?勃黍?
- ViewGroup默認不攔截任何事件。Android源碼中的ViewGroup的onInterceptTouchEvent默認返回false
- View沒有onInterceptTouchEvent方法晕讲,一旦有點擊事件傳遞給它覆获,那么它的onTouchEvent方法就會被調(diào)用
- View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable 和 longClickable 同時為false)益兄,clickable屬性要分情況锻梳,比如Botton的clickable屬性默認為true,而Textview的clickable屬性默認為false净捅。
- View的enable屬性不影響onTouchEvent的默認返回值疑枯。哪怕一個View是disable的,只要它的clickable或者longClickable有一個為true蛔六,那么它的onTouchEvent就返回true荆永。
- onClick發(fā)生的前提是當前View是可點擊的,并且它收到了down和up事件国章。
- 事件傳遞過程是由外向內(nèi)的具钥,事件總是先傳遞給父元素,然后再由父元素傳遞給子元素液兽。子元素可以通過requestDisallowInterceptTouchEvent方法干預父元素的分發(fā)過程骂删。
事件分發(fā)的源碼解析
Activity對事件的分發(fā)過程
當一個點擊操作發(fā)生時,事件最先傳遞給Activity四啰,由Activity的dispatchTouchEvent進行事件派發(fā)宁玫,具體工作由Activity內(nèi)部的Window來完成。
<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
</pre>
事件交給Activity所屬的Window進行分發(fā)柑晒,如果返回了true欧瘪,整個事件循環(huán)就結束了,返回false意味事件沒人處理匙赞,所有的View的onTouchEvent都返回了false佛掖,那么Activity的onTouchEvent就會被調(diào)用。
getWindow().superDispatchTouchEvent(ev),這里的Window是SDK里window的唯一實現(xiàn)類PhoneWindow
<pre>
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
</pre>
這里的邏輯非常的清晰涌庭,PhoneWindow直接將事件傳遞給了DecorView芥被,隨后DecorView進行事件的分發(fā)。
ViewGroup對事件的分發(fā)過程
ViewGroup onInterceptTouchEvent返回true坐榆,事件由自己處理撕彤,如果ViewGroup setOnTouchListener則onTouch會被調(diào)用,否則onToucheEvent會被調(diào)用,簡言之羹铅,如果兩個都提供的話onTouch會屏蔽onTouchEvent蚀狰,如果setOnClickListener了,在onTouchEvent中职员,onClick會被調(diào)用麻蹋?這里作者說的有些問題,onTouch返回為true才會屏蔽焊切。如果ViewGroup不攔截事件扮授,則事件會傳遞給它所在的點擊事件上的子view。
<pre>final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
這里調(diào)用onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
</pre>
上面的代碼表示专肪,ViewGroup在兩種情況下會判斷是否攔截當前事件刹勃,事件類型為ACTION_DOWN或者mFirstTouchTarget!=null嚎尤; 當事件由ViewGroup的子view成功處理時荔仁,mFirstTouchTarget會被賦值并指向子view這段代碼按照我的理解:事件開始時ACTION_DOWN,ViewGroup進入判斷是否攔截,如果攔截mFirstTouchTarget就為null芽死,然后event 為ACTION_MOVE乏梁,ViewGroup進入判斷ACTION==DOWN false,mFirstTouchTarget关贵!=null false遇骑,然后直接進入else intercepted = true,結果為onInterceptTouchEvent不會再被調(diào)用揖曾,并且同一事件序列里除ACTION_DOWN的事件都由ViewGroup處理落萎。
FLAG_DISALLOW_INTERCEPT標記位,這個標記位通過requestDisallowInterceptTouchEvent方法設置炭剪,一般用于子View中模暗。FLAG_DISALLOW_INTERCEPT一旦設置后,ViewGroup無法攔截除了ACTION_DOWN其他的點擊事件念祭。因為在ViewGroup分發(fā)事件時,如果是ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT這個標記位碍侦。
ViewGroup不攔截事件時粱坤,事件會向下分發(fā)給它的子View,首先遍歷ViewGroup的所有子元素瓷产,然后判斷子元素是否能狗接收到點擊事件站玄。是否接收到點擊事件由兩點衡量:子元素是否在播放動畫,點擊事件的坐標是否在子元素的區(qū)域內(nèi)濒旦。如果某個子元素滿足這兩個條件株旷,那么事件就會交給他處理,這時會調(diào)用子View的dispatchTouchEvent。如果子元素的dispatchTouchevent返回true那么mFirstTouchTarget就會被賦值同時跳出for循環(huán)晾剖。
遍歷所有的子元素事件沒有被合適地處理到锉矢,有兩種情況:1.ViewGroup沒有子元素 2.子元素處理了事件但是在dispatchTouchEvent中返回了false,一般是在onTouchEvent里返回了false齿尽,這兩種情況ViewGroup自己處理事件
View的事件分發(fā)過程
<pre>
public boolean dispatchTouchEvent(MotionEvent event) {
..
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
..
return result;
}
</pre>
從上面的代碼看沽损,首先會判斷是否設置了OnTouchListener,如果OnTouchListener里的OnTouch返回了true循头,OnTouchevent就不會被執(zhí)行绵估,OnTouch的優(yōu)先級要高于OnTouchEvent,DispatchTouchEvent也會返回true
然后是OnTouchEvent里的具體處理:
<pre>
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
</pre>
挑重點看卡骂,只要View的CLICKABLE和LONG_CLICKABLE有一個為true国裳,那么它就會消耗這個事件不管他是不是DISABLE狀態(tài),View的LONG_CLICKABLE默認為false全跨,而CLICKABLE是否為false和具體的View有關缝左,例如:Button是可點擊的CLICKABLE為true,Imageview螟蒸、TextView為false盒使。這里我有了一個疑問?通過前面的分析知道如果OnClickListener.onClick在onTouchEvent里調(diào)用七嫌,如果CLICKABLE和LONG_CLICKABLE直接返回了false少办,其他邏輯根本沒有走?那么為什么TextView 設置了點擊事件可以正常調(diào)用呢诵原?
原因是:setOnClickListener會自動講View的CLICKABLE置為true英妓,setOnLongClickListener會自動講LONG_CLICKABLE置為true
<pre>
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
</pre>
View的滑動沖突
外部攔截法
指事件都先經(jīng)過父容器的攔截處理,外部攔截法需要重寫父容器的onInterceptTouchEvent 偽碼表示:
<pre>
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean isInterceped = false;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isInterceped = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要攔截當前事件){
isInterceped = true;
}else {
isInterceped = false;
}
break;
case MotionEvent.ACTION_UP:
isInterceped = false;
break;
}
return isInterceped;
}
</pre>
在onInterceptTouchEvent中绍赛,首先是ACTION_DOWN蔓纠,父容器必須返回false,即不攔截ACTION_DOWN事件吗蚌,因為一旦父容器攔截了ACTION_DOWN事件那么后續(xù)的ACTION_MOVE和ACTION_UP事件都直接交給了父容器處理腿倚,這個時候事件就沒法傳遞給子元素了,然后是ACTION_MOVE事件蚯妇,這個事件可以根據(jù)需求考慮是否攔截敷燎,如果攔截就返回true,否則返回false箩言。最后是ACTION_UP事件必須返回false硬贯,因為ACTION_UP本身沒有什么意義,假設事件交給子元素處理陨收,如果ACTION_UP返回了true饭豹,子元素無法接收ACTION_UP事件,這時候子元素中的onCLick事件就無法觸發(fā)。
內(nèi)部攔截
指父容器不攔截任何事件拄衰,所有事件都傳遞給子元素它褪,如果子元素需要此事件就直接消耗掉,否則就交給父容器進行處理肾砂,這種方式需要配合requestDisallowInterceptTouchEvent使用
在子View的dispatchTouchEvent方法里處理列赎,同時父容器要默認攔截除ACTION_DOWN的其他事件
eg:ListView嵌套ViewPager 如果手指是側這滑動的話,就會造成事件沖突镐确,這里我的策略是:判斷橫向還是豎向距離更多一些包吝,橫向的話就交給ViewPager處理,豎向的話交給ListView處理源葫。
外部攔截方式:
<pre>
PointF lastPont = new PointF();
PointF currentPoint = new PointF();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
currentPoint.x = ev.getX();
currentPoint.y = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float dX = Math.abs(currentPoint.x - lastPont.x);
float dY = Math.abs(currentPoint.y - lastPont.y);
if (dY/dX>1){
return super.onInterceptTouchEvent(ev);
}else {
return false;
}
case MotionEvent.ACTION_UP:
break;
}
lastPont.x = currentPoint.x;
lastPont.y = currentPoint.y;
return super.onInterceptTouchEvent(ev);
}
</pre>
內(nèi)部攔截方式:
在ViewPager onTouchEvent中
<pre>
if ((y>1||x>1)&&x/y>1) {
橫向滑動诗越,不讓父布局攔截
getParent().requestDisallowInterceptTouchEvent(true);
}else {
getParent().requestDisallowInterceptTouchEvent(false);
}
</pre>