Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android源碼解析ViewGroup的touch事件分發機制

Android源碼解析ViewGroup的touch事件分發機制

編輯:關於Android編程

概述

本篇是繼上一篇Android 源碼解析View的touch事件分發機制之後的,關於ViewGroup事件分發機制的學習。同樣的,將采用案例結合源碼的方式來進行分析。

前言

在分析ViewGroup事件分發機制之前,我們也需要學習一下基本的知識點,以便後面的理解。
ViewGroup中有三個關鍵的方法參與事件的分發
dispatchTouchEvent(MotionEvent event),onInterceptTouchEvent(MotionEvent event),和onTouchEvent(MotionEvent event)。
所有Touch事件類型都被封裝在對象MotionEvent中,包括ACTION_DOWN,ACTION_MOVE,ACTION_UP等等。
每個執行動作必須執行完一個完整的流程,再繼續進行下一個動作。比如:ACTION_DOWN事件發生時,必須等這個事件的分發流程執行完(包括該事件被提前消費),才會繼續執行ACTION_MOVE或者ACTION_UP的事件。

案例分析

同上篇所介紹的一樣,這次我們選擇繼承一個布局類,然後重寫上面的三個方法,便於來觀察ViewGroup的事件分發流程。
上代碼:

package com.yuminfeng.touch;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

public class MyLayout extends LinearLayout {

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i("yumf", "MyLayout=====dispatchTouchEvent ACTION_DOWN");
            break;

        case MotionEvent.ACTION_UP:
            Log.i("yumf", "MyLayout=====dispatchTouchEvent ACTION_UP");
            break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i("yumf", "MyLayout=====onInterceptTouchEvent ACTION_DOWN");
            break;

        case MotionEvent.ACTION_UP:
            Log.i("yumf", "MyLayout=====onInterceptTouchEvent ACTION_UP");
            break;
        }
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i("yumf", "MyLayout=====onTouchEvent ACTION_DOWN");
            break;

        case MotionEvent.ACTION_UP:
            Log.i("yumf", "MyLayout=====onTouchEvent ACTION_UP");
            break;
        }
        return super.onTouchEvent(event);
    }
}

在布局文件中,引用如下:



    

如上,我通過繼承LinearLayout來代表ViewGroup,並重寫了參與事件分發的三個重要的方法。關於MyButton我采用上篇文章中一樣的代碼並沒有做修改,這裡就不列出了。
同樣的,在執行完上面的代碼後,我們可以得到下面的打印日志:
這裡寫圖片描述
由此可以知道事件的流程為:
MyLayout的dispatchTouchEvent ->MyLayout的onInterceptTouchEvent ->MyButton的dispatchTouchEvent -> MyButton的onTouchEvent。該流程最後執行了MyButton的onTouchEvent方法,表示該事件由MyButton消費。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPtXiyrHO0sPHwLTQ3rjEy/zDx7XEt7W72Na1o6yy6b+0ysK8/rXEwfezzMfpv/ahozxiciAvPg0KyejWw015TGF5b3V0tcRkaXNwYXRjaFRvdWNoRXZlbnQgzqp0cnVlo7o8YnIgLz4NCjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160625/20160625095410554.png" title="\" />
由此可以看到,該觸摸事件不進行調度處理,它的子View無法獲得事件。
設置MyLayout的onInterceptTouchEvent 為true:
這裡寫圖片描述
從這裡可以看到,onInterceptTouchEvent為true時,表示直接攔截事件,後續action無法繼續調度,子View無法獲得事件,該事件由MyLayout自己消費。
設置MyButton的dispatchTouchEvent 為true:
這裡寫圖片描述
可以看到這裡設置MyButton的dispatchTouchEvent 為true時,事件流程到了MyButton的dispatchTouchEvent就截止了,沒有繼續下去。因為MyButton停止了事件的調度,MyButton無法消費事件。
設置MyButton的onTouchEvent 為false(默認為ture,表示消費事件):
這裡寫圖片描述
可以看到MyButton的onTouchEvent 為false時,表示MyButton不消費該事件,將會上傳給MyLayout的onTouchEvent方法。由MyLayout消費該事件。
我們可以畫一個事件流程圖,如下:
這裡寫圖片描述

總結:
在Activity中,當Touch一個控件時,最先收到Touch事件的是這個View的父布局容器ViewGroup,由ViewGroup一步步層層遞進向內部的View或ViewGroup分發事件。期間如果攔截事件的話,即調用ViewGroup的onInterceptTouchEvent方法返回true,那麼這個ViewGroup中的子View無法獲得該事件,該事件由ViewGroup調用onTouchEvent方法消費。此方式可稱之為隧道式分發。
當View已經獲得事件的分發後,如果在View的onTouchEvent中返回false時,表示該View不對事件進行消費。那麼該事件會繼續分發到View的直接父布局中,由父布局容器,即ViewGroup的onTouchEvent方法處理該事件。如果該ViewGroup的onTouchEvent方法也是返回false,那麼事件繼續向該ViewGroup的直接父布局傳遞,如果存在的話。一直分發到onTouchEvent返回為true的ViewGroup,然後由該ViewGroup消費這個事件,結束為止。這就是所謂的冒泡式消費。

源碼閱讀

根據事件分發的流程,我們先分析ViewGroup的dispatchTouchEvent方法。如下:

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // Check for cancelation.
            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) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        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);
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                        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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            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);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

代碼比較多,我們可以抓住重點來分析,根據安全策略過濾Touch事件後,進入執行體中:
1.actionMasked == MotionEvent.ACTION_DOWN 時,首先執行方法cancelAndClearTouchTargets和resetTouchState。處理一個初始化的down事件,當開始一個新的touch手勢時,去掉之前所有的狀態。我們先看一下cancelAndClearTouchTargets方法:

    /**
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }

在for循環中,開始遍歷Touch的ViewGroup中的子View,設置view的mPrivateFlags狀態為不包括PFLAG_CANCEL_NEXT_UP_EVENT。在方法dispatchTransformedTouchEvent中,計算touch的x,y是否在View上,如果在,執行child.dispatchTouchEvent(event)。接著清除所有的touch targets,設置mFirstTouchTarget = null;

2.檢查攔截狀態,根據mGroupFlags是否包含FLAG_DISALLOW_INTERCEPT狀態,即是否不允許攔截,如果可以攔截則執行方法onInterceptTouchEvent來返回一個false。如果mFirstTouchTarget == null時,直接設置攔截狀態intercepted = true。

3.如果mPrivateFlags中包含PFLAG_CANCEL_NEXT_UP_EVENT或者沒有被攔截時,那麼開始遍歷子View設置newTouchTarget為子view,把事件分發下去。

關於攔截的方法onInterceptTouchEvent

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

默認不攔截,可以重寫該方法進行攔截。
以上便是對ViewGroup中有關事件分發進行簡單分析。
我們可以對其進行一些總結:
一般情況下,ViewGroup通過調度Touch事件,通過遍歷找到能夠處理該事件的子View。
通過重寫onInterceptTouchEvent,可以攔截子View獲得touch事件。這時會調用ViewGroup的onTouchEvent方法。
子View也可以通過調用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對其MOVE或者UP事件進行攔截;

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved