結(jié)合源碼,重溫 Android View 的事件處理知多少 ?

前言

  • Android View 的 事件處理在我們的編程中,可謂是無(wú)處不在了。但對(duì)于大多數(shù)人而言,一直都是簡(jiǎn)單的使用,對(duì)其原理缺乏深入地認(rèn)識(shí)。
  • 學(xué) Android 有一段時(shí)間了,最近發(fā)現(xiàn),很多基礎(chǔ)知識(shí)開始有些遺忘了,所以從新復(fù)習(xí)了 View 的事件分發(fā)。特地整理成了這篇文章分享給大家。
  • 本文不難,可以作為大家茶余飯后的休閑。

祝大家閱讀愉快!

Android View 的事件處理

方便大家學(xué)習(xí),我在 GitHub 上建立個(gè) 倉(cāng)庫(kù)


一、View 的事件回調(diào)

  • 我們結(jié)合源碼看看 View 的事件分發(fā)是個(gè)怎樣的過(guò)程,首先我們建立一個(gè)類 MyButton 類繼承 AppCompatButton 用于測(cè)試:
public class MyButton extends AppCompatButton {

    private final String TAG = "DeBugMyButton";
        public MyButton(Context context) {
        super(context);
    }

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

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

}

1.1 事件分發(fā)流程

  • 我們都知道有一個(gè)方法叫做 public boolean dispatchTouchEvent(MotionEvent event) 。首先我們要知道,對(duì)于我們這個(gè)自定義控件,他的觸摸事件都是從我們 dispatchTouchEvent 這個(gè)方法開始往下去分發(fā)的。所以可以說(shuō):這個(gè)方法是一個(gè)入口方法。

1.1.1 onTouchEvent 作用

  • 現(xiàn)在我們重寫該方法和另一個(gè)方法:onTouchEvent ,并且打印一行日志:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.d(TAG, "----on dispatch Touch Event----");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "----on touch event----");
    }
    return super.onTouchEvent(event);
}
  • 然后我們?cè)?MainActivity 中,設(shè)置一個(gè)實(shí)例化一個(gè) MyButton 控件對(duì)象用于測(cè)試,并且給他添加一個(gè) onClickListentersetOnTouchListener
public class MainActivity extends AppCompatActivity {

    private final String TAG = "DeBugMainActivity";

    /**
     * 自定義控件 MyButton
     */
    private MyButton mMyButton;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        iniView();
    }

    /**
     * 實(shí)例化控件
     */
    private void iniView() {
        mMyButton = findViewById(R.id.my_button);

    mMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.d(TAG, "----on touch----");
                    break;
                default:
                    break;
            }
            return false;
        }
    });
    
    mMyButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG, "----on click----");
        }
    });
    }
}
  • 然后我們運(yùn)行這個(gè) Demo ,點(diǎn)擊 MyButton 按鈕,會(huì)的到如下日志:
image
  • 我們可以看到首先回調(diào)了這個(gè) dispatchTouchEvent ,然后是它的監(jiān)聽(tīng)器 OnTouch ,接著是它的 onTouchEvent,最后又執(zhí)行了 dispatchTouchEvent ,那么這是為什么呢?

  • 這是因?yàn)槲覀冞@兒只監(jiān)聽(tīng)了 ACTION_DOWN 而當(dāng)手指抬起時(shí)它同樣還回去回調(diào) dispatchTouchEvent ,最后我們打印 OnClick 的回調(diào)。

  • 總結(jié)一下就是:
    dispatchTouchEvent -> setOnTouchListener -> onTouchEvent -> setOnClickListener

  • 說(shuō)明我們 setOnClickListener 是通過(guò) onTouchEvent 處理,產(chǎn)生了 OnClick 。一會(huì)我們?cè)賮?lái)看看其中的原理。

  • 既然說(shuō) dispatchTouchEvent 像一個(gè)入口,就先讓我們來(lái)看下它是怎么處理和操作的: 首先,既然我們調(diào)用了 super.dispatchTouchEvent(event) ,那么我們就來(lái)看看它父類中是怎么實(shí)現(xiàn)該方法的。不信的是,它的父類 AppCompatButton 也沒(méi)有實(shí)現(xiàn)該方法 ,最后經(jīng)過(guò)層層搜尋,我們發(fā)現(xiàn)這個(gè)方法是屬于 View 的方法。

1.1.2 dispatchTouchEvent 的實(shí)現(xiàn)

  • 那么現(xiàn)在我們來(lái)看看 ViewdispatchTouchEvent 怎么實(shí)現(xiàn)的:
public boolean dispatchTouchEvent(MotionEvent 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;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}
  • dispatchTouchEvent 中,我們可以發(fā)現(xiàn)下面這樣一個(gè)代碼塊
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
  • 不難看出:如果執(zhí)行了這個(gè)代碼段,那么后面的方法就不會(huì)執(zhí)行了,并且 dispatchTouchEvent 會(huì)返回 true 。我們?cè)僮屑?xì)觀察下其中的條件:在 if 條件中我們發(fā)現(xiàn):只有當(dāng)其滿足 li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) 時(shí)才會(huì)執(zhí)行 if 內(nèi)的操作

  • 經(jīng)過(guò)上面分析,我們可以知道: onTouch 事件必須返回 true 時(shí),才會(huì)執(zhí)行該方法塊。那么我們就回到 MainActivity 中。我們發(fā)現(xiàn) setOnTouchListeneronTouch 默認(rèn)返回值是 false( 不滿足返回值為 true ), 這就表明他會(huì)繼續(xù)去執(zhí)行下一個(gè)代碼塊:

if (!result && onTouchEvent(event)) {
    result = true;
}
  • 執(zhí)行這個(gè) if 語(yǔ)句的過(guò)程中。首先調(diào)用了 onTouchEvent 方法。這就解釋了,為什么它先執(zhí)行了 mOnTouchListener ,然后再執(zhí)行 onTouchEvent 。

  • 現(xiàn)在我們就可以總結(jié)一下:首先我們回調(diào)了 dispatchTouchEvent ,然后回調(diào) OnTouchListener 。這個(gè)時(shí)候,如果 TouchListener 沒(méi)有 return true ,那么就會(huì)接著去運(yùn)行 onTouchEvent ( 當(dāng)然,如果 return true 后面的層級(jí)就不會(huì)執(zhí)行了 。一句話說(shuō)就是:到那個(gè)層級(jí) return true 那么哪個(gè)層級(jí)就消費(fèi)掉了這個(gè)事件 )。

1.1.3 onTouchEvent 的處理

  • 同時(shí)我們還有一個(gè)結(jié)果:我們 onClick ( 包括我們的 onLongClick ) 是來(lái)自于我們 onTouchEvent 這個(gè)方法的處理。那么下面我們就來(lái)看看 View 中是怎么處理 onTouchEvent 的:
public boolean onTouchEvent(MotionEvent event) {
    。。。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                。。。
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                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:
                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;
        }

        return true;
    }

    return false;
}

二、onClick 和 OnLongClick

  • 因?yàn)槲覀兪悄?ACTION_DOWN 作為舉例的。那么我們先來(lái)分析一下 case MotionEvent.ACTION_DOWN : 中 onTouchEvent 是怎么執(zhí)行的,以及 onClickOnLongClick 是如何產(chǎn)生的:

2.1 onClick 和 OnLongClick 的產(chǎn)生

  • 首先,當(dāng)我們手指按下時(shí),有一個(gè) mHasPerformedLongPress 標(biāo)識(shí)會(huì)先被設(shè)為 false 。再往下會(huì)執(zhí)行一行 postDelayed(mPendingCheckForTapViewConfiguration.getTapTimeout()); 我們來(lái)看看這一行的作用:

  • 首先,從名字我們就可以猜測(cè),這是個(gè)延時(shí)執(zhí)行的方法。我們進(jìn)一步閱讀發(fā)現(xiàn) mPendingCheckForTap 是一個(gè) Runnable 動(dòng)作; ViewConfiguration.getTapTimeout() 是一個(gè) 100mm 的延時(shí)。也就是說(shuō)延時(shí) 100mm 后去執(zhí)行 mPendingCheckForTap 中的動(dòng)作。那么我們就來(lái)看看 mPendingCheckForTap 中做了什么:

private final class CheckForTap implements Runnable {
    public float x;
    public float y;

    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
    }
}
  • 也就是說(shuō),停一百秒后就開始檢查,用戶的手指是否離開了屏幕。( 就是當(dāng)前 ACTION_DOWN 之后,有沒(méi)有觸發(fā)了 ACTION_UP 這個(gè)環(huán)節(jié) ),但是 ACTION_DOWN 后,我們還有一個(gè) ACTION_MOVE 過(guò)程。在這個(gè) ACTION_MOVE 中,如果 100mm 內(nèi)離開了屏幕、或者離開了這個(gè)控件就會(huì)觸發(fā) ACTION_UP ,那么就認(rèn)為這是一個(gè)點(diǎn)擊事件 onClick 。如果沒(méi)有觸發(fā) ACTION_UP 的話,就會(huì)再延時(shí) 400mm 。

2.2 ACTION_DOWN 之后流程

  • ACTION_DOWN 之后,會(huì)先等 100mm
  • 如果沒(méi)有離開屏幕或者離開控件,就是沒(méi)有觸發(fā) ACTION_UP 的話,就會(huì)再延時(shí) 400mm。
  • 500mm 后就會(huì)觸發(fā) onLongClick 事件。

2.3 那么我們現(xiàn)在來(lái)驗(yàn)證一下 onLongClick :

  • 首先再 MainActivity 中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {

        return true;
    }
});
  • 接著,我們發(fā)現(xiàn) OnLongClick 是有返回值的,如果返回值是 false 還會(huì)接著去觸發(fā) onClick 事件,如果返回 true 的話,那么這個(gè)長(zhǎng)按事件就直接被消費(fèi)掉了( 也就是這個(gè)點(diǎn)擊事件就不會(huì)完后傳遞到 OnClickListener 中去了 )。

2.4 總結(jié)

  • 100mm 時(shí)為點(diǎn)擊,500mm 時(shí)為長(zhǎng)按,接著觸發(fā)長(zhǎng)按事件。
  • 再看長(zhǎng)按事件的返回值,如果時(shí) true 就結(jié)束。
  • 如果時(shí) false 那么 OnClickListener 就同樣也被執(zhí)行。
  • 這就是由 obTouchEvent 產(chǎn)生出來(lái)的 onClick/onLongClick 的來(lái)龍去脈。

總結(jié)

  • 我們 View 的事件方法,基本上就是這么一個(gè)思路,從 dispatchTouchEventOnTouchListener 監(jiān)聽(tīng)器,再到 onTouchEvent,接著 onTouchEvent 由產(chǎn)生了 onClick/onLongClick 。
  • 如果大家感興趣的話可以更深入的去閱讀源碼。
  • 重點(diǎn):學(xué) Android 有一段時(shí)間了,我打算好好的梳理一下所學(xué)知識(shí),包括 Activity 、Service 、BroadcastRecevier 事件分發(fā)、滑動(dòng)沖突、新能優(yōu)化等所有重要模塊,歡迎大家關(guān)注 _yuanhao 的 簡(jiǎn)書~ ,方便及時(shí)接收更新
  • 如果有可以補(bǔ)充的知識(shí)點(diǎn),歡迎大家在評(píng)論區(qū)指出。

碼字不易,你的點(diǎn)贊是我總結(jié)的最大動(dòng)力!


Android
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容