這里面的代碼以及文字來自 任玉剛的 <<android開發藝術探索>> 此處僅作為個人筆記使用
點擊事件的傳遞規則
/** * 用來進行事件的分發,如果事件能夠傳遞給當前view,則此方法一定會被調用,返回結果受onTouchEvent * 和下級的dispatchTouchEvent影響 * @param event * @return 表示是否消耗了該點擊事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { boolean consume=false; if (onInterceptTouchEvent(event)){ consume=onTouchEvent(event); }else { consume= child.dispatchTouchEvent(event); } return consume; } /** * 在dispatchTouchEvent內部調用,用來判斷是否攔截某個某個事件,如果當前View 攔截了某個事件, * 那么在同一個事件序列中,此方法不會被再次調用 * @param ev * @return 表示是否攔截當前事件. */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { throw new RuntimeException("Stub!"); } /** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }
viewgroup的事件處理如圖所示

view的點擊處理邏輯

點擊事件的傳遞過程
activity->window->view
頂級的view接收到點擊事件以后,就會按照分發機制一層層的分發事件.如果其中的某一個view的ontouchevent返回了false,那么它父容器的ontouchevent將會被調用
結論:
- 同一事件序列包括down,move,up
- 正常情況,一個事件序列全部被一個view消耗.特殊情況是view強行使用onTouchEvent傳遞
- view一旦決定攔截,那么這個事件序列全部歸它,並且onInterceptTouchEvent不會再被調用
- view不消耗down事件,那么事件序列其他事件也與它無緣.會再次給父控件onTouchEvent
- 如果view只消耗down事件,那么這個點擊事件會消失,會直接傳遞給activiy
- viewGroup默認不攔截任何事件
- view沒有onInterceptTouchEvent,只要收到事件就調用onTouchEvent
- view的onTouchEvent默認返回true.如果clickable和longClick同時為false,那么返回false.
- view的enable不影響onTouchEvent
- onclick發生的條件 a.view可點擊 b.收到了down和up
- 事件傳遞由外向內.子view可以通過requestDisallowInterceptTouchEvent干涉父元素除了down事件以外的其他事件
實踐
好了經過上面的理論討論,我們來實踐一下.我們新建兩個自定義控件,一個是繼承LinearLayout一個繼承Button.具體代碼如下
public class TestLayout extends LinearLayout { private static final String TAG = "TestLayout"; public TestLayout(Context context) { super(context); } public TestLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public TestLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 用來進行事件的分發,如果事件能夠傳遞給當前view,則此方法一定會被調用,返回結果受onTouchEvent * 和下級的dispatchTouchEvent影響 * @param event * @return 表示是否消耗了該點擊事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent: "+event.getAction()); return super.dispatchTouchEvent(event); } /** * 在dispatchTouchEvent內部調用,用來判斷是否攔截某個某個事件,如果當前View 攔截了某個事件, * 那么在同一個事件序列中,此方法不會被再次調用 * @param ev * @return 表示是否攔截當前事件. */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.d(TAG, "onInterceptTouchEvent: "+ev.getAction()); return super.onInterceptTouchEvent(ev); } /** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent: "+event.getAction()); return super.onTouchEvent(event); } }
public class TestBtn extends Button{ private static final String TAG = "TestBtn"; public TestBtn(Context context) { super(context); } public TestBtn(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public TestBtn(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 用來進行事件的分發,如果事件能夠傳遞給當前view,則此方法一定會被調用,返回結果受onTouchEvent * 和下級的dispatchTouchEvent影響 * @param event * @return 表示是否消耗了該點擊事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent: "+event.getAction()); return super.dispatchTouchEvent(event); } /** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent: "+event.getAction()); return super.onTouchEvent(event); } }
剩下activity和布局的代碼太簡單這里我們就不貼了,我們直接運行一下.然后點擊一下按鈕.
可以得到以下運行結果
1. down事件:
TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
2. move事件:
TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
3. up事件:
TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
4. button的click事件
以上我們可以得到以下結論:
1.一開始我們的流程是對的
2.驗證了上面結論的1,2,6,7
3.click事件是最后觸發的
我們現在再修改代碼,將testBtn的onTouch返回值更改為false
/** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent: "+event.getAction()); boolean isTouch=super.onTouchEvent(event); Log.d(TAG, "onTouchEvent: "+isTouch); return false; }
然后再運行一下代碼,看一下獲得的結果
1. down事件:
TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent->TestLayout:onTouchEvent
然后就沒了,對,沒了,沒有move事件,也沒有up事件,也沒有點擊事件
我們可以得到以下結論:
1.首先down事件正常傳遞,當傳遞到testBtn的時候,testBtn的onTouchEvent返回false,代表了沒有對點擊事件進行處理.那么按照上面我們總結的結論的第4條,很自然的就調用了父控件也就是TestLayout的onTouchEvent方法.由於viewgroup的onTouchEvent的返回值為
false.所以最終這個事件序列,他們兩個都木有處理,所以這個事件序列的其他事件自然與他們無關了
2.並且從這里我們也可以看出,如果testBtn只收到down沒有收到up,是不會調用onclick方法的
現在我們進一步修改代碼,我們將testLayout的onTouchEvent的返回值由false更改為true.看看會發生什么
/** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent: "+event.getAction()); boolean isTouch=super.onTouchEvent(event); Log.d(TAG, "onTouchEvent: "+isTouch); return true; }
好的,我們運行一下程序,看一下會發生什么
1. down事件:
TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent->TestLayout:onTouchEvent
2. move事件:
TestLayout:dispatchTouchEvent ->TestLayout:onTouchEvent
3. up事件:
TestLayout:dispatchTouchEvent ->TestLayout:onTouchEvent
總結
其實這次獲得的結論和上面的測試獲得的結論是一樣的.只是更新驗證了我們的結論.當然我們也可以看到,如果下面的view沒有消耗事件流的down事件,那么后面的事件都和它無關.並且我們也可以看到上面結論3也被證實了.
好了,我們看一下之前總結的結論,除了5,8,9.其他的,都得到了證實.那么我們接下來就來驗證結論5,8,9.
先是結論5
我們將testLayout的onTouchEvent恢復成原來的樣子,返回值更改為false
將testBtn的onTouchEvent改成下面的樣子
/** * 在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗, * 則在同一個事件序列中,當前view無法再次接收到事件. * @param event * @return 表示是否消耗當前事件, */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent: "+event.getAction()); boolean isTouch=super.onTouchEvent(event); Log.d(TAG, "onTouchEvent: "+isTouch); if (event.getAction()==MotionEvent.ACTION_DOWN){ return true; }else { return false; } }
運行后我們會獲取結果:
1. down事件:
TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
2. move事件:
TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
3. up事件:
TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent
4. button的click事件
乍一看這個結果,咦,不是和第一個button和layout什么都不改不是一樣的嘛.其實就是一樣的.但是你發現沒有,其實move事件和up事件,我們沒有任何控件去消耗它.它們就這樣消失了,即沒有被testButton消耗,也沒有被testLayout消耗
下面我們再把現場恢復到默認的樣子.
然后在activity中添加一個
testBtn.setClickable(false);
但是請注意,這個設置一定要在setOnClickListener以后調用,源碼嘛,看源代碼
/** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. * * @param l The callback that will run * * @see #setClickable(boolean) */ public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
好的,現在我們運行以后,很明顯的就能看到運行結果,testBtn的onTouchEvent的返回值編程了false,很輕易的驗證了,我們的結論8.同時也知道了為什么TextView的onclickable為false,還能添加監聽
