天天看點

Anroid觸摸事件分發機制---ViewGroup相關

這篇文章隻介紹ViewGroup事件分發機制,因為Activity和View的事件分發,相對很簡單,網上随便找一篇文章基本就能了解了,但是對于ViewGroup的事件分發機制,我真的看了不下20篇文章,沒有一篇講的到位的,沒有一篇告訴你重點關注哪個點,最後隻是告訴你一堆結論,或者給你看一堆流程圖,最氣人的是沒有一篇文章告訴你ACTION_CANCEL怎麼回事,ACTION_CANCEL時系統其實做了一個很重要的操作,這個操作你了解了,你之前的疑惑也許就都 了解了,後面會提到,其實對于ViewGroup的分發源碼,你看Android2.3的源碼就可以了,真的很簡單,甚至讓你看了後一輩子都忘不了,肯本不需要記任何結論或畫任何複雜的流程圖,高版本的源碼太過複雜但是原理和Android2.3基本一樣。

重點要關注的點,我會在涉及到的時候強調。

一:Android事件分發整體流程簡圖

這是個關注點

這個圖很重要,可能你也懂這個圖,但是我隻是想提醒你看源碼的時候心裡想着這個圖,這樣邏輯才清晰。

Anroid觸摸事件分發機制---ViewGroup相關

二:ViewGroup事件分發Anroid2.3源碼

我先把源碼帖出來,這個源碼跟高版本相比已經很簡單了,但是我們看的時候還是可以簡化一些,我下面會貼出重點代碼

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;
    if (target == null) {
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }
        return super.dispatchTouchEvent(ev);
    }
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }
    return target.dispatchTouchEvent(ev);
}
           

上面的代碼我們再簡化一下,并做 一些說明:

pirvate View mMotionTarget;
public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    //是否允許攔截
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {//隻有ACTION_DOWN時才走這個if語句
      //ACTION_DOWN時重置mMotionTarget為null,mMotionTarget指向處理并且消費掉目前ACTION_DOWN的子View。
      //如果不為null說明指向上次處理并消費了ACTION_DOWN的子View,是以要重置為null
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        //disallowIntercept預設為false,可通過requestDisallowInterceptTouchEvent(true)修改其值
        //高版本anroid中我有看到,在ACTION_DOWN時有一個對FLAG_DISALLOW_INTERCEPT重置的操作,避免requestDisallowInterceptTouchEvent(true)造成的影響
        //是以我在這裡加這句代碼和高版本一緻
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        //不攔截有兩種情況:1,不允許攔截;2,雖然允許攔截,但是并沒有攔截。
        //既然不攔截就找對應的子View處理這個ACTION_DOWN.就是for循環判斷 點選區域落到哪個子View身上。
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            for (所有子View) {
                final View child = 找到的那個可以接收ACTION_DOWN的子View;
                if (child.dispatchTouchEvent(ev))  {
                  //交給這個子View處理,如果子View消費掉了,就讓mMotionTarget指向這個子View,且直接return,本次ACTION_DOWN事件分發結束。
                  //如果這個子View雖然處理了但是沒有消費掉該ACTION_DOWN,則繼續向下執行。
                    mMotionTarget = child;//唯一給mMotionTarget指派的地方,謹記!!!
                    return true;
                }
            }
            //沒有找到合适的子View處理該ACTION_DOWN,例如點選了空白區域,即沒有子View的區域等等。繼續向下執行
        }
    }
    //targt和mMotionTarget指向同一個對象,需要注意的是,隻有在ACTION_DOWN時ViewGroup不攔截且子View消費掉ACTION_DOWN時,才給mMotionTarget,隻有這一個地方,謹記!!!
    final View target = mMotionTarget;
    //target為null,說明沒有可接收事件的子View,需要ViewGroup自己處理該事件
    //target為null的原因有很多:
    //1,ACTION_DOWN時,可能是之前的邏輯中onInterceptTouchEvent()傳回true即攔截了,或者雖然沒攔截但是沒有找到合适的子View處理該事件,
    //或者雖然找到子View處理該事件了但是子View沒有消費掉該事件,這些都導緻mMotionTarget沒有被指派(參考前面的for循環部分)。
    //2,非ACTION_DOWN時,則說明之前的事件序列都是ViewGroup自己處理的(參考1)或者之前ViewGroup攔截了某次事件(後面的代碼可看到攔截時會将mMotionTarget置為null)
    if (target == null) {
      //ViewGroup自己處理事件的邏輯
        return super.dispatchTouchEvent(ev);//若執行了這裡則後續代碼不再執行
    }
    //能走到這裡的不可能是ACTION_DOWN,且target不是null,說明子View有處理能力同時ViewGroup也可以攔截,體會一下。
    //如果攔截則給子View(target)一個ACTION_CANCEL事件,同時mMotionTarget置為null,本次分發結束,下次分發的事件就交給ViewGroup自己處理,
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        if (!target.dispatchTouchEvent(ev)) {
        }
        //下面兩行可看到,所謂的攔截其實并不是攔截,而是讓子View執行ACTION_CANCEL事件,但是ViewGroup并不處理攔截下來的這個事件,而是直接消費掉(true),隻是說下次事件
        //由我ViewGroup來處理。
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    //如果不攔截,則由子View處理
    return target.dispatchTouchEvent(ev);
}
           

這段代碼你肯定看的懂,沒必要告訴你一堆結論,給你一堆流程圖,記結論很累,你的了解就是結論。

這裡并沒有詳細介紹onInterceptTouchEvent(ev)和requestDisallowInterceptTouchEvent(true)兩個方法,這兩個方法很好了解,也有很多文章介紹,,簡單說下就是:

requestDisallowInterceptTouchEvent(true)用于修改FLAG_DISALLOW_INTERCEPT标記為,為true說明禁止Viewgroup攔截,這樣也就不會調用onInterceptTouchEvent(ev)判斷是否攔截了,為false說明允許攔截,注意,允許攔截不代表肯定攔截,還要調用onInterceptTouchEvent(ev)判斷最終攔截不攔截。

關注點*:再次強調看源碼時一定要結合事件分發大體過程看,結合最上面的圖看,腦子裡要有這個分發的過程,提醒 自己時間是随着手指邊移動邊不斷分發的。且是不斷調用dispatchTouchEvent(MotionEvent ev) 的

關注點1:mMotionTarget預設為null,唯一給它指派的地方在有子View消費掉ACTION_DOWN時,注意是唯一。

關注點2:target就是mMotionTarget

關注點3:ViewGroup攔截子View的某個非ACTION_DOWN事件時,會給這個子View一個ACTION_CANCEL事件同時把mMotionTarget置為null,這樣下次分發的事件就由ViewGroup處理了。因為mMotionTarget為null了,這個事件序列之後的事件就全部交給ViewGroup處理了。

如果對您有用,請打賞仨瓜倆棗的!

Anroid觸摸事件分發機制---ViewGroup相關
Anroid觸摸事件分發機制---ViewGroup相關