Android View相關-事件分發機制詳解-View

這篇文章我們來探究下Android中關於事件分發機制的一些細節和流程,由於這部分源碼比較繁雜,拆開來講,本文只探究View的事件分發流程,ViewGroup留到之後再說,在分析完這兩者的事件分發機制之後我們來對Android的時間分發機制進行總結。那麼本文就從View的子類Button來着手分析事件分發的流程,之後我們再從源碼角度分析具體實現過程。

舉個栗子

這裏用一個很簡單的小例子來演示View中dispatchTouchEvent、onTouchEvent、TouchListener的執行順序,繼承自Button的TestButton代碼很簡單,幾個log:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, "onTouchEvent ACTION_DOWN");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG, "onTouchEvent ACTION_MOVE");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG, "onTouchEvent ACTION_UP");
            break;
        default:
            break;
    }
    return super.onTouchEvent(event);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG, "dispatchTouchEvent ACTION_UP");
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

Activity中設置onTouchListener,也是幾個log:

testView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouch ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouch ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouch ACTION_UP");
                break;
            default:
                break;
        }
        return false;
    }
});

接下來是執行流程,第一次點擊按鈕:

第二次點擊後鼠標移動一下鬆開:

可以看到一次事件分發從ACTION_DOWN開始,到ACTION_UP結束,且其傳遞順序是從dispatchTouchEvent –> onTouch –> onTouchEvent。下面我們來從源碼中對這幾個方法進行查看。

源碼分析

我們先從View的dispatchTouchEvent開始吧:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    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;
}

我這裏略去了部分代碼,來看核心代碼,可以看到第10行開始這裏開始進行了判斷,若同時附和這幾個條件,則返回true,注意這裏調用了OnTouchListener的onTouch方法,也就是說如果我們調用了View的setOnTouchListener方法那麼在dispatchTouchEvent方法執行過程中,會調用OnTouchListener的onTouch方法,若onTouch方法返回true,則設置result爲true,onTouchEvent不再執行,若onTouch方法返回爲false,則第十行if語句不成立,result爲改變,執行下一個判斷語句,同時會執行onTouchEvent,若返回true則result爲true,反之亦然。

我們繼續來看一看onTouchEvent中的代碼:

public boolean onTouchEvent(MotionEvent event) {
    //View狀態爲Disable並且可點擊,返回true
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        return clickable;
    }
    //若TouchDelegate(觸摸代理類)不爲空,則調用其onTouchEvent方法並返回true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //若可點擊或者可長按以及長按出現ToolTip
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                break;
            case MotionEvent.ACTION_CANCEL:
                ...
                break;
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}

這段代碼極長,有興趣的朋友可以通讀一下源碼,我這裏略去了一部分,前兩個判斷已經在註釋裏寫清楚了,不多贅述,我們重要看下switch語句中的內容,接下來一個一個分析:

ACTION_DOWN

ACTION_DOWN是整個Touch流程的起點,代表觸摸點按下操作。我們來看看onTouchEvent中的判斷是怎樣的:

case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;
    //判斷是否爲鼠標右鍵或者手寫筆第一個按鈕,若是,返回true後續代碼不執行
    if (performButtonActionOnTouchDown(event)) {
        break;
    }
    //當前視圖是否可滾動(例如:當前是ScrollView視圖,返回true)
    boolean isInScrollingContainer = isInScrollingContainer();

    if (isInScrollingContainer) {
        // 滾動視圖內,先不設置爲按下狀態,因爲用戶之後可能是滾動操作
        // 不是此次分析的重點,感興趣可以自己瞭解下
        mPrivateFlags |= PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
        // 不在滾動視圖內,立即反饋爲按下狀態
        mPrivateFlags |= PRESSED;
        // 刷新爲按下狀態
        refreshDrawableState();
        //通過Handler發送一個延遲消息來判斷是否是長按,500ms
        checkForLongClick(0);
    }
break;

各個步驟的註釋已經寫清楚了,下面分解一下各個方法:

首先是performButtonActionOnTouchDown

protected boolean performButtonActionOnTouchDown(MotionEvent event) {
    // 如果是鼠標右鍵,手寫筆第一個按鈕,看BUTTON_SECONDARY註釋
    if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
        if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
            return true;
        }
    }
    return false;
}

很簡單的判斷,如果成功就返回true,不成功就false。

接下來是isInScrollingContainer

public boolean isInScrollingContainer() {
    ViewParent p = getParent();
    while (p != null && p instanceof ViewGroup) {
        if (((ViewGroup) p).shouldDelayChildPressedState()) {
            return true;
        }
        p = p.getParent();
    }
    return false;
}

這裏獲取到了當前View的父控件,而後一層一層向上判斷是否處於滾動容器中(shouldDelayChildPressedState返回true),如果是,則返回true

接下來是checkForLongClick

private void checkForLongClick(int delayOffset) {
    // 當前視圖可以執行長按操作
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        // 延遲一段時間(500ms)把runnable添加到消息隊列
        postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

ACTION_DOWN中的邏輯大概就是這樣

ACTION_MOVE

ACTION_MOVE代表觸摸點發生滑動

case MotionEvent.ACTION_MOVE:
    //這個方法暫時不用關注
    if (clickable) {
        drawableHotspotChanged(x, y);
    }

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, mTouchSlop)) {
        // Outside button
        // Remove any future long press/tap checks
        removeTapCallback();
        removeLongPressCallback();
        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }
break;

我們先來看看pointInView方法裏做了什麼:

public boolean pointInView(float localX, float localY, float slop) {
    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
        localY < ((mBottom - mTop) + slop);
}

這個方法是判斷是否劃出控件可視區域,爲了保證觸摸點發生及輕微變化就導致ACTION_MOVE被執行,這裏加入了一個slop的邊界值,即在視圖上下左右擴大slop

接下來是兩個remove方法,這裏我們放在一起講:

private void removeTapCallback() {
    if (mPendingCheckForTap != null) {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        removeCallbacks(mPendingCheckForTap);
    }
}
private void removeLongPressCallback() {
    if (mPendingCheckForLongPress != null) {
        removeCallbacks(mPendingCheckForLongPress);
    }
}

這兩個方法主要是刪除觸摸和長按回調(還記得之前按下後發送的長按延時消息嗎)

ACTION_CANCLE

ACTION_CANCLE代表取消觸摸操作,觸摸流程結束

if (clickable) {
    setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

這裏代碼很簡單,清除View狀態

ACTION_UP

ACTION_UP代表觸摸點擡起操作,同樣是一個觸摸流程的結束

case MotionEvent.ACTION_UP:    
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    if ((viewFlags & TOOLTIP) == TOOLTIP) {
        handleTooltipUp();//popWindow之類相關的,不必關心
    }
    //清除觸摸狀態以及觸摸回調和長按的回調
    if (!clickable) {
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        break;
    }
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        // 當前視圖處於預按下或者按下狀態,如果失去焦點,獲取焦點狀態
        boolean focusTaken = false;
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
            focusTaken = requestFocus();
        }

        if (prepressed) {
            //重設按下狀態
            setPressed(true, x, y);
        }
        //長按未觸發
        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            //移除長按回調
            removeLongPressCallback();

            //按下狀態執行點擊事件
            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)) {
            //執行失敗的話,保證視圖不會永遠處於按下狀態
            //直接執行一次
            mUnsetPressedState.run();
        }
        // 清除輕觸回調
        removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
break;

這短代碼是觸摸流程裏最重要的一部分。代碼中已經註釋清除,不必全部理解,理解流程即可。

到此,我們整個View的觸摸流程就結束了,光看這部分還是很繞的,下一篇文章我們會詳細講講ViewGroup中事件分發流程,並且和本篇做對照,可能會理解的更透徹。

enjoy~

我的個人博客

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