Android源碼分析——ViewGroup的事件分發機制(二)

在這裏插入圖片描述
通過前一篇博客View的事件分發機制,從dispatchTouchEvent說起(一)的介紹相信大家對 Android View 事件的分發機制有了很深的理解。我們知道 Android 中 View 是存在於 Activity。 今天我們繼續學習 Activity 到 ViewGroup 的事件分發機制。

一、Activity 分發到 ViewGroup

當我們手指觸摸到屏幕時,最先接收到事件的肯定是Activity,首先調用的是ActivitydispatchTouchEvent(event),那麼我們下面先來看它的源碼:

1、dispatchTouchEvent(event)

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

這裏我們看到首先它判斷了是不是 DOWN 事件,如果是的話調用了onUserInteraction()這個方法我們看到在Activity源碼中是空的,它可以讓子類去實現。我們這裏不需要多做關注。

下面我們發現這裏它繼續調用了getWindow().superDispatchTouchEvent(ev),這裏我們後面再講。我們看到如果前面返回時false的話,後面調用了ActivityonTouchEvent(ev)

那麼我們繼續看getWindow().superDispatchTouchEvent(ev),這裏我們之前文章提到過,getWindow()返回的是 PhoneWindow 對象,那麼我們繼續看PhoneWindow,我們會發現它調用了mDecor.superDispatchTouchEvent(event),也就是執行到 DecorView 的superDispatchTouchEvent(event)。我們繼續追蹤發現,最終掉的是 ViewGroup 的 diapatchTouchEvent(event)方法。

那我們這裏先總結下dispatchTouchEvent
Activity==>PhoneWindow==>DecorView==>ViewGroup

2、onTouchEvent(event)

上面的具體到 ViewGroup 我們後面再看,這裏我們還是回到之前的如果返回false那麼就需要調用Activity.onTouchEvent(ev),那麼它的源碼如下:

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

這裏只是判斷了點擊事件的邊界條件,如果在邊界內就直接返回了false,否則finish當前 Activity。

二、ViewGroup 的分發

我們上面看到Activity.dispatchTouchEvent(ev)最終調用到ViewGroup.diapstchTouchEvent(ev)那麼下面我們繼續看下它的源碼:

1、ViewGroup.dispatchTouchEvent(ev)

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    /*****************省略部分非核心代碼***********************/
        //標記事件是否被處理
        boolean handled = false;
        //判斷當前窗口是不是模糊窗口如果是則攔截掉,不是則繼續分發
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 初始化DOWN事件
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 檢查是否攔截該事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    //判斷是否可以被攔截
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//判斷是否攔截該事件
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }
             /*****************省略部分非核心代碼***********************/
            //檢查是否取消
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //如果不攔截並且不取消則繼續執行
            if (!canceled && !intercepted) {
              
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                   /*****************省略部分非核心代碼***********************/
                    final int childrenCount = mChildrenCount;
                    //newTouchTarget觸摸目標鏈表裏面存儲有子View
                    if (newTouchTarget == null && childrenCount != 0) {
                       /*****************省略部分非核心代碼***********************/
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                           /*****************省略部分非核心代碼***********************/
                           //將當前子View添加到newTouchTarget
                            newTouchTarget = getTouchTarget(child);
                           /*****************省略部分非核心代碼***********************/
                           //這裏去分發TouchEvent 
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            
                                /*****************省略部分非核心代碼***********************/
                                  
                            }
                            
             /*****************省略部分非核心代碼***********************/
          
        return handled;
    }

這個方法可以說是非常的多,看着就令人頭大。

這裏我們主要看核心代碼,以下重要的代碼我再裏面都添加了註釋。非核心代碼我們這裏略過。我們這裏注意以下幾點:

  1. handled: 這個變量是標記觸摸事件是否被處理的,默認是false代表未被消耗。
  2. onFilterTouchEventForSecurity(ev): 這個方法我們看到是一個非常重要的方法,後面幾乎所有的代碼都在這個方法返回true之後才執行。那麼這個方法是幹啥的呢?它其實是判斷當前窗口是否是模糊窗口?如果是的話則返回false,此時將不會在將事件分發給子 View.
  3. intercepted: 標記當前父佈局要不要攔截當前事件,一般都是取onInterceptTouchEvent(ev)的方法。
  4. onInterceptTouchEvent(ev):判斷當前 ViewGroup 是否攔截當前事件,一般返回flase不攔截。我們可以在子類重寫該方法。
  5. cancled: 這個變量就是帶判斷當前手勢是不是被取消。如果cancledintercepted都是false纔會去執行後面的分發。
  6. newTouchTarget:這是一個單向鏈表,裏面存儲的是觸摸目標的 View 。後面調用getTouchTarget(child)將子 View 的觸摸目標添加到 newTouchTarget 。
  7. dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign): 這個方法就是去將觸摸事件分發給對應子 View,下面我們就來看下這個方法。

I、ViewGroup.dispatchTransformedTouchEvent

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        //判斷是否是取消
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

      /*****************省略部分非核心代碼***********************/
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);
                    
                    //調用子 View 的dispatchTouchEvent(event)
                    handled = child.dispatchTouchEvent(event);

         /*****************省略部分非核心代碼***********************/
     
        return handled;
    }

這個方法的代碼也不少我們來看看核心代碼,首先前面判斷當前事件是否是取消事件。如果是的話則執行子 View 的dispatchTouchEvent。 後面我們看到最終執行到handled = child.dispatchTouchEvent(event);來返回handled。到這裏我們就發現最終這裏調用的是 View 的 dispatchTouchEvent(event)。所以這裏我們可以用下面的僞代碼來總結一下 ViewGroup 的事件分發機制。

2、僞代碼總結ViewGroup的事件分發

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean concume = false;
    //調用onInterceptTouchEvent判斷是否攔截
    if(onInterceptTouchEvent(ev)){
      //如果攔截則調用自己的onTouchEvent(ev)消耗
      consume = onTouchEvent(ev);
    }else{
        //如果不攔截,則將事件分發給子View
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

總體來說就是首先判斷 ViewGroup 自己攔截不攔截,如果攔截則調用自己的onTouchEvent(ev),如果不攔截則調用子 View 的dispatchTouchEvent(event)

三、總結測試

我們還是寫一個簡單的項目來測試一下,一個 Activity,一個ViewGroup ,一個View。我們分別重寫我們上述提到的方法。
寫之前我們先思考下面兩個問題:

  1. 當所有View都不消耗該事件的時候,事件如何傳遞?
  2. 子View不處理事件時,UP 事件是跟 DOWN 事件一樣傳遞的嗎?
  3. 子 View 消耗完事件,ViewGroup 的 onTouchEvent還會執行嗎?

MainActivity.java

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new CustomLayout(this));
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("TAG_紫霧凌寒_MainActivity","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("TAG_紫霧凌寒_MainActivity","===dispatchTouchEvent===="+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
}

CustomLayout.java

class CustomLayout extends LinearLayout {
    public CustomLayout(Context context) {
        super(context);
        addView(new CustomView(context));
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("TAG_紫霧凌寒_CustomLayout","===onInterceptTouchEvent===="+ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
        Log.e("TAG_紫霧凌寒_CustomLayout","===dispatchTouchEvent===="+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event){
        Log.e("TAG_紫霧凌寒_CustomLayout","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }
}

CustomView.java

class CustomView extends View {
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e("TAG_紫霧凌寒_CustomView__","===dispatchTouchEvent===="+event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(5f);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(300,300,280,paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("TAG_紫霧凌寒_CustomView__","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }

    public CustomView(Context context) {
        super(context);
    }
}

1、子View也不處理觸摸事件

我們回到之前我們的問題,當子 View 也不處理觸摸事件的時候,那麼觸摸事件該如何傳遞?
對於這樣的問題我們模擬一下,上面的代碼就是子View對事件沒有消耗,我們點擊自定義 View 的區域,我們看到日誌如下所示:
在這裏插入圖片描述
我們通過日誌看到,當子View不處理時,我們看到事件的傳遞是會返回到 Activity 執行 Activity 的 onTouchEvent(ev).

同樣我們可以看到,當執行完 DOWN 事件後,UP 事件只在 Activity 層,並沒有傳遞至 ViewGroup 和 View。

2、子View處理觸摸事件

那麼我們讓子View處理觸摸事件,也就是View.onTouchEvent(ev)返回true.我們再來看下日誌:
在這裏插入圖片描述
這裏我們看到當View如果處理觸摸事件時,DOWN 和 UP事件的傳遞是一樣的。消耗完之後就不會再返回上一層了。我們看到子 View 處理完事件後,ViewGroup 的 onTouchEvent 沒再執行了。

3、父佈局攔截事件

當我們讓 ViewGroup 攔截觸摸事件時我們看看日誌是不是跟我們前面分析的一樣?
在這裏插入圖片描述
我們看到這裏跟我們分析的一樣,當 ViewGroup 攔截後,就不會傳遞到 View 。

4、總結

我們回顧一下本文我們主要講的 Activity 的觸摸事件的分發機制:手指按下屏幕==>Activity==>PhoneWindow==>DecorView==>ViewGroup==View。下面我們還是通過流程圖來總結一下它的傳遞機制:
Activity事件的分發

歡迎在評論區留下你的觀點大家一起交流,一起成長。如果今天的這篇文章對你在工作和生活有所幫助,歡迎轉發分享給更多人。

同時歡迎大家掃描左側二維碼關注我的公衆號和加入我組建的大前端學習交流羣,羣裏大家一起學習交流 Android、Flutter等知識。從這裏出發我們一起討論,一起交流,一起提升。

羣號:872749114

往期推薦
View的事件分發機制,從dispatchTouchEvent說起(一)
Android源碼分析——View是如何被添加到屏幕的?
Android熱修復——深入剖析AndFix熱修復及自己動手實現
深入理解HashMap原理(一)——HashMap源碼解析(JDK 1.8)
深入理解HashMap原理(二)——手寫HashMap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章