Android(安卓)View相关-事件分发机制详解-View
这篇文章我们来探究下Android中关于事件分发机制的一些细节和流程,由于这部分源码比较繁杂,拆开来讲,本文只探究View的事件分发流程,ViewGroup留到之后再说,在分析完这两者的事件分发机制之后我们来对Android的时间分发机制进行总结。那么本文就从View的子类Button来着手分析事件分发的流程,之后我们再从源码角度分析具体实现过程。
举个栗子
这里用一个很简单的小例子来演示View中dispatchTouchEvent、onTouchEvent、TouchListener的执行顺序,继承自Button的TestButton代码很简单,几个log:
@Overridepublic boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouchEvent ACTION_UP"); break; default: break; } return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "dispatchTouchEvent ACTION_UP"); break; default: break; } return super.dispatchTouchEvent(event);}
Activity中设置onTouchListener,也是几个log:
testView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouch ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouch ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouch ACTION_UP"); break; default: break; } return false; }});
接下来是执行流程,第一次点击按钮:
第二次点击后鼠标移动一下松开:
可以看到一次事件分发从ACTION_DOWN开始,到ACTION_UP结束,且其传递顺序是从dispatchTouchEvent –> onTouch –> onTouchEvent。下面我们来从源码中对这几个方法进行查看。
源码分析
我们先从View的dispatchTouchEvent开始吧:
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; 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; } } ... return result;}
我这里略去了部分代码,来看核心代码,可以看到第10行开始这里开始进行了判断,若同时附和这几个条件,则返回true,注意这里调用了OnTouchListener的onTouch方法,也就是说如果我们调用了View的setOnTouchListener方法那么在dispatchTouchEvent方法执行过程中,会调用OnTouchListener的onTouch方法,若onTouch方法返回true,则设置result为true,onTouchEvent不再执行,若onTouch方法返回为false,则第十行if语句不成立,result为改变,执行下一个判断语句,同时会执行onTouchEvent,若返回true则result为true,反之亦然。
我们继续来看一看onTouchEvent中的代码:
public boolean onTouchEvent(MotionEvent event) { //View状态为Disable并且可点击,返回true if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; return clickable; } //若TouchDelegate(触摸代理类)不为空,则调用其onTouchEvent方法并返回true if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //若可点击或者可长按以及长按出现ToolTip if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ... break; case MotionEvent.ACTION_DOWN: ... break; case MotionEvent.ACTION_CANCEL: ... break; case MotionEvent.ACTION_MOVE: ... break; } return true; } return false;}
这段代码极长,有兴趣的朋友可以通读一下源码,我这里略去了一部分,前两个判断已经在注释里写清楚了,不多赘述,我们重要看下switch语句中的内容,接下来一个一个分析:
ACTION_DOWN
ACTION_DOWN是整个Touch流程的起点,代表触摸点按下操作。我们来看看onTouchEvent中的判断是怎样的:
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; //判断是否为鼠标右键或者手写笔第一个按钮,若是,返回true后续代码不执行 if (performButtonActionOnTouchDown(event)) { break; } //当前视图是否可滚动(例如:当前是ScrollView视图,返回true) boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { // 滚动视图内,先不设置为按下状态,因为用户之后可能是滚动操作 // 不是此次分析的重点,感兴趣可以自己了解下 mPrivateFlags |= PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 不在滚动视图内,立即反馈为按下状态 mPrivateFlags |= PRESSED; // 刷新为按下状态 refreshDrawableState(); //通过Handler发送一个延迟消息来判断是否是长按,500ms checkForLongClick(0); }break;
各个步骤的注释已经写清楚了,下面分解一下各个方法:
首先是performButtonActionOnTouchDown:
protected boolean performButtonActionOnTouchDown(MotionEvent event) { // 如果是鼠标右键,手写笔第一个按钮,看BUTTON_SECONDARY注释 if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) { return true; } } return false;}
很简单的判断,如果成功就返回true,不成功就false。
接下来是isInScrollingContainer:
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返回true),如果是,则返回true
接下来是checkForLongClick:
private void checkForLongClick(int delayOffset) { // 当前视图可以执行长按操作 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); // 延迟一段时间(500ms)把runnable添加到消息队列 postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }}
ACTION_DOWN中的逻辑大概就是这样
ACTION_MOVE
ACTION_MOVE代表触摸点发生滑动
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;
我们先来看看pointInView方法里做了什么:
public boolean pointInView(float localX, float localY, float slop) { return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && localY < ((mBottom - mTop) + slop);}
这个方法是判断是否划出控件可视区域,为了保证触摸点发生及轻微变化就导致ACTION_MOVE被执行,这里加入了一个slop的边界值,即在视图上下左右扩大slop
接下来是两个remove方法,这里我们放在一起讲:
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; removeCallbacks(mPendingCheckForTap); }}private void removeLongPressCallback() { if (mPendingCheckForLongPress != null) { removeCallbacks(mPendingCheckForLongPress); }}
这两个方法主要是删除触摸和长按回调(还记得之前按下后发送的长按延时消息吗)
ACTION_CANCLE
ACTION_CANCLE代表取消触摸操作,触摸流程结束
if (clickable) { setPressed(false);}removeTapCallback();removeLongPressCallback();mInContextButtonPress = false;mHasPerformedLongPress = false;mIgnoreNextUpEvent = false;mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
这里代码很简单,清除View状态
ACTION_UP
ACTION_UP代表触摸点抬起操作,同样是一个触摸流程的结束
case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp();//popWindow之类相关的,不必关心 } //清除触摸状态以及触摸回调和长按的回调 if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 当前视图处于预按下或者按下状态,如果失去焦点,获取焦点状态 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { //重设按下状态 setPressed(true, x, y); } //长按未触发 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { //移除长按回调 removeLongPressCallback(); //按下状态执行点击事件 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)) { //执行失败的话,保证视图不会永远处于按下状态 //直接执行一次 mUnsetPressedState.run(); } // 清除轻触回调 removeTapCallback(); } mIgnoreNextUpEvent = false;break;
这短代码是触摸流程里最重要的一部分。代码中已经注释清除,不必全部理解,理解流程即可。
到此,我们整个View的触摸流程就结束了,光看这部分还是很绕的,下一篇文章我们会详细讲讲ViewGroup中事件分发流程,并且和本篇做对照,可能会理解的更透彻。
enjoy~
我的个人博客
更多相关文章
- AsyncTask的使用和原理探究(一)
- Android蓝牙播放如何显示歌曲信息?
- 解锁Retrofit -- 浅析Retrofit源码
- Android笔试和面试常见题目(一)
- Activity life times——Android
- Android使用AIDL设计和调用远程接口
- 针对网上流传的"Android(安卓)再按一次后退键退出应用程序"方法
- android smack源码分析——接收消息以及如何解析消息
- Android的App Widget实现