事件分发机制之 源码解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事件分发机制之 源码解析相关的知识,希望对你有一定的参考价值。
事件的下发:dispatchTouchEvent
ViewGroup相关事件有三个:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent
View相关事件有两个:dispatchTouchEvent、onTouchEvent
简单来说就是:当一个Touch事件到达根节点时,它会依次【下发】,下发的过程是调用子View的dispatchTouchEvent方法实现的。
详细来说就是:ViewGroup遍历它包含着的【子View】,如果Touch事件在屏幕上的位置处于该子View的范围内,则调用此子View的dispatchTouchEvent方法;如果此子View为ViewGroup时,又会通过调用此子ViwGroup的dispatchTouchEvent方法继续调用其子View的dispatchTouchEvent方法;一旦发现某个View的dispatchTouchEvent方法返回值为true时,则下发过程结束。
注意:上面说的"结束"指的是"下发"过程结束,也即一旦某个View的dispatchTouchEvent方法返回值为true,则其子View就不可能会获取到任何Touch事件;但是此View的所有父View都可以获取到此View能获取到的任何事件,包括ACTION_DOWN、ACTION_UP 和ACTION_MOVE 。
View mTarget=null;//记录捕获Touch事件处理的那个View
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev.getAction()==KeyEvent.ACTION_DOWN){//当是DOWN事件时
if(!onInterceptTouchEvent()){//如果没有拦截
mTarget=null;//每次Down事件,都置为Null
View[] views=getChildView();
for(int i=0;i<views.length;i++){//逐个遍历所有子View
if(...){ //判断Touch事件在屏幕上的位置是否在该子View的范围内……
if(views[i].dispatchTouchEvent(ev)) mTarget=views[i];//判断该子View的dispatchTouchEvent方法是否返回true
return true;//如果返回true,则分发过程结束,且此ViewGroup的dispatchTouchEvent也返回true
}
}
//当子View没有捕获down事件时,ViewGroup自身处理。这里处理的Touch事件包含Down、Up和Move
if(mTarget==null) return super.dispatchTouchEvent(ev);
...
if(onInterceptTouchEvent()){ }
//这一步在Action_Down中是不会执行到的,只有Move和UP才会执行到。
return mTarget.dispatchTouchEvent(ev);
} //View
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) return false;
//如果mOnTouchListener不为null,并且view是enable的状态,并且mOnTouchListener的onTouch的返回值为true if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnTouchListener.onTouch(this, event)) {
return true;//返回true。也就是说,此时View自己的onTouchEvent方法就不会被执行了
}
return onTouchEvent(event);//否则,返回值完全由自己的onTouchEvent方法决定
}
在此可以看出,【ViewGroup】的dispatchTouchEvent是真正在执行事件【分发】工作,而【View】的dispatchTouchEvent方法只是调用了自己的【onTouchEvent】方法,此方法决定了View是否要处理此touch事件。一般情况下,我们不该在普通View内重写dispatchTouchEvent方法,因为它并不执行分发逻辑,它仅仅是返回了onTouchEvent方法的返回值而已,我们只需重写onTouchEvent方法即可。
事件的上传:onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch events, it just doesn‘t respond to them.
// 如果当前View是Disabled状态且是可点击则会消费掉事件
return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
// 如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true
if (mTouchDelegate != null && mTouchDelegate.onTouchEvent(event)) return true;
// 如果我们的View可以点击或者可以长按,则……注意if范围内,最终一定是 return true ;
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
// 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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) mPendingCheckForTap = new CheckForTap();
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button 判断当前触摸点有没有移出我们的View
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}关于对ACTION_DOWN的处理
当用户按下触发ACTION_DOWN后,首先会设置标识为PREPRESSED(预压),如果115ms后没有抬起,会去掉PREPRESSED标识并将View的标识设置为PRESSED(压),然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms),这个115ms刚好是检测PREPRESSED时间。
也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:
- 如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true才把mHasPerformedLongPress置为ture
- 否则,如果没有设置长按回调或者长按回调返回的是false,则mHasPerformedLongPress依然是false
关于对ACTION_MOVE的处理
首先拿到当前触摸的x,y坐标,然后判断当前触摸点有没有移出我们的View,如果移出了:
- 1、执行removeTapCallback(),移除DOWN触发时设置的PREPRESSED的检测,即当前触发时机在DOWN触发不到115ms时,你就已经移出控件外了;
- 2、然后判断是否包含PRESSED标识(如果115ms后才移出控件外,则包含),如果包含,调用removeLongPressCallback()移除长按的检查
- 3、然后把mPrivateFlags中PRESSED标识去除
- 4、最后刷新背景
简单说就是:只要用户移出了我们的控件,则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等。
关于对ACTION_UP的处理
如果mPrivateFlags包含PRESSED或者PREPRESSED则进入执行体,也就是无论是115ms内或者之后抬起都会进入执行体。
如果mHasPerformedLongPress没有被执行,则调用removeLongPressCallback()移除长按的检测;
如果mPerformClick为null则初始化一个实例,然后立即通过handler添加到消息队列尾部
- 如果添加失败则直接执行 performClick()
- 如果添加成功,则在mPerformClick的run方法中执行performClick()
下面看一下performClick()方法:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}久违了~我们的mOnClickListener ;
如果prepressed为true,为mPrivateFlags设置表示为PRESSED,刷新背景,125毫秒后执行mUnsetPressedState.run();
否则mUnsetPressedState.run()立即执行;也就是不管咋样,最后mUnsetPressedState.run()都会执行;
看看这个UnsetPressedState主要干什么:
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
if (pressed) mPrivateFlags |= PRESSED;
else mPrivateFlags &= ~PRESSED;
refreshDrawableState();
dispatchSetPressed(pressed);
}把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。
onTouch和onTouchEvent的区别
对于View,我们可以通过重写onTouchEvent方法来处理Touch事件,也可以通过实现OnTouchListener的接口,然后在onTouch方法中达到同样的目的,这两种监听有什么区别呢?
这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
总结
- 1、onTouchListener的onTouch方法优先级比onTouchEvent方法高,会先触发。
- 2、假如onTouch方法返回false会接着触发onTouchEvent方法,反之onTouchEvent方法不会被调用。
- 3、内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。
以上是关于事件分发机制之 源码解析的主要内容,如果未能解决你的问题,请参考以下文章
Android事件分发机制完全解析,带你从源码的角度彻底理解
Android View体系从源码解析View的事件分发机制