Android下Touch事件的分發機制

pgl198 8年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

我們通過一個示例來分析Touch事件的分發過程。

示例:

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.maimingliang.test.view.TestTouchActivity">



    <TextView
        android:id="@+id/txt"
        android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="55dp"
        android:text="textView"/>


    <ImageView
        android:id="@+id/img"
        android:layout_marginTop="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/ic_launcher"/>


</LinearLayout>

Activity:

public class TestTouchActivity extends AppCompatActivity {

    private static final String TAG = "TestTouchActivity";
    @Bind(R.id.txt)
    TextView tv;
    @Bind(R.id.img)
    ImageView img;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_touch);
        ButterKnife.bind(this);
        initView();
    }

    private void initView() {

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG,"-------> tv Onclick");
            }
        });

        tv.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                Log.d(TAG, "-------> tv onTouch");
                return false;
            }
        });


        img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "--------> img onClick");
            }
        });

        img.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "--------> img onTouch");

                return true;
            }
        });
    }

點擊圖片,現象

這里寫圖片描述

可以看到onTouch事件比onClick事件優先級高。

再看看把setOnTouchListener事件的返回值改為true:

這里寫圖片描述

可以看到onClick事件沒有了。這是為什么?我們透過源碼來看看這個現象。

事件分發機制源碼分析

當我們觸摸屏幕上的某個控件時,底層的設備硬件傳遞給InputManager經過一 定的處理后,傳遞給AmS,再經過AmS的處理后就傳遞到我們的Activity,接著傳遞Window,最后傳遞到頂級View。

觸摸事件的分發過程有三個重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)

用來分發事件的,如果當前事件能傳遞到該View,該 方法一定調用,View的onTouchEvent方法會調用,而該方法的返回值所onTouchEvent影響。

public boolean onInterceptHoverEvent(MotionEvent event)

用來攔截事件的,如果返回值為true,表示攔截。否則不攔截。

public boolean onTouchEvent(MotionEvent event)

處理當前事件的。如果返回值為true表示消耗該事件。否則無法再接收同一個序列的事件。

同一個序列的事件是;DOWN事件--》多個MOVE事件--》UP事件。

Activity觸摸事件分發過程

當觸摸事件傳遞到Activity,Activity的dispatchTouchEvent()方法就會調用,我們去看看:

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

如果當前事件是DOWN事件,調用了onUserInteraction方法,該方法是一個空方法,我們可以重載該方法,在DOWN事件做一些處理。接著就把事件傳遞給Window來處理該事件。如果返回true,表示有View處理該事件,onTouch Event()方法返回了true,整個事件處理完成。否則Activity的onTouchEvent方法就會被調用。

Window觸摸事件的分發過程

Window類是abstract的,唯一的具體實現類是PhoneWindow類,我們去看看PhoneWindow的superDispatchTouchEvent()方法:

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

    private DecorView mDecor;

DecorView類繼承于FrameLayout:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {....}

因此就是調用了DecorView的superDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

可以看到,其實就是調用了父類的dispatchTouchEvent()方法。DecorView繼承于FrameLayout,FrameLayout繼承于ViewGroup。因此就是調用了ViewGroup的dispatchTouchEvent()方法。

DecorView就是我們的頂層View,當我們通過setContentView()方法設置的是頂層View的一個子View。DecorView組成為:

這里寫圖片描述

可以看出,事件傳遞的大概過程:

Activity--》Window--》View。某個View的onTouchEvent()方法被調用。如果返回true,傳遞會Window,Window再傳遞會Activity,事件處理結束。否則返回false,再同樣的傳遞會Activity。

頂層View事件分發的過程

DecorView繼承與FrameLayout,是一個ViewGroup,ViewGroup繼承于View,繼承圖:

這里寫圖片描述

ViewGroup重載了dispatchTouchEvent()方法。那我們去看看該方法:

@Override
   public boolean dispatchTouchEvent(MotionEvent ev) {

        ....

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

            1.
            // 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();
            }

            2.
            // 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;
            }


            ....

            3.
            if (!canceled && !intercepted) {

                ....


                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        ....

                        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)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }



                            ....


                            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;
                            }

                           ....

                        }
                    }

                    ....

                }
            }

            ....



        return handled;
    }

這個方法很長我們分幾部分來分析。代碼中標有1.2.3.....。

1.ViewGroup對DOWN事件重置狀態的操作。

private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

標志FLAG_DISALLOW_INTERCEPT可以通過requestDisallowInterceptTouchEvent方法設置。因此在DOWN事件該方法不影響該標志,簡單來說,就是不影響ViewGroup處理DOWN事件的操作。

2.判斷是否攔截事件。

首先判斷是否DOWN事件或者mFirstTouchTarget != null。

mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功處理,mFirstTouchTarget就會指向該元素。

如果當前事件是DOWN:FLAG_DISALLOW_INTERCEPT不影響ViewGroup對DOWN事件的處理,因此調用了onInterceptTouchEvent()方法。是否攔截取決于該方法的返回值。

如果onInterceptTouchEvent()返回true,說明ViewGroup攔截事件,mFirstTouchTarget為null,同一序列的事件都由它處理,onInterceptTouchEvent也不會再調用了,因為actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null條件都不滿足。如果子 View調用了requestDisallowInterceptTouchEvent()方法后,ViewGroup將無法攔截除DOWN事件以外的其他事件。該方法不影響ViewGroup的DOWN事件。

3.如果ViewGroup不攔截,ViewGroup遍歷所有的子View,判斷子View是否滿足當前的事件。滿足的條件有兩個:子View是否播放動畫和事件的坐標是否在子View的區域。

如果滿足條件,調用了dispatchTransformedTouchEvent()方法。去看看:

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

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        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;
        }

    ....

}

其實就是調用了子View的dispatchTouchEvent()方法。如果返回了true,就會通過addTouchTarget()方法對mFirstTouchTarget賦值并停止遍歷子View。

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到,mFirstTouchTarget是一個單鏈表的數據結構。

如果遍歷全部的子View都沒有成功處理的,mFirstTouchTarget成員變量為null,當該成員變量為null,就會調用:

if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }

因為第三個參數為null,就會調用super.dispatchTouchEvent()方法,調用到了View的dispatchTouchEvent()方法。

View的事件分發過程

dispatchTouchEvent方法如下:

/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        ....

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

    ....

        return result;
    }

從上面的代碼可以看出,判斷了是否設置了setOnTouchListener,是否為ENABLED,onTouch是否返回了true。

ENABLED對這個判斷沒有影響。

但onTouch返回true,onTouchEvent方法就不會執行了。而onClick的方法是在onTouchEvent()方法執行的。因此onTouch事件的優先級比onClick事件高,而且還當onTouch方法返回了true,onClick事件就不會調用了。說明了上面的示例的現象。

我們去看看onClick事件是否在onTouch Event方法中執行的。

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

      .....
      .....
            }

            return true;
        }

        return false;
    }

從上述代碼看到,判斷了viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE判斷是否可點擊或者長點擊。只要有一個為true,就會返回true,表示消耗此事件。

CLICKABLE和LONG_CLICKABLE的值可以在清單文件中通過android:clickable和 android:longClickable屬性設置,也可以通過setOnclickListener()和setLongClickListener()方法設置。

當設置了點擊事件調用了performClick()方法:

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可以看到回調了我們設置的onClick方法。由此看出onClick事件是在onTouch Event方法執行的。

這就是事件分發的大概流程。

我們根據上面的示例走一下整個觸摸事件的分發流程。

我們從頂View開始分析:

整個View樹的結構如下:

這里寫圖片描述

上面的示例,我們點擊的圖片。

首先由頂層View(FrameLayout)的dispatchTouch()方法根據點擊圖片等坐標首先分發到第一個LinearLayout的,然后調用了ViewGroup的dispatchTouch()方法,又根據點擊圖片等坐標:u6709:?分發到了第二個LinearLayout,接著有調用了ViewGroup的dispatchTouch()方法,又根據點擊圖片的坐標分發到了ImageView,然后調用了View的dispatchTouch()方法。ImageView設置setOnTouchListener方法和setOnclickListener方法,如果setOnTouchListener方法返回了false,接著調用了onTouchEvent()方法,從而onClick方法調用,onTouchEvent返回true,消耗了此事件。否則dispatchTouch()方法直接返回了true,消耗此事件。

ViewGroup的onInterceptTouchEvent()方法默認返回false,默認不攔截。

END.

 

來自:http://www.jianshu.com/p/1e2d439487f0

 

 本文由用戶 pgl198 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!