前言
- Android View 的 事件處理在我們的編程中,可謂是無處不在了。但對于大多數人而言,一直都是簡單的使用,對其原理缺乏深入地認識。
- 學 Android 有一段時間了,最近發現,很多基礎知識開始有些遺忘了,所以從新復習了 View 的事件分發。特地整理成了這篇文章分享給大家。
- 本文不難,可以作為大家茶余飯后的休閑。
祝大家閱讀愉快!
方便大家學習,我在 GitHub 上建立個 倉庫
倉庫內容與博客同步更新。由于我在
稀土掘金
簡書
CSDN
博客園
等站點,都有新內容發布。所以大家可以直接關注該倉庫,即使獲得精彩內容
一、View 的事件回調
- 我們結合源碼看看
View
的事件分發是個怎樣的過程,首先我們建立一個類MyButton
類繼承AppCompatButton
用于測試:
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 事件分發流程
- 我們都知道有一個方法叫做
public boolean dispatchTouchEvent(MotionEvent event)
。首先我們要知道,對于我們這個自定義控件,他的觸摸事件都是從我們dispatchTouchEvent
這個方法開始往下去分發的。所以可以說:這個方法是一個入口方法。
1.1.1 onTouchEvent 作用
- 現在我們重寫該方法和另一個方法:
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);
}
- 然后我們在
MainActivity
中,設置一個實例化一個MyButton
控件對象用于測試,并且給他添加一個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();
}
/**
* 實例化控件
*/
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----");
}
});
}
}
- 然后我們運行這個
Demo
,點擊MyButton
按鈕,會的到如下日志:
我們可以看到首先回調了這個
dispatchTouchEvent
,然后是它的監聽器OnTouch
,接著是它的onTouchEvent
,最后又執行了dispatchTouchEvent
,那么這是為什么呢?這是因為我們這兒只監聽了
ACTION_DOWN
而當手指抬起時它同樣還回去回調dispatchTouchEvent
,最后我們打印OnClick
的回調。總結一下就是:
dispatchTouchEvent
->setOnTouchListener
->onTouchEvent
->setOnClickListener
說明我們
setOnClickListener
是通過onTouchEvent
處理,產生了OnClick
。一會我們再來看看其中的原理。既然說
dispatchTouchEvent
像一個入口,就先讓我們來看下它是怎么處理和操作的: 首先,既然我們調用了super.dispatchTouchEvent(event)
,那么我們就來看看它父類中是怎么實現該方法的。不信的是,它的父類AppCompatButton
也沒有實現該方法 ,最后經過層層搜尋,我們發現這個方法是屬于View
的方法。
1.1.2 dispatchTouchEvent 的實現
- 那么現在我們來看看
View
的dispatchTouchEvent
怎么實現的:
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
中,我們可以發現下面這樣一個代碼塊
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
不難看出:如果執行了這個代碼段,那么后面的方法就不會執行了,并且
dispatchTouchEvent
會返回true
。我們再仔細觀察下其中的條件:在if
條件中我們發現:只有當其滿足li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
時才會執行if
內的操作經過上面分析,我們可以知道:
onTouch
事件必須返回true
時,才會執行該方法塊。那么我們就回到MainActivity
中。我們發現setOnTouchListener
的onTouch
默認返回值是false
( 不滿足返回值為true
), 這就表明他會繼續去執行下一個代碼塊:
if (!result && onTouchEvent(event)) {
result = true;
}
執行這個
if
語句的過程中。首先調用了onTouchEvent
方法。這就解釋了,為什么它先執行了mOnTouchListener
,然后再執行onTouchEvent
。現在我們就可以總結一下:首先我們回調了
dispatchTouchEvent
,然后回調OnTouchListener
。這個時候,如果TouchListener
沒有return true
,那么就會接著去運行onTouchEvent
( 當然,如果return true
后面的層級就不會執行了 。一句話說就是:到那個層級return true
那么哪個層級就消費掉了這個事件 )。
1.1.3 onTouchEvent 的處理
- 同時我們還有一個結果:我們
onClick
( 包括我們的onLongClick
) 是來自于我們onTouchEvent
這個方法的處理。那么下面我們就來看看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
- 因為我們是拿
ACTION_DOWN
作為舉例的。那么我們先來分析一下case MotionEvent.ACTION_DOWN
: 中onTouchEvent
是怎么執行的,以及onClick
和OnLongClick
是如何產生的:
2.1 onClick 和 OnLongClick 的產生
首先,當我們手指按下時,有一個
mHasPerformedLongPress
標識會先被設為false
。再往下會執行一行postDelayed(mPendingCheckForTap
和ViewConfiguration.getTapTimeout())
; 我們來看看這一行的作用:首先,從名字我們就可以猜測,這是個延時執行的方法。我們進一步閱讀發現
mPendingCheckForTap
是一個Runnable
動作;ViewConfiguration.getTapTimeout()
是一個100mm
的延時。也就是說延時100mm
后去執行mPendingCheckForTap
中的動作。那么我們就來看看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);
}
}
- 也就是說,停一百秒后就開始檢查,用戶的手指是否離開了屏幕。( 就是當前
ACTION_DOWN
之后,有沒有觸發了ACTION_UP
這個環節 ),但是ACTION_DOWN
后,我們還有一個ACTION_MOVE
過程。在這個ACTION_MOVE
中,如果100mm
內離開了屏幕、或者離開了這個控件就會觸發ACTION_UP
,那么就認為這是一個點擊事件onClick
。如果沒有觸發ACTION_UP
的話,就會再延時400mm
。
2.2 ACTION_DOWN 之后流程
- 在
ACTION_DOWN
之后,會先等100mm
- 如果沒有離開屏幕或者離開控件,就是沒有觸發
ACTION_UP
的話,就會再延時 400mm。 - 這
500mm
后就會觸發onLongClick
事件。
2.3 那么我們現在來驗證一下 onLongClick :
- 首先再
MainActivity
中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});
- 接著,我們發現
OnLongClick
是有返回值的,如果返回值是false
還會接著去觸發onClick
事件,如果返回true
的話,那么這個長按事件就直接被消費掉了( 也就是這個點擊事件就不會完后傳遞到OnClickListener
中去了 )。
2.4 總結
-
100mm
時為點擊,500mm
時為長按,接著觸發長按事件。 - 再看長按事件的返回值,如果時
true
就結束。 - 如果時
false
那么OnClickListener
就同樣也被執行。 - 這就是由
obTouchEvent
產生出來的onClick/onLongClick
的來龍去脈。
總結
- 我們
View
的事件方法,基本上就是這么一個思路,從dispatchTouchEvent
到OnTouchListener
監聽器,再到onTouchEvent
,接著onTouchEvent
由產生了onClick/onLongClick
。 - 如果大家感興趣的話可以更深入的去閱讀源碼。
-
重點
:學Android
有一段時間了,我打算好好的梳理一下所學知識,包括Activity
、Service
、BroadcastRecevier
事件分發、滑動沖突、新能優化等所有重要模塊,歡迎大家關注 _yuanhao 的 簡書~ ,方便及時接收更新 - 如果有可以補充的知識點,歡迎大家在評論區指出。
碼字不易,你的點贊是我總結的最大動力!
由于我在「稀土掘金」「簡書」「
CSDN
」「博客園」等站點,都有新內容發布。所以大家可以直接關注我的GitHub
倉庫,以免錯過精彩內容!一萬多字長文,加上精美思維導圖,記得點贊哦,歡迎關注 _yuanhao 的 簡書~ ,我們下篇文章見!