android触摸的分配机制,Android触摸事件分发机制

用户在操作的时候,不可避免地就会触发触摸事件。Android把触摸过程分成很多个动作(Action),而开发中最常见也最主要考虑的触摸过程就是:从ACTION_DOWN(触摸落下)开始、到ACTION_UP(触摸弹起)/ACTION_CANCEL(触摸取消,譬如在按下控件,将控件移动到外层控件的时候,就会触发,而不是UP)结束,在这之间的是ACTION_MOVE(触摸移动,非必然存在)。

同时,我们在界面上触发触摸事件的时候,同样不可避免地会涉及到这三部分:Activity(可视界面当然还有Fragment,Dialog这些,但它们都依附着Activity),ViewGroup(通常来说做布局容器的LinearLayout、RelativeLayout等都算是),View(Button、ImageView等等)。一个简单的情况就是我们在点击界面(Activity)上在布局(ViewGroup)中的Button(View)的时候,这必然触发了触摸事件,但这具体是怎样的一个过程?我们可以在这些过程中做些什么?这就涉及到了Android触摸事件分发机制,先看一张简略的分发流程图(图片来源于从Android源码的角度理解应用开发(1)-Touch机制,感谢,侵删):

7668ae0337e2

简单的Touch事件传递过程

从流程图可以得知,Touch事件的分发情况是这样的,Activity将事件分发到ViewGroup中,而ViewGroup层层分发直到找到需要处理Touch事件的子元素(可能是View也可能是ViewGroup),将事件传递下去。这里也可以提前告知一个逻辑,就算传递到了,但如果子元素不能处理触摸事件,会将事件交回上一级处理,最后可以到Activity去处理,总之触摸事件最终会被消费掉。

先让我们来了解一下Touch事件分发和处理的三个重要方法。

1.public boolean dispatchTouchEvent(MotionEvent ev)

MotionEvent-手势事件,它里面就包含了上面说的Action。这个方法从字面上的意思都很好理解,调度触摸事件,这个方法是Activity、ViewGroup、View都有的,Touch事件都从它开始,也就是说Touch事件的分发和处理过程中,dispatchTouchEvent()是第一个被调用的方法。

2.public boolean onInterceptTouchEvent(MotionEvent ev)

也是从字面意思上就很好理解了,拦截触摸事件。如果返回值为true,就拦截当前事件,不分发给子元素。很明显这个方法View是没有的,可以说三者中只有ViewGroup才有,因为Activity肯定要把事件分发下去(后面有说到),而View下面是没有子元素的,要么处理触摸事件要么交回ViewGroup处理。

3.public boolean onTouchEvent(MotionEvent ev)

这个方法表示对事件进行处理,在dispatchTouchEvent方法内部调用,如果返回true表示消耗当前事件,如果返回false表示不消耗当前事件。

为了更好地理解整个流程,我们从View——>ViewGroup——>Activity的顺序展开,让我们先来看看是怎么处理Touch事件的,再看具体是怎么分发的。就以上面说到的按钮点击的情况来说一说。

View

上面的按钮点击中,View就是Button,刚刚说到整个过程最先被调用的就是dispatchTouchEvent(),Button的dispatchTouchEvent()是调用View的,我们来看一下这个方法:

7668ae0337e2

View的dispatchTouchEvent()实现

可以看到,if语句的判断条件有三个部分,只要有一部分条件是false,那么就会执行onTouchEvent()。

1.判断是否有注册触摸监听事件;

2.判断控件是否可用(ENABLE),一般控件是默认可用的,除非通过setEnable(false)禁用控件,不然这部分条件一般为true;

3.监听事件里面的onTouch()的返回值。

所以很简单的,控件监听触摸事件让onTouch()返回true,就不会再执行下去了,当然我们是要看下去的,看一下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.

return (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));

}

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

return true;

}

}

if (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

case MotionEvent.ACTION_UP:

...

if (mPerformClick == null) {

mPerformClick = new PerformClick();

}

if (!post(mPerformClick)) {

performClick();

}

...

break;

case MotionEvent.ACTION_DOWN:

...

mHasPerformedLongPress = false;

postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

break;

case MotionEvent.ACTION_CANCEL:

...

break;

case MotionEvent.ACTION_MOVE:

...

break;

}

return true;

}

return false;

}

这里我们可以简单分析一下,这里面主要有三块if语句:

1.如果控件是禁用(DISABLED)的,控件可以点击(CLICKABLE)或长按点击(LONG_CLICKABLE),返回true,消费事件但不做任何操作,如果控件不可点击,onTouchEvent()返回false;

2.如果存在触摸委托对象(TouchDelegate),交由其onTouchEvent()处理;

3.首先可以看到,只要控件是不可点击的,不满足if语句条件,直接返回false;反之,再处理不同的ACTION,都会返回true。

有意思的是,继续深入查看源码可以发现,控件的onLongClick事件是在ACTION_DOWN里面触发的(postDelayed())、onClick事件是在ACTION_UP里面触发的(performClick()),这里不展开说。

上面说了那么多,好像和分发没什么关系啊?别急,我们可以知道View的Touch事件其实都是为了知道View的dispatchTouchEvent()的返回值是什么,而这个值与分发事件大有关联,请看ViewGroup。

ViewGroup

从上面简略的流程图可以看出,触摸事件由ViewGroup传递给View,很显然,这就是一个事件分发过程,那么,这个过程是怎么做的呢?

ViewGroup首先调用dispatchTouchEvent(),让我们来看一看具体的实现:

public boolean dispatchTouchEvent(MotionEvent ev) {

...

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (action == MotionEvent.ACTION_DOWN) {

if (mMotionTarget != null) {

mMotionTarget = null;

}

if (disallowIntercept || !onInterceptTouchEvent(ev)) {

...

for (int i = count - 1; i >= 0; i--) {

final View child = children[i];

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE

|| child.getAnimation() != null) {

child.getHitRect(frame);

if (frame.contains(scrolledXInt, scrolledYInt)) {

final float xc = scrolledXFloat - child.mLeft;

final float yc = scrolledYFloat - child.mTop;

ev.setLocation(xc, yc);

child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

if (child.dispatchTouchEvent(ev)) {

mMotionTarget = child;

return true;

}

}

}

}

}

}

...

if (target == null) {

...

return super.dispatchTouchEvent(ev);

}

if (!disallowIntercept && onInterceptTouchEvent(ev)) {

...

return true;

}

if (isUpOrCancel) {

mMotionTarget = null;

}

...

return target.dispatchTouchEvent(ev);

}

我们可以看到,首先对ACTION_DOWN做了一大堆处理,来看一下具体在干嘛。

首先清除手势目标;

第二个是重点:

disallowIntercept-禁止拦截,这个变量是一个Boolean值,默认值是false,意味着一般情况下是不禁止拦截功能的;

onIntercepTouchEvent()-这个方法在上面也提到过,拦截事件分发下去,将触摸事件交给ViewGroup自己处理。

很好,那么

if (disallowIntercept || !onInterceptTouchEvent(ev))

这个条件语句的意思是只要不拦截,就进入条件判断内部,它的逻辑运算符是||,只要disallIntercept为true或者!onInterceptTouchEvent(ev)为true,也就是onInterceptTouchEvent(ev)为false就好了。

第一个条件,disallIntercept默认为false,但我们可以通过requestDisallowInterceptTouchEvent(Boolean)使得它的值为true;

第二个条件,看一下onInterceptTouchEvent(ev)的源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {

return false;

}

看!它的返回值是默认false的!也就是!onInterceptTouchEvent(ev)为true,那么也就是默认情况下,并不拦截事件,进入条件判断内部。

这一大块代码具体做了什么?

1.获得ViewGroup下的子View

2.for循环判断,手指落下的范围在哪个View之中,接着!

if (child.dispatchTouchEvent(ev)) {

mMotionTarget = child;

return true;

}

上面说了那么多View里面获取dispatchTouchEvent()的返回值,在这里用上了,确定分发目标为dispatchTouchEvent()返回值为true的子View!同时,ViewGroup的dispatchTouchEvent()的返回值也是true,那么ACTION_DOWN就结束了,再次进来就是其它ACTION的执行逻辑了。

所以如果,View的dispatchTouchEvent()返回值为false,target为空,那么可以看到往下的逻辑里面,将自身作为一个View设为target:

if (target == null) {

···

return super.dispatchTouchEvent(ev);

}

也就是View的onTouch()返回值为true,View禁用,View的onTouchEvent()返回false等种种情况都会导致把事件重新交回给ViewGroup,然后ViewGroup执行super.dispatchTouchEvent(ev),ViewGroup的超类是View,又回到了前面说的View的内容了。

总结(来自于从Android源码的角度理解应用开发(1)-Touch机制,很精准简练的总结,感谢,侵删):

1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target;

2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch());

3.如果有Target并且拦截,则发送Cancel给子View ;

4.如果有Target并且不拦截,则调用Target的dispatchTouch;

5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)。

接着就剩下最后一块了,我们来看一下Activity又是怎么样把事件分发给ViewGroup的。

Activity

还是先看dispatchTouchEvent():

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

代码很简单,但是其实经过一系列的步骤之后,传递过程是这样的:

Activity->Window->DecorView->ViewGroup,同理,如果最后Touch事件没有被消费,也会交回由Activity的onTouchEvent()里面去处理。

补充: ACTION的传递过程:

对于在onTouchEvent消费事件的情况:

在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:

ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值