安卓事件機制原理

其實要想很好的理解安卓事件機制,最好的方式就是本身擼一個簡化的版本。也就是提取閱讀源碼後的思想。java

廢話很少說,直接開始。若是沒有閱讀安卓事件機制源碼的,能夠參考這篇安卓事件分發機制源碼詳讀 。 咱們知道事件是從 Activity 開始分發,最終回到 ViewGroup 的 dispatchTouchEvent(event) 。這裏本質上就是模仿,新建一個安卓工程,並添加一個 java 庫的 lib, 注意是 java lib。git

爲了提取思想,咱們也是仿造安卓事件機制部分源碼。這裏也新建一個 MyViewGroup 的 java 類繼承自 MyView, 由於事件機制中 ViewGroup 也是繼承自 View,這裏也是作了類似的操做。咱們在使用手勢的時候,應該對 MotionEvent 確定是不陌生的,這個類中封裝了當前點相對於控件的 x,y 座標,以及當前的事件類型,好比 ACTION_DOWN、ACTION_MOVE、ACTION_UP。 固然這裏只是簡化了的。github

首先闡述核心原理,在 MyViewGroup 中保存添加的子視圖,當一個事件來的時候先來到 MyViewGroup, 而後倒敘遍歷查找容器的的子視圖是否有被符合點擊範圍的,若是符合。則將事件分發給這個子視圖,子視圖若是是容器,則會繼續遍歷其子視圖。若是是子視圖就檢查是否攔截事件,若是不攔截就會返回到父視圖的 onTouchEvent,父視圖能夠選擇攔截,反之繼續往上層容器返回事件。bash

先來看 MyViewGroup 的 dispatchTouchEvent(event)ide

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        System.out.println("當前視圖容器的名稱: " + super.name);
        int actionMasked = event.getActionMasked();
        boolean intercepted = false;

        boolean handled = false;
        
        // 當按下事件來
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 容器視圖能夠重寫此方法返回 true 攔截事件
            intercepted = onInterceptTouchEvent(event);
        }

        boolean alreadyDispatchedToNewTouchTarget = false;
        // 事件未被攔截也沒有取消
        if (!intercepted && actionMasked != MotionEvent.ACTION_CANCEL) {
            // 獲取按下的 x, y 距離視圖的左上點
            float x = event.getX();
            float y = event.getY();
            int childrenCount = mChildrenCount;
            MyView[] children = mChildren;
            
            // 採起倒敘遍歷的方式,由於通常日後添加的視圖都會在最上面可見。
            for (int i = childrenCount - 1; i >= 0; i--) {
                MyView child = children[i];

                // 檢查按下的座標是否在 view 上.
                if (!isTransformedTouchPointInView(x, y, child)) {
                    System.out.println("未點擊的view上: " + child.name);
                    continue;
                }

                // 將事件交給 child 來處理,由 child 決定是否消費
                TouchTarget newTouchTarget;
                if (dispatchTransformedTouchEvent(event, child)) {
                    newTouchTarget = addTouchTarget(child);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }

        // 若是沒有事件被消費就會將事件交給容器來處理.
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(event, null);
        } else {
            if (alreadyDispatchedToNewTouchTarget) {
                handled = true;
            }
        }

        return handled;
    }

複製代碼

代碼中有詳細的註釋, 接着來看 dispatchTransformedTouchEvent(event), 這個方法就很簡單了,就是根據 child 判斷,是否繼續傳遞,仍是將事件返回給父視圖post

private boolean dispatchTransformedTouchEvent(MotionEvent event, MyView child) {
        boolean handled = false;

        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }

        return handled;
    }

複製代碼

怎麼檢查手勢是否在按下的範圍呢?isTransformedTouchPointInView(float x, float y, MyView child), 其實也很簡單,也就是 x 的起點是否大於左邊 mLeft且小於右側 mRight, y 軸大於頂部 mTop, 小於底部 mBottom。ui

private boolean isTransformedTouchPointInView(float x, float y, MyView child) {
        if (x >= child.mLeft && x <= child.mRight && y >= child.mTop && y <= child.mBottom) {
            return true;
        }

        return false;
    }
複製代碼

接下來經過調試來驗證代碼,首先沒有任何視圖消費事件,看看是否是符合安卓事件機制的分發過程。當運行後,看到以下打印,能夠看到若是視圖不消費事件就會將事件回傳。並調用 視圖 onTouchEvent。spa

第一個驗證符合,接下來咱們讓容器攔截掉事件。看看還會不會往下傳遞, onInterceptTouchEvent 返回true。調試

// container 容器返回 true
 public boolean onInterceptTouchEvent(MotionEvent event) {

    return true;
}
複製代碼

能夠看到只有 container 容器的 onTouchEvent 被調用。第二個驗證符合。 code

第三個驗證來看,子視圖若是設置了 setOntouchListener 後還會不會調用 onTouchEvent

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 視圖的 onTouch 方法被調用.");
                return true;
            }
    });

複製代碼

能夠看到沒有打印任何相關 onTouchEvent 的信息, 咱們返回 false 看看。

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 視圖的 onTouch 方法被調用.");
                return false;
            }
    });
複製代碼

是否是就先打印了 onTouch, 而後再打印了 onTouchEvent。 因爲沒有事件被消費,因此事件最終會被回傳到 container 容器。

驗證最後一個,就是點擊事件確定是在觸摸事件以後被調用,前提是 onTouch 沒有攔截。

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 視圖的 onTouch 方法被調用.");
                return false;
            }
        });


        myView.setOnClickListener(new MyView.OnClickListener() {
            @Override
            public void onClick(MyView view) {
                System.out.println("myView 視圖的 onClick 方法被調用.");
            }
        });
複製代碼

OK, 符合驗證。若是 onTouch 返回 true。

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 視圖的 onTouch 方法被調用.");
                return true;
            }
        });


        myView.setOnClickListener(new MyView.OnClickListener() {
            @Override
            public void onClick(MyView view) {
                System.out.println("myView 視圖的 onClick 方法被調用.");
            }
        });
複製代碼

onClick 方法就不會被調。

這就是事件機制流程,至於不少細節未去處理。好比安卓事件機制維護了一個 TouchTarget, 它本職上是一個鏈表。經過 mFirstTouchTarget 來指向它的頭節點,若是找到一個 view 須要被處理事件的時候就用頭節點去記錄它。以便後續的事件來的時候能夠很快的找處處理的視圖。

最後若是想看源碼的,能夠克隆下載查看,AndroidMotionEvent

相關文章
相關標籤/搜索