Android觸摸事件分發(fā)機制(2)之ViewGroup

Android觸摸事件分發(fā)機制(1)之View

上一篇文章我們分析了View的事件分發(fā)機制,今天我們分析下ViewGroup的時間分發(fā)機制魄揉。ViewGroup是View的子類剪侮,所以它肯定有繼承部分View的特性,當然它也有自己的特性洛退,二者具體有何不同呢票彪?

例子

我們來舉一個栗子吧~

首先我們自定義Button和自定義LinearLayout,將Button放入在LinearLayout中不狮,下面主要重寫部分方法,添加Log。

TestButton.java
public class TestButton extends Button {
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("w", "TestButton dispatchTouchEvent-- action=" + event.getAction());
        return super.dispatchTouchEvent(event);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("w", "TestButton onTouchEvent-- action=" + event.getAction());
        return super.onTouchEvent(event);
    }
}
TestLinearLayout.java
public class TestLinearLayout extends LinearLayout {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("w", "TestLinearLayout onInterceptTouchEvent-- action=" + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("w", "TestLinearLayout dispatchTouchEvent-- action=" + event.getAction());
        return super.dispatchTouchEvent(event);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("w", "TestLinearLayout onTouchEvent-- action=" + event.getAction());
        return super.onTouchEvent(event);
    }
}
main_activity.xml
<io.weimu.caoyang.TestLinearLayout      
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/mylayout">
    
    <io.weimu.caoyang.TestButton
        android:id="@+id/my_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="click test"/>
        
</com.zzci.light.TestLinearLayout>
MainActivity.java
public class MainActivity.java extends Activity implements View.OnTouchListener, View.OnClickListener {
    private TestLinearLayout mLayout;
    private TestButton mButton;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        setContentView(R.layout.main);
    
        mLayout = (TestLinearLayout) this.findViewById(R.id.mylayout);
        mButton = (TestButton) this.findViewById(R.id.my_btn);
    
        mLayout.setOnTouchListener(this);
        mButton.setOnTouchListener(this);
    
        mLayout.setOnClickListener(this);
        mButton.setOnClickListener(this);
    }
    
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i("w", v+" onTouch-- action="+event.getAction());
        return false;
    }
    
    @Override
    public void onClick(View v) {
        Log.i("w", v+" OnClick");
    }
}

讓我們看一下打印出來的Log(PS:0為DOWN,1為UP)

例子1:當觸摸事件是在TestLinearLayout內在旱,TestButton內

消息
TestLinearLayout dispatchTouchEvent-- action=0
TestLinearLayout onInterceptTouchEvent-- action=0
TestButton dispatchTouchEvent-- action=0
TestButton onTouch-- action=0
TestButton onTouchEvent-- action=0
TestLinearLayout dispatchTouchEvent-- action=1
TestLinearLayout onInterceptTouchEvent-- action=1
TestButton dispatchTouchEvent-- action=1
TestButton onTouch-- action=1
TestButton onTouchEvent-- action=1
TestButton onClick

我們可以看出子View所得到的事件都是由父View派發(fā)所得的摇零。整一個大概流程為:

  1. 執(zhí)行TestLinearLayout的DispatchTouchEvent
  2. 執(zhí)行TestLinearLayout的onIntercepTouchEvent
  3. 執(zhí)行TestButton的disPatchToucheEvent
  4. 接下去的分發(fā)流程與上一文章一致

例子2:當觸摸事件在TestLinearLayout內,TestButton外

消息
TestLinearLayout dispatchTouchEvent-- action=0
TestLinearLayout onInterceptTouchEvent-- action=0
TestLinearLayout onTouch-- action=0
TestLinearLayout onTouchEvent-- action=0
TestLinearLayout dispatchTouchEvent-- action=1
TestLinearLayout onTouch-- action=1
TestLinearLayout onTouchEvent-- action=1
TestLinearLayout onClick

以上可以看到桶蝎,沒有TestButton什么事情了~分發(fā)的流程基本與上面一致驻仅。有一處地方有點奇怪谅畅,就是當action=1(ACTION_UP)時,竟然沒有onInterceptTouchEvent噪服。

ViewGroup和View既有些相似又很大的差異毡泻。View是所有視圖的最小單位,而ViewGroup一般內部都包含著多個View粘优。具體他們有啥區(qū)別呢仇味?接下去我們來分析分析。

源碼解讀:

Step1 ViewGroup

從上面的Log雹顺,我們可以看出ViewGroup事件分發(fā)先觸發(fā)的是dispatchTouchEvent方法(此方法重寫了View的方法)丹墨。此方法代碼較多,代碼會盡量注釋,請耐心閱讀嬉愧。

閱讀前的小準備:

  • mFirstTouchTarget:判斷ViewGroup是否已經找到可以傳遞事件的目標組件
  • newTouchTarget:新的目標組件
  • intercepted:標記ViewGroup是否攔截Touch事件的傳遞
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();//獲取當前的分發(fā)事件
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        
        //【Part01】處理初始化按下操作
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //重置一切的點擊狀態(tài)
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
    
        //【Part02】檢查是否要攔截
        final boolean intercepted;
        //只要事件為Down,或無傳遞的目標組件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //是否不允許攔截(此變量可設置贩挣,默認為false)
            if (!disallowIntercept) {
                //☆重要的☆  
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
        
        //【Part03】檢查取消操作,默認為false
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
    
        //如果需要没酣,更新目標視圖列表
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;//
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        //【Part04】子View的掃描
        if (!canceled && !intercepted) {
                ...
                //獲取子View的數量
                final int childrenCount = mChildrenCount;
            //當無傳遞的目標組件王财,且子View數量不為0
                if (newTouchTarget == null && childrenCount != 0) {
                //獲取當前子View的X,Y
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    
                    //從頭到尾掃描,找到一個可以接受事件分發(fā)的子View裕便,
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    //for循環(huán)掃描
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);
                        ...
                        
                        //判斷子View是否為VISIBLE,且判斷觸摸事件是否落在當前子View
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //獲取新的傳遞控件
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
    
                        resetCancelNextUpFlag(child);
                        
                         //☆重要的☆  【文章下面會分析】
                         //觸發(fā)子視圖的普通分發(fā)事件或者本身的普通分發(fā)事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ...
                            
                            //☆重要的☆  增加新的傳遞控件  【文章下面會分析】
                            //給mFirstTouchTarget賦值
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
    
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                    
                    //如果沒找到子View可以傳遞事件绒净,則將指針分配給最近添加的對象
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
    
        //part05 分發(fā)事件給傳遞對象
        if (mFirstTouchTarget == null) {
            //因無傳遞對象,所以直接調用自身的普通傳遞事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                   //分發(fā)事件到指定的傳遞對象
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    
        //part06 刷新傳遞對象狀態(tài)
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }
    ...
    return handled;
}
Part01 處理初始化按下操作

當我們每次按下(ACTION_DOWN)的時候會有初始化操作:cancelAndClearTouchTargets(ev)闪金,resetTouchState()里面有比較重要的操作:將mFirstTouchTarget初始化為null

Part02 檢查是否要攔截

if(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)的判斷下疯溺,只要事件為ACTION_DOWN或者傳遞事件不為空,就可以進入下一個執(zhí)行體哎垦,否則intercepted = true;在下一個執(zhí)行體中我們調用onInterceptTouchEvent并返回值給intercepted囱嫩。

讓我們看一下onInterceptTouchEvent里寫了什么:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

默認返回返回false,也就是intercepted = false;

Part03 檢查取消操作,默認為false

通過標記和action檢查cancel漏设,然后將結果賦值給局部boolean變量canceled墨闲。

Part04 子View的掃描

1.當前觸摸事件的x,y,通過for循環(huán)掃描每個子View,判斷子View是否為VISIBLE和x,y是否落在當前子View上,不是的話跳過此循環(huán)郑口,換下一個子View鸳碧。

2.通過getTouchTarget(child);嘗試獲取newTouchTarget

//為指定的子View獲取觸摸對象,如果沒發(fā)現返回空
private TouchTarget getTouchTarget(@NonNull View child) {
    for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
    
        //判斷當前target所對應的view是否與傳入的View相同
        if (target.child == child) {
            return target;
        }
    }
    return null;
}

3.接著調用方法dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)將觸摸事件傳遞給特定的子View!H浴瞻离!此方法重點在于第三個參數,如果傳子View乒裆,內部會調用子View的dispatchTouchEvent進行子View的普通觸摸事件分發(fā)并返回參數套利,而傳入null時,會調用父類的super.dispatchTouchEvent進行父View的普通觸摸事件分發(fā)并返回參數。

在這種情況下肉迫,如果dispatchTransformedTouchEvent為false,即子View沒有消費验辞。為ture,表明子View消費了。

4.當子View消費事件時喊衫,通過addTouchTarget給newTouchTarget賦值跌造,給alreadyDispatchedToNewTouchTarget賦值為true。

//給mFirstTouchTarget賦值W骞骸?翘啊!
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

到這里我們可以稍微總結一下:dispatchTransformedTouchEvent的返回值

return description mFirstTouchTarget
ture 事件被消費(子View或自身) 賦值
false 事件未消費(子View或自身) null

5.接下來if (newTouchTarget == null && mFirstTouchTarget != null) 該if表示經過前面的for循環(huán)沒有找到子View接收Touch事件并且之前的mFirstTouchTarget不為空則為真联四,然后newTouchTarget指向了最初的TouchTarget撑碴。

Part05 分發(fā)事件給傳遞對象

經過上面的流程后,接著if (mFirstTouchTarget == null)判斷

mFirstTouchTarget為空時朝墩,表明觸摸事件未被消費醉拓,即觸摸事件被攔截或找不到目標子View。此時調用dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS)
內部在調用super.dispatchTouchEvent進行父View的普通觸摸事件分發(fā)

mFirstTouchTarget不為null,依然是遞歸調用dispatchTransformedTouchEvent()方法來實現的處理收苏。

part06 刷新傳遞對象狀態(tài)

當觸摸事件為ACTION_UP或ACTION_HOVER_MOVE時刷新傳遞對象的狀態(tài),mFirstTouchTarget = null;將mFirstTouchTarget置空等操作

Step2 ViewGroup

以上dispatchTouchEvent分析完畢亿卤,此外我們這里在分析下上面經常用到的一個方法dispatchTransformedTouchEvent()

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    //由于重復代碼較多,直接提取精簡代碼
    final boolean handled;
     if (child == null) {
            handled = super.dispatchTouchEvent(event);//父類的普通事件分發(fā)
        } else {
            handled = child.dispatchTouchEvent(event);//子類的普通事件分發(fā)
     }
     return handled;
 }

這個方法是ViewGroup獨特于View的一個方法鹿霸。我們上面的分析也大概講了一下這個方法:第三個參數有無對象排吴,直接影到了內部的調用【child -> view.dispatchTouchEvent】【null -> super.dispatchTouchEvent】

總結 Summary

  1. Android事件派發(fā)是先傳遞到最頂級的ViewGroup,再由ViewGroup遞歸傳遞到View的懦鼠。
  2. 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截钻哩,onInterceptTouchEvent方法返回true代表攔截不向子View傳遞,返回false代表不對事件進行攔截肛冶。默認返回false街氢。
  3. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件睦袖。

Andorid觸摸事件分發(fā)機制(3)之Activity


PS:本文整理自以下文章珊肃,若有發(fā)現問題請致郵 caoyanglee92@gmail.com
工匠若水 Android觸摸屏事件派發(fā)機制詳解與源碼分析二(ViewGroup篇)
Hongyang Android ViewGroup事件分發(fā)機制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市馅笙,隨后出現的幾起案子伦乔,更是在濱河造成了極大的恐慌,老刑警劉巖董习,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈和,死亡現場離奇詭異,居然都是意外死亡皿淋,警方通過查閱死者的電腦和手機斥杜,發(fā)現死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門虱颗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔗喂,你說我怎么就攤上這事「咛” “怎么了缰儿?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長散址。 經常有香客問我乖阵,道長,這世上最難降的妖魔是什么预麸? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任瞪浸,我火速辦了婚禮,結果婚禮上吏祸,老公的妹妹穿的比我還像新娘对蒲。我一直安慰自己,他們只是感情好贡翘,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布蹈矮。 她就那樣靜靜地躺著,像睡著了一般鸣驱。 火紅的嫁衣襯著肌膚如雪泛鸟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天踊东,我揣著相機與錄音北滥,去河邊找鬼。 笑死闸翅,一個胖子當著我的面吹牛再芋,可吹牛的內容都是我干的。 我是一名探鬼主播缎脾,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼祝闻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遗菠?” 一聲冷哼從身側響起联喘,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辙纬,沒想到半個月后豁遭,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡贺拣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年蓖谢,在試婚紗的時候發(fā)現自己被綠了捂蕴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡闪幽,死狀恐怖啥辨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情盯腌,我是刑警寧澤溉知,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站腕够,受9級特大地震影響级乍,放射性物質發(fā)生泄漏。R本人自食惡果不足惜帚湘,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一玫荣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧大诸,春花似錦捅厂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至建邓,卻和暖如春盈厘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背官边。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工沸手, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人注簿。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓契吉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诡渴。 傳聞我的和親對象是個殘疾皇子捐晶,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容