Android事件传递机制详解及最新源码分析——View篇

时间:2021-08-09 14:03:27

摘要: 版权声明:本文出自汪磊的博客,转载请务必注明出处。

对于安卓事件传递机制相信绝大部分开发者都听说过或者了解过,也是面试中最常问的问题之一。但是真正能从源码角度理解具体事件传递流程的相信并不多,那么接下来我将写四篇文章从我目前掌握的情况来与大家共同探讨一下安卓事件传递机制。四篇文章分别为:View篇,ViewGroup篇,Activity篇,以及最后一篇实用例子。废话少话,马上开始View篇。

从简单的Demo例子现象开始分析

我们先写一个简单的demo。布局文件,代码如下:

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wl.viewdispatchtouchevent.MainActivity" > <Button
android:id="@+id/click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Clear灬Heart" /> </RelativeLayout>
 public class MainActivity extends Activity implements OnTouchListener, OnClickListener {

     private static final String TAG = "WL";
private Button mButton; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); mButton = (Button) findViewById(R.id.click);
mButton.setOnTouchListener(this);
mButton.setOnClickListener(this);
} @Override
public void onClick(View v) {
//
Log.i(TAG, "onClick :"+v);
} @Override
public boolean onTouch(View v, MotionEvent event) {
//
Log.i(TAG, "onTouch :"+",...action :"+event.getAction()+"...View :"+v);
return false;
}
}

很简单吧,就一个Button,设置触摸以及点击事件,我们看看运行情况:

一,正常点击Button打印如下:

Android事件传递机制详解及最新源码分析——View篇

二,我们发现onTouch方法有个返回值,默认情况下是false,我们将返回值改为true,点击Button打印如下:

Android事件传递机制详解及最新源码分析——View篇

到这里根据运行情况我们就可以小小的总结一下:

1⃣️ View的是事件执行顺序为先执行onTouch在执行onClick事件

2⃣️如果我们在onTouch事件中返回true那么将不会执行onClick事件

相信上面这些很简单,这结论很多开发者也都知道,那么源码中是怎么控制的呢?接下来我们看看源码中的逻辑。

View事件传递机制源码分析(源码版本为API23)

首先我们要知道在安卓中触摸控件首先会触发控件的dispatchTouchEvent方法然而在Button类中并没有找到这个方法,在其父类TextView中还是没有找到,那么继续向上找,果然在其"爷爷"类View中找到dispatchTouchEvent方法,其实这个方法在View子类(ViewGroup除外,对于ViewGroup下一篇我们在做讨论)中一般都没有,那么我们看下View中dispatchTouchEvent方法源码

 /**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
} boolean result = false; if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
} final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
} if (onFilterTouchEventForSecurity(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;
}

这个方法还可以,不算很长,对于分析源码,强烈建议大家只看重点部分,否则只会把自己绕进去出不来了。

第19行,定义一个标记result,并且最后会反回这个标记。

最重点的就是31-43行代码,首先if (onFilterTouchEventForSecurity(event))主要判断当前窗体是否被遮盖,如果遮盖则返回false, 那么也就不会执行32-42代码逻辑,从而也不会改变result的值,最后58行返回result,即返回false。

大多数情况下onFilterTouchEventForSecurity(event)返回true,那么就执行32-42的代码逻辑。

33行代码ListenerInfo li = mListenerInfo;这行代码有个疑问,ListenerInfo这个类是做什么的?通过搜寻我们可以在View内部找到这个类,其实就是个简单的静态内部类,用来定义View的各种监听事件,源码就不贴出来了,自行查找看一下。

34-36行代码if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))首先li不为空,然后li.mOnTouchListener 不为空,这里mOnTouchListener是在哪里赋值的呢?稍微有经验就应该想到是在设置触摸事件监听的方法中设置的,我们看下:

  /**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}

果然是在这里对其赋值,这里的getListenerInfo()又是什么呢?很简单就是给View中定义的mListenerInfo赋值,源码如下:

 ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}

接下来继续判断View是否是enabled,这个没什么多解释的,应该都理解。最后li.mOnTouchListener.onTouch(this, event)调用我们设置的onTouch事件。

前面判断都成立的前提下,34-36行if判断是否成立则由onTouch事件的返回值决定。如果onTouch返回false,则if判断不成立result依然为false。如果onTouch返回true,则if判断成立,可以继续执行37行代码将result置为true。那么result为true或者false有什么用呢?别急,继续往下看。

40-42行代码中if (!result && onTouchEvent(event))判断,我们会发现只有在 !result 为true的前提下才会继续执行onTouchEvent方法,通过上面分析,正常情况下,result是否置为true是由onTouch返回值决定的,默认情况下在我们设置触摸事件时系统帮我们返回false,如下(代码出自demo):

 @Override
public boolean onTouch(View v, MotionEvent event) {
//
Log.i(TAG, "onTouch :"+",...action :"+event.getAction()+"...View :"+v);
return false;
}

所以默认情况下result变量不会置为true,则!result为true,可以继续执行onTouchEvent。如果我们人为将onTouch事件返回true则result会被置为true,进而不会执行onTouchEvent事件。

到目前为止我们还没有看到任何关于onClick事件的代码逻辑,别急,我们继续分析。

40行代码if (!result && onTouchEvent(event))是否成立通过以上分析得知默认情况下由onTouchEvent决定,那么onTouchEvent具体做了什么呢?我们来看一下:

     public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction(); if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
} if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
} if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
} if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
} 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)) {
performClick();
}
}
} if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
} if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
} removeTapCallback();
}
mIgnoreNextUpEvent = false;
break; case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false; 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);
}
break; case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break; case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback(); setPressed(false);
}
}
break;
} return true;
} return false;
}

也还好,不算太长,我们还是分析重点部分。

7-16行代码分析可以发现如果View是disabled的,并且是clickable的,那么直接返回true, 消费掉此事件,只是不给予响应。(源码中注视已经说明)

                  如果View是disabled的,并且是disclickable的,那么直接返回false, 不消费掉此事件。默认情况下View都是enabled的,这里不会进入。

我们继续往下分析

24-137行,我们会发现只要View是disclickable状态则onTouchEvent事件就会返回false,只要View是clickable就会进入判断内部根据事件不同分别进行对应操作最后都会反回true。

Switch的判断ACTION_DOWN,ACTION_CANCEL,ACTION_MOVE情况主要是做一些设置,记录状态操作。

在ACTION_UP情况下:

29-50代码主要判断之前是否按下过以及是否能获取焦点。

46行代码,主要判断是否已经执行长按事件,如果没有执行则进入if条件判断,接下来48行移除长按事件的回掉监听。

55-57行代码,判断mPerformClick是否为空,为空则初始化,PerformClick类是做什么的呢?看一下源码:

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

很简单吧,就是一个继承自Runnable接口的类,

58-60行,重点。主要做的就是通过Handler将PerformClick对象mPerformClick,Post到主线程执行performClick()方法。

接下来我们具体看下performClick()方法做了什么。

 public boolean performClick() {
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);
return result;
}

第3行代码不用过多解释了吧,之前分析过。

第4行进行if判断,li不为空好理解,那么li.mOnClickListener中mOnClickListener在哪赋值的呢?通过搜寻我们发现在设置点击事件的时候给其赋的值,如下:

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

通过上面代码我们还会发现在对mOnClickListener赋值前会先判断View是否可以点击,如果不可以点击则通过代码设置为可点击。这就说明即使我们在布局中将View设置为不可点击状态,如果我们设置了其点击事件依然会从代码层面设置回可点击状态,依然可以响应点击事件,可自行测试。

如果我们设置了点击事件监听回掉那么第4行if条件判断就会成立,进而执行5-7逻辑。

第6行则调用了onClick方法,到此我们终于找到在哪里执行了onClick方法。

我们回看dispatchTouchEvent方法40-42行if判断,默认情况下!result为true, 如果onTouchEvent返回true则接下来将result置为true,相反则置为false。

dispatchTouchEvent返回true则告诉父类子类已经消耗此事件,如果返回false则告诉父类子类没有消耗掉此事件,也可以理解为子类不能处理父类传递过来的事件,之后父类也不会将后续事件传递给子类,只有前一个事件返回true,才会出发后续事件。

总结:

我们点击一个View会首先触发View的dispatchTouchEvent方法。

在dispatchTouchEvent方法中会判断View是否设置触摸事件,如果设置触摸事件并且是enable的状态,则调用onTouch方法。

如果View是disabled状态则不会执行onTouch事件,直接执行onTouchEvent方法。

如果onTouch方法中返回true则消耗掉此事件,dispatchTouchEvent直接返回true。

如果onTouch事件返回false,则继续向下执行onTouchEvent方法。

在onTouchEvent方法中会先判断View是否为disabled状态,如果是并且是clickable的则直接返回true,消耗此事件。如果是disabled状态,但是是disclickable状态则返回false。

如果View是enable状态并且是clickable状态则onTouchEvent方法返回true,进而dispatchTouchEvent方法返回true,消耗此事件。

如果View是enable状态但是是disclickable状态则onTouchEvent方法返回false,进而dispatchTouchEvent方法返回false,不消耗此事件。

好了到此为止View的事件分发机制我想说的就已经全部讲解完毕,其实很简单,但是非常重要,如果View事件分发机制掌握不好那么再学习ViewGroup的事件分发机制的时候肯定是要蒙逼的,因为ViewGroup的事件分发机制分析起来会绕一些。要复杂很多。