進階必備-Android Click事件是怎麼觸發的?

一、背景

閱讀本篇文章前,假設你已經閱讀前一篇文章上一篇鏈接。  
由於有同學問到onClick和touch事件的關係,這裏就從源碼的角度分析下onClick和onLongClick與onTouchEvent事件是怎麼關聯的。本文將通過View.java、TextView.java、Button.java的源碼作爲例子分析。

二、源碼解讀

首先我們知道View、TextView、Button三者的關係,即:Button繼承自與TextView,TextView繼承自View。
在默認我們不做任何特殊設置時,三者能響應click事件的只有Button。這是什麼原因呢?

首先我們看到View的源碼中onTouchEvent方法中:

final int viewFlags = mViewFlags;

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
         || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
         || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

在onTouchEvent方法的一開始,通過viewFlags去判斷當前View的CLICKABLE或者LONG_CLICKABLE位是否可以點擊或者長按。默認情況下,在View初始化的時候會從xml讀clickable屬性或者longclickable屬性。所以如果不在xml中設置,View和TextView是不會響應點擊事件的,那麼我們翻開Button的源碼看下爲什麼唯獨它是響應的呢?

public Button(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.buttonStyle);
}

可以看到在Button初始化的時候,設置了Button的style,我們翻開Styles.xml中找到Button:

    <style name="Widget.Button">
        <item name="background">@drawable/btn_default</item>
        <item name="focusable">true</item>
        <item name="clickable">true</item>
        <item name="textAppearance">?attr/textAppearanceSmallInverse</item>
        <item name="textColor">@color/primary_text_light</item>
        <item name="gravity">center_vertical|center_horizontal</item>
    </style>

沒錯,Button在默認的Style中就將clickable設置爲true了。所以在默認情況下Button的clickable=true。通過下面這行代碼(View.java的13743行)就可以知道,當clickable=true時,

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP){
    // 處理邏輯
}

就可以進入if當中繼續處理,因爲我們響應click事件一般是在我們手按下再擡起後進行。所以,我們猜測是在MotionEvent.ACTION_UP事件後觸發click的。所以我們直接看if條件中的ACTION_UP中的邏輯:

    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)) {
                performClickInternal();
            }
        }
    }  

在進入處理click之前,會先判斷是否已經被長按做了處理,並且此次的ACTION_UP事件沒有被忽略掉。當這些條件都滿足後,首先會將長按的callback remove。然後會通過Post Runable的方式將PerformClick的實例post到隊列中等待處理,不直接去處理click事件而是使用post的方式是確保如果有視圖相關的更新操作完成後再觸發performClickInternal()。我們先看下PerformClick類是幹什麼的?

    private final class PerformClick implements Runnable {
        @Override
        public void run() {
            performClickInternal();
        }
    }

很顯然就是Runable對象,其中就是調用了performClickInternal()方法,而此方法中調用的是performClick方法:

    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

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

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

可以看到,最後是直接同過ListenerInfo中的mOnClickListener對象調用onClick方法。而ListenerInfo中的mOnClickListener對象就是我們通常使用view.setOnclickListener()方法設置賦值的:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        // 賦值操作
        getListenerInfo().mOnClickListener = l;
    }

至此,onClick事件是如何從onTouchEvent中觸發的就可以完全看出來了。  
同理,onLongClick類似,筆者這裏就不做詳細分析了。留給讀者自己去詳細的看下源碼,這裏簡單的介紹下。  
onLongClick事件是如何處理的呢?因爲onCLick事件是在手指擡起後觸發的,所以我們選擇分析的是ACTION_UP事件,但是長按事件是在我們長按某個View的時候觸發的,所以並沒有將手指擡起來。所以我們肯定是在分析處理ACTION_DOWN中處理的。我們查看ACTION_DOWN事件下調用的checkForLongClick方法:

    private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

我們看到onTouchEvent中的checkForLongClick中在最後一行通過postDelayed延遲發送了一個Runable對象:mPendingCheckForLongPress。延時時間是ViewConfiguration.getLongPressTimeout() - delayOffset的時間差。綜上,簡單來說,當我們按下屏幕的時候發送了一個延時的Runable,然後等到Runable被執行的時候,在通過一些標誌位判斷當前是否還滿足長按被執行的條件,如果滿足,回調listener中的onLongClick。 細節請讀者自行對着源碼看哦。

三、總結

對於一般的View來講,onTouchEvent中處理的無非是對View的一個點擊事件的處理、按下狀態的處理、長按的處理。讀者可以對類似於ScrollView這種帶滑動的控件的onTouchEvent分析一下,對比於此文中的實現也不太一樣哦。

長按下方二維碼,你想知道的知識點後臺回覆,我會拿出來一起分析探討。

發佈了60 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章