一 前言
把android开发艺术探索第三章阅读了,对事件分发机制有一个大概的了解,关于事件分发的文章也很多,自己也看了一些相关的文章,决定自己分析一遍记录下来,加深映象和对这个机制的了解。
二 View的事件分发
View的事件分发过程
我们直接分析代码吧,前面对事件分发的一些结论在上一篇文章Android开发艺术探索第三章 读书笔记已经提到了,不了解的可以先去看一下,带着问题来分析源码。我们先从View的dispatchTouchEvent
方法入手,android中的所有事件都是经过dispatchTouchEvent
来分发的,我们通过返回值来决定是否自己处理还是传分发给子控件: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)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //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; }
代码很多,我们慢慢分析,2-10行首先判断了当前事件是否可以获取焦点,如果不能获取焦点或者找不到一个view,直接返回false,紧接着12-22行设置一些标记位和input手势传递等,然后这里是关键
if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //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; } }
onFilterTouchEventForSecurity(event)
判断view是否被遮住,然后判断view是不是enable的,判断handleScrollBarDragging(event)
事件是否为滚动条拖动,是则为true,如果上面条件都为真那这里直接返回true,接着定义了ListenerInfo
的局部变量,接着就是if里面几个条件判断,可以说是关键点,首先li != null
这个自然不会为null,在代码里面可以看出:ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }
接着
li.mOnTouchListener != null
这个属性为不为null,我们可以在源码里看到:/** * 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; }
这个主要取决于我们系统view是否
setOnTouchListener
,设置了就不为null;第三个条件(mViewFlags & ENABLED_MASK) == ENABLED
判断view是不是enable的,默认都是enable的;第四个条件li.mOnTouchListener.onTouch(this, event)
这里主要判断onTouch的返回值,如果onTouch消费了这个事件,则返回true,那么这里直接就返回true了,就不会执行下面这个方法了if (!result && onTouchEvent(event)) { result = true; }
所以onTouchEvent的执行与否跟onTouch的返回值有很大关系,有时候我们在对view的onTouch里面返回true,会发现这个view的点击事件没效果,点击事件是不是就在
onTouchEvent
方法里面,我们接着分析,不过我们可以得出一个结论,就是在dispatchTouchEvent
方法里面首先执行onTouch
方法,如果if里面所有条件满足则dispatchTouchEvent
返回值返回true,并不执行onTouchEvent
方法,如果控件不是enable或者mOnTouchListener
返回null,或者onTouch
返回false,那么就会执行下面的onTouchEvent
方法,那么dispatchTouchEvent
返回值方法跟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, x, y); } 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; }
这个代码比上面的更长,我们还是只分析主要的,第2-16行可以看出,如果view是
DISABLED
状态,只要满足下面三种条件之一那么这个view虽然被禁用了,但是满足这三个之一我们还是会消费这个事件,只是不响应它们而已。// 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);
17 - 21行,如果view设置了代理,那么还会执行
mTouchDelegate
的onTouchEvent
方法。那么接下来23行可以看见,一个view是enable且满足三个状态之一则进入swift判断中去,反之则返回false。我们首先看看ACTION_DOWN
,mHasPerformedLongPress = false;
设置了一个长按的标记位,然后if (performButtonActionOnTouchDown(event)) { break; }
一般的设备都是false,这是一个处理鼠标右键的事件。接下来的所有方法都是判断当前view是否在一个滚动的view容器内,避免把滑动当成一次点击事件,然后根据判断的结果查看是否在一个滚动容器内,检查的方法实现代码如下:
public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; }
遍历整个View树,通过这个方法来判断
shouldDelayChildPressedState
是否是一个滚动的布局,这个方法不能滚动的布局都重写了并返回false,能滚动的都是返回true,判断的结果如果在滚动容器内,先设置一个mPrivateFlags |= PFLAG_PREPRESSED;
准备点击的标记位,然后在发送一个延迟100ms的消息确定用户是要滚动还是点击。先实例化一个mPendingCheckForTap
,然后将它加入到一个消息队列延迟执行,我们看看这个CheckForTap
实例,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); } }
里面设置了按下事件,然后检查延迟发送一个100ms消息看看是不是长按事件,在给定的时间内如果没有移动那么就当做用户是想点击,而不是滑动,将 mPendingCheckForTap添加到消息队列中,延迟执行。如果在这TapTimeout之间用户触摸移动了,取消了什么,则移除此消息。后面会有很多移除这些事件的。否则:执行按下状态.然后检查长按.我们看看
checkForLongClick
这个函数private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
检查长按思路等等看看是不是长按事件,主要调用的还是
CheckForLongPress
这个类中的方法,我们看一下private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; private float mX; private float mY; @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } } public void setAnchor(float x, float y) { mX = x; mY = y; } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } }
主要是根据view的
mWindowAttachCount
方法统计view的attach到window的次数,检查长按的时候attach次数和长按到形成的attach次数一致的话,则认为是一个长按处理,里面则执行了一个长按的事件,长按事件就是在这个performLongClick(mX, mY)
方法里面执行的,我就不继续深入了,明白一点就是onLongClick事件只要你在长按识别的时间内检查长按的标志位为true,就会执行,跟click事件不一样,click事件是在手指抬起后执行,后面分析;不一样的话,则界面可能发生了其它事情,暂停或者重新启动造成了界面重新刷新,那长按自然应该不执行。如果不是在一个滚动容器内,则调用
setPressed(true, x, y);
设置一个按下状态,然后在检查长按状态,这里跟上面长按机制差不多,不在分析。我们看看
ACTION_UP
事件,if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed)
判断了不是在一个滚动操作的容器中,已经可以确定这是一个按下状态,然后if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); }
三个条件分别是当前view是否能够获取焦点,触摸是否能够获得焦点,当前view还没有获取焦点,就请求获取一个焦点。
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); }
接着就是一个判断用户在按下没有效果之前就不按了,我们还是要进行实际的操作前让用户看到一个效果。
接着判断用户是否进行了长按,如果没有移除相关检测,
mHasPerformedLongPress
这个是一个标志位,根据这个判断是不是一个长按事件,长按事件发生后会将这个标志位设置为true,不是则移除掉长按的延迟消息。// 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 (!post(mPerformClick))
然后就是使用post到主线程中执行一个performClick的Runnable,而不是直接执行一个点击事件,为了让用户感觉到一个按下的状态,我们看看这个重点吧,就是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; }
这个方法和上面执行4个判断的if语句有点像,先定义一个变量后赋值,然后判断
li.mOnClickListener != null
,这个在哪里可以看到呢,自然也是在setOnClickListener
里面赋值的,其实view的Clickable
属性是否为false,也和具体的view有关系,可以点击的一般就是true,不可点击的一般就为false。可以通过调用 setClickable`方法设置,如果调用了点击事件,那么都会自动的设为true。public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
然后点击下执行一个click事件,result返回true,这个事件就被消费了,至此,也就明白了OnClick是在onTouchEvent里面执行的,只要一个控件满足clickable,并且设置了监听,那么都会执行这个点击事件。后面就是一个按下的效果显示时间,由
ViewConfiguration.getPressedStateDuration()
这个常量指定,也就是64ms,然后将设置的按下的状态设置为false。最后就是还有一个
ACTION_CANCEL
这个比较容易理解,就是setPressed(false);
然后移除各种延迟发送的消息。那么这一部分的源码大概就分析完了,我们可以得出一个结论,就是点击事件是在onTouchEvent方法中的ACTION_UP中执行一个click事件,并且我们每次执行一个action,只有前一个action返回true,才会执行下一个action,因为前一个action返回了false,那么dispatchTouchEvent
将不会派发下一次事件。三 验证分析
我们分析了这么多,我们下面来简单的验证一下,对上述分析来一个验证。
- 例子
我们自己定义一个Button,实例代码如下,非常简单,我就不解释了:
public class TestButton extends Button{ private static final String TAG = "TestButton"; public TestButton(Context context) { super(context); } public TestButton(Context context, AttributeSet attrs) { super(context, attrs); } public TestButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return super.onTouchEvent(event); } }
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_test" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.lhtb.okhttpdemo.TestActivity"> <com.lhtb.okhttpdemo.TestButton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="点击"/> </RelativeLayout>
public class TestActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener,View.OnLongClickListener{ private TestButton button; private static final String TAG = "TestActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); button = (TestButton) findViewById(R.id.btn); button.setOnClickListener(this); button.setOnTouchListener(this); button.setOnLongClickListener(this); } @Override public void onClick(View v) { Log.d(TAG, "onClick: " + v); } @Override public boolean onTouch(View v, MotionEvent event) { Log.d(TAG, "onTouch: " + event.getAction()); return false; } @Override public boolean onLongClick(View v) { Log.d(TAG, "onLongClick: " + v); return false; } }
- 例子
现象分析
2.1 我们先不更改任何事件返回,点击button如下:
发现所有事件都得到了正常派发,我没有移动所以没有派发move事件,先执行了长按事件,然后执行了点击事件。
我们现在修改TestActivity中的
onLongClick
:@Override public boolean onLongClick(View v) { Log.d(TAG, "onLongClick: " + v); return true; }
我们返回true,其它代码不改动。现在看看点击事件的派发:
我们改为true,前面都一样,只是没有执行点击事件,这是为什么呢,因为true就是消费了这个事件,我们在
ACTION_UP
里面对长按做了判断,如果长按事件发生了并返回为true,就不会去执行点击事件的,上面源码有做分析,这个比较常用,各位老司机也是轻车熟路。2.2 我们简单的修改TestButton的dispatchTouchEvent返回为false,其它代码不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return false; }
点击button后的事件如下:
你发现点击后任何事件都没有得到触发。
我们在改为false的基础上调用super父类的方法,其它不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); super.dispatchTouchEvent(event); return false; }
点击button后的事件如下:
发现跟上面执行的有点不一样的,返回false不执行下一次派发,但是执行了长按事件,这是为什么呢,因为我们上面分析过我们按下的时候就延迟发送消息判断是不是长按事件,只要点击了一下我们就发送了消息,但是后续事件没有得到派发,也就没有移除长按的消息,所以就可以得到执行。
2.3 我们修改TestButton的dispatchTouchEvent返回值为true,不调用super父类方法:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return true; }
点击button如下:
你会发现虽然派发了事件,但是没有执行任何方法,比如onTouchEvent啊,onTouch什么的,不调用super任何方法都不执行。
我们在上面的基础上调用super方法,其它不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); super.dispatchTouchEvent(event); return true; }
点击button后的事件如下:
我们发现跟正常派发的事件机制一样。
2.4 我们修改TestButton的返回值为false:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return false; }
点击button如下
发现返回了false,也就是dispatchTouchEvent返回false,那么将不派发下一次事件。
我们在上面基础上添加调用父类的super方法:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); super.onTouchEvent(event); return false; }
点击button后的点击事件如下,:
跟上面差不多,只不过执行了长按事件,上面分析过了,不在说明。
2.5我们修改TestButton的onTouchEvent返回true:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return true; }
点击button后如下:
你会发现所有事件都得到正常派发,只是没有执行点击事件,没用调用super方法。
在上面的基础上调用super方法:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); super.onTouchEvent(event); return true; }
点击button后如下:
发现所有事件都得到了正常派发和执行。
四 结尾
这篇文章就讲完了,主要讲的是view的事件分发,其中有些还是没讲到,关于view的知识也比较多,其中难免会有错误,欢迎大家提出来。