Android探索之路(三)—View的事件分發機制

前言

View作爲Android應用與用戶交互入口,除了展示視圖外,還承擔了處理用戶操作的任務,比如用戶的點擊、長按、滑動事件等。處理點擊事件的機制就是View的事件分發機制。

View的事件分發機制

當用戶點擊屏幕時,就會產生點擊事件,這個事件信息被封裝在一個類中,這個類就是MotionEvent。事件產生後Android系統會將事件傳遞到View的層級中,然後MotionEvent就會在View的層級中傳遞分發。

在View的分發機制中會設計到三個重要的方法,這三個方法承擔了View事件機制的處理任務。它們分別是:

  • dispatchTouchEvent(MotionEvent ev)—對事件進行分發。
  • onInterceptTouchEvent(MotionEvent ev)—用來攔截事件,在dispatchTouchEvent中調用,這個方法存在於ViewGroup中。
  • onTouchEvent(MotionEvent ev)—用來處理事件

View點擊事件的發生

當點擊事件發生後,事件首先會傳遞到當前的Activity中,這個過程調用了Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); //空方法,用於重寫回調
        }
        //調用當前Window的superDispatchTouchEvent方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //調用Activity的onTouchEvent方法
        return onTouchEvent(ev);
    }
//PhoneWindow中的方法
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
public boolean superDispatchTouchEvent(MotionEvent event) {
	//調用ViewGroup中的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }

可以看到當事件產生後,首先在當前Activity中會進行事件攔截,如果當前Window不攔截就會調用Activity的onTouchEvent方法。

同時,我們可以看到在PhoneWindow中會調用DecorView的superDispatchTouchEvent方法。這個方法又調用了dispatchTouchEvent方法。這是就開始了View層級的事件分發。

事件分發開始

從上面的代碼中可以看到。在View層級中,事件處理從ViewGroup的dispatchTouchEvent方法開始。我們開始從這裏分析。

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

            // Handle an initial down.
            //一個完整的事件從DOWN事件開始,UP事件結束
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 重置觸摸狀態,因爲程序可能由於切換、ANR或者某些其他狀態改變。框架已經刪除了up和cancel事件
                cancelAndClearTouchTargets(ev);
                //重置狀態
                resetTouchState();
            }

            // Check for interception.
            //檢查是否有攔截事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN  || mFirstTouchTarget != null) {
                //這個標誌也跟requestDisallowInterceptTouchEvent有關,通過此函數設置標誌可以另子View決定父容器是否攔截子View事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                	//調用onInterceptTouchEvent攔截事件
                    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;
            }
             //......
            // 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) {
                //不攔截事件,繼續分發事件
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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;

                   //......
                    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<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍歷子View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //獲取點擊範圍內的字View
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            //獲取touchTarget
                            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();
                    }
                    //......
                }
            }
            // 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 {
                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;
                        //父容器不攔截情況下,分發事件到對應的子View
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                       //......
                    predecessor = target;
                    target = next;
                }
            }
        //......
        return handled;
    }

從代碼中可以看到,事件分發的情況分爲兩種,一種是如果父容器不攔截事件,就把事件分發到對應的子View;另一種是父容器攔截事件,事件交由自己處理。在第一種情況下,ViewGroup會遍歷子View,判斷子View是否在點擊區域內,如果是就將事件交由子View分發。第二種情況下,ViewGroup攔截事件。這個兩種情況最終都會調用dispatchTransformedTouchEvent方法。接下來分析這個方法的作用。

//最終分發事件的方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
       //......省略部分代碼
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            //父容器攔截事件,調用View中的dispatchTouchEvent
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            //父容器不攔截事件,將事件分發到子View中
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

從代碼中可以看到,這個實現了剛纔ViewGroup中分發事件的兩種情況,父容器攔截以及不攔截。攔截的情況下child就爲null,這個時候調用View的dispatchTouchEvent方法。不攔截的情況下調用child的dispatchTouchEvent方法。我們再來分析下View中的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
        //.......此處省略部分代碼
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
    }

從代碼中可以看到,在View的dispatchTouchEvent方法中,如果OnTouchListener不爲null,就優先調用OnTouchListener的onTouch方法,並且會返回true,表示該事件被消耗。否則會調用onTouchEvent方法。在這裏我們只分析onTouchEvent方法。

public boolean onTouchEvent(MotionEvent event) {
        //......
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //如果View可以點擊,處理點擊事件
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                //處理UP事件
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if (!clickable) {
                       //取消長按事件
                        removeLongPressCallback();
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        //獲取焦點
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    //處理點擊事件
                                    performClick();
                                }
                            }
                        }
                    }
                    mIgnoreNextUpEvent = false;
                    break;
		        //處理DOWN事件
                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    //......
                    break;
                case MotionEvent.ACTION_MOVE:
                    //......
                    break;
            }
            return true;
        }
        return false;
    }

從代碼中可以看到,onTouchEvent處理了分發過來的事件。事件類型有ACTION_UP、ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL。onTouchEvent處理事件的前提是View是可點擊的。其中當View註冊了OnCLickListener和onLongClickLinster即爲可點擊的。可以看到處理點擊事件是在ACTION_UP中處理的,通過調用perfromClick方法實現,當調用點擊事件時,說明長按事件未到達長按的時間。而長按事件是在ACTION_DOWN中實現的,通過checkForLongClick方法發送延遲消息,當達到長按時間時就調用長按事件。

事件分發的原理

經過上面的分析,現在總結一下View事件分發的原理。事件的開始是從Activity到PhoneWindow中,最後經由View層級。在View的層級中從頂級View(DecorView)分發。

  1. 當點擊事件產生後,有頂層的ViewGroup分發事件。
  2. 通過調用dispatchTouchEvent方法,當父容器攔截事件時就調用View的dispatchTouchEvent方法,進而調用onTouchEvent方法或者OnTouchListener的onTouch方法。
  3. 否則,調用子View的dispatchTouchEvent方法。如果子View是ViewGroup類型,則繼續按照步驟1分發事件。否則調用View的dispatchTouchEvent方法。

總結

View的事件分發機制,處理了用戶通過觸摸屏幕產生的事件。一般來說通過View的事件分發,我們經常需要處理的有DOWN、MOVE、UP事件。通過實現這些類型的事件,就可以實現不同的交互操作,進而豐富View與用戶的交互體驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章