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

方便大家學(xué)習(xí),我在 GitHub 上建立個(gè) 倉(cāng)庫(kù)
倉(cāng)庫(kù)內(nèi)容與博客同步更新。由于我在
稀土掘金簡(jiǎn)書CSDN博客園等站點(diǎn),都有新內(nèi)容發(fā)布。所以大家可以直接關(guān)注該倉(cāng)庫(kù),即使獲得精彩內(nèi)容倉(cāng)庫(kù)地址:
超級(jí)干貨!精心歸納Android、JVM、算法等,各位帥氣的老鐵支持一下!給個(gè) Star !
一、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è)onClickListenter和setOnTouchListener
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ì)的到如下日志:

我們可以看到首先回調(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)看看
View的dispatchTouchEvent怎么實(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)setOnTouchListener的onTouch默認(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í)行的,以及onClick和OnLongClick是如何產(chǎn)生的:
2.1 onClick 和 OnLongClick 的產(chǎn)生
首先,當(dāng)我們手指按下時(shí),有一個(gè)
mHasPerformedLongPress標(biāo)識(shí)會(huì)先被設(shè)為false。再往下會(huì)執(zhí)行一行postDelayed(mPendingCheckForTap和ViewConfiguration.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è)思路,從dispatchTouchEvent到OnTouchListener監(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)力!
由于我在「稀土掘金」「簡(jiǎn)書」「
CSDN」「博客園」等站點(diǎn),都有新內(nèi)容發(fā)布。所以大家可以直接關(guān)注我的GitHub倉(cāng)庫(kù),以免錯(cuò)過(guò)精彩內(nèi)容!倉(cāng)庫(kù)地址:
超級(jí)干貨!精心歸納Android、JVM、算法等,各位老鐵支持一下!給個(gè) Star !一萬(wàn)多字長(zhǎng)文,加上精美思維導(dǎo)圖,記得點(diǎn)贊哦,歡迎關(guān)注 _yuanhao 的 簡(jiǎn)書~ ,我們下篇文章見(jiàn)!