View 點擊事件的分發機制


這里面的代碼以及文字來自 任玉剛的 <<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將會被調用

結論:

  1. 同一事件序列包括down,move,up
  2. 正常情況,一個事件序列全部被一個view消耗.特殊情況是view強行使用onTouchEvent傳遞
  3. view一旦決定攔截,那么這個事件序列全部歸它,並且onInterceptTouchEvent不會再被調用
  4. view不消耗down事件,那么事件序列其他事件也與它無緣.會再次給父控件onTouchEvent
  5. 如果view只消耗down事件,那么這個點擊事件會消失,會直接傳遞給activiy
  6. viewGroup默認不攔截任何事件
  7. view沒有onInterceptTouchEvent,只要收到事件就調用onTouchEvent
  8. view的onTouchEvent默認返回true.如果clickable和longClick同時為false,那么返回false.
  9. view的enable不影響onTouchEvent
  10. onclick發生的條件 a.view可點擊 b.收到了down和up
  11. 事件傳遞由外向內.子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,還能添加監聽

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM