Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Touch事件傳遞機制詳解 上

Android Touch事件傳遞機制詳解 上

編輯:關於Android編程

 

最近總是遇到關於Android Touch事件的問題,如:滑動沖突的問題,以前也花時間學習過Android Touch事件的傳遞機制,可以每次用起來的時候總是忘記了,索性自己總結一下寫篇文章避免以後忘記了,其實網上關於Touch事件的傳遞的文章真的很多,但是很少有系統性的,都是寫了一個簡單的demo運行了一下,對於我們了解Android Touch事件基本上沒有任何幫助。

今天我打算從源碼的角度來分析一下Touch事件的傳遞機制。在了解Touch事件之前,最好了解下Android中窗口的創建過程,這樣對於Android窗口的整體結構和事件的傳遞過程會了解更深。

我就把事件的始點定在PhoneWindow中的DecorView吧,至於是誰把事件傳遞給DecorView的我們先不用去關心它。(如果想深入研究,請閱讀我的另外一篇文章Android中按鍵事件傳遞機制)我們只需要知道它的上家是通過dispatchTouchEvent方法將事件分發給DecorView就行了,我進入到該方法瞧瞧究竟。

在閱讀之前最好閱讀Android窗口創建過程

 

@Override
        public boolean dispatchTouchEvent(MotionEvent ev) 
	{
	    //該Callback就是該DecorView附屬的Activity,可以看我的另外一篇文章《Android中窗口的創建過程》
            final Callback cb = getCallback();
	    //如果cb!=null && mFeatureId<0 就執行Activity中的dispatchTouchEvent方法,對於應用程序窗口 			    //這兩個條件一般是滿足的
            return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
                    .dispatchTouchEvent(ev);
        }
在DecorView中事件通過dispatchTouchEvent方法被分發到了Activity中,相信Activity對於每個Android開發者都不會陌生吧,那我們就進入Activity的dispatchTouchEvent方法中。

 

 

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
	//getWindow返回什麼?如果閱讀過我的《Android中窗口創建過程》的都知道就是PhoneWindow,如果PhoneWindow中的superDispatchTouchEvent方法返回了true,
	//那麼該Touch事件就被PhoneWindow給消費掉了,不會再繼續傳遞,如果返回false,那麼就會執行Activity的onTouchEvent方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
進入PhoneWindow中的superDispatchTouchEvent方法:

 

 

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
		//mDecor是一個DecorView類型變量
        return mDecor.superDispatchTouchEvent(event);
    }

進入DecorView中的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
			//直接調用父類的dispatchTouchEvent方法
            return super.dispatchTouchEvent(event);
        }

走到這裡我們先暫停一下,會看一下DecorView類的dispatchTouchEvent方法,如果callBack不為空,那麼調用CallBack的dispatchTouchEvent方法,否則調用super.dispatchTouchEvent方法,但是在CallBack不為空的條件下最中也是調用到了super.dispatchTouchEvent方法,那麼它的super是哪個那,我們繼續往下看:
通過源碼我們可以看到DecorView是繼承自FrameLayout。所以事件最終是傳遞到了FrameLayout的dispatchTouchEvent中,FrameLayout中的此方法是繼承自ViewGroup的,我們直接到ViewGroup中查看此方法吧:

 

 

 

@Override
    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;
		//可以通過requestDisallowInterceptTouchEvent方法來設置該變量的值,通常是false
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
			//onInterceptTouchEvent在默認情況下是返回false的,所以這裡通常是可以進去的
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
				//遍歷ViewGroup的孩子,如果觸摸點在某一個子View中,則調用在子View的dispatchTouchEvent
                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)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
				//調用了某一個子View 的dispatchTouchEvent ,如果這個子View 的dispatchTouchEvent返回true,那麼意味著這個事件
				//已經被這個子View消費了,不會繼續傳遞
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
		//對於一個Action_down事件,如果走到了這裡,說明所有的子View 都沒有消費掉這個事件,那麼它就調用父類的
		//的dispatchTouchEvnet方法,ViewGroup的父類就是View
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            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 have a target, see if we're allowed to and want to intercept its
        // events
        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)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        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);
    }

剛才在看ViewGroup的dispatchTouchEvent方法時,我們看到了一個方法onInterceptTouchEvent,這個方法是干什麼的呢,我們先看看他都干了什麼吧

 

 

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

發現裡面就是返回了一個false, 通過方法名字我們就可以知道該方法的作用,是否阻止TouchEvent的傳遞,默認是false 也就是不會阻止。

現在總結一下ViewGroup的dispatchTouchEvnet的邏輯 ,畢竟這個方法有些復雜:
1、如果disallowIntercept|| !onInterceptTouchEvent(),那麼事件才可以繼續傳遞下去,否則直接調用該ViewGroup的父類的dispatchTouchEvent,也就是View的dispatchTouchEvent.
2、依次遍歷ViewGroup的所有子View,將事件傳遞個子View,如果某一個子View處理了該事件,並且返回true,那麼事件結束,停止傳遞
3、如果所有的子View沒有消費掉這個事件,那麼就調用View的dispatchTouchEvent

 

對於任何一款Android應用,展現給用戶最上面的通常就是一個View,如Button,ImageView等等,也就是說一些觸摸事件最終都是傳遞給了這個控件,如果控件消費了這些事件,那麼就停止傳遞了,如果沒有消費,那麼就交給控件所屬ViewGroup的onTouchEvnet處理,我們就看看View的dispatchTouchEvent方法吧

 

public boolean dispatchTouchEvent(MotionEvent event) {
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

View的這個方法非常簡單,首先判斷mTouchListener是否為空,並且這個View是否Eneable,如果都滿足,那麼首先調用mOnTouchListener.onTouch方法,如果onTouch方法返回true,那麼就是說這個View消費了該事件,直接返回true,如果onTouch返回false,那麼就會調用onTouchEvnet方法,這個mOnTouchListener是什麼?

 

 

 public void setOnTouchListener(OnTouchListener l) {
        mOnTouchListener = l;
    }

看了這個就明白了吧,就是我們通過setOnTouchListener賦值的,另外我們還需要注意一點就是這個onTouch是在onTouchEvent方法之前執行的哦。
最後我們就看看這個View的onTouchEvnet吧

 

 

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
		//(A)
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
		//(B)
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
                            // 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();
                                }
								//(C)
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

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

                        if (prepressed) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
			//(D)
            return true;
        }

        return false;
    }

這個方法也是相當的復雜啊,但是我們沒有必要每一行都看,我們只需要挑重點看就Ok了。
請細看我標了 A B C D的四個地方,在A處,如果該View是Disable的,那麼只要該View是clickable或者longclickable的,那麼這個事件就被該View消費掉了,返回true
在看B 和 D,兩處,如果該View是clickable或者longclickable的,那麼D出總是返回true,也就是說事件一直被消費,至於C處我主要是說明的是View的onClick事件是在ACTION_UP中觸發的。

學習到這裡,我又需要總結一下:
如果我們觸摸的一個View是clickable或者longclickable的,那麼這個事件肯定會被這個View消費掉(當然前提是你沒有改寫它所在ViewGroup的onInterceptTouchEvent方法,如果你改寫此方法返回true,那麼View是無法接收到這個事件的)

我們現在還要思考一個問題,如果這個View沒有消費掉這個事件,這個事件最終拋向何方?
還記得前面我說過ViewGroup的dispatchTouchEvent方法嗎,如果它的所有的子View沒有處理掉該事件,那麼調用的是父類View的dispatchTouchEvnet方法,從而執行到了該ViewGroup的onTouch和onTouchEvent方法。

那如果ViewGroup也沒有處理該事件呢,這裡就要分兩種情況啦:
1、如果這個ViewGroup不是DecorView,也就是說他的父View就是一個普通的ViewGroup(如LinearLayout裡面放置一個LinearLayout),那麼和上面子View沒有處理掉消息有點類似,調用父類的onTouch和onTouchEvent方法
2、如果這個ViewGroup就是DecorView,那麼就調用到了Activity的onTouchEvnet方法(此時沒有onTouch方法)。

今天就先寫到這裡吧,後面我回用一個簡單的Demo和一個簡單的滑動沖突問題在深入學習TouchEvnet事件的。如果哪裡沒有寫清楚的 ,歡迎拍磚。。。


 

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