1. 程式人生 > >進階必備-Android Click事件是如何觸發的?

進階必備-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分析一下,對比於此文中的實現也不太一樣哦。

微信搜尋公眾號:南京Android部落,你想知道的知識點後臺回覆,我會拿出來一起分析探討。