Android事件的分发、拦截和执行

在平常的开发中,我们经常会遇到点击,滑动之类的事件。有时候不同的view之间也存在各种滑动冲突。比如布局的内外两层都能滑动的话,那么就会出现冲突了。这个时候我们就需要了解Android的事件分发机制。

介绍

Android的触摸事件分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。我先将这三个方法大体的介绍一下。

  • public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。ACTION_DOWN的dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

  • public boolean onInterceptTouchEvent(MotionEvent event)

这个方法是在dispatchTouchEvent方法中调用的,用来拦截某个事件的。如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回的结果表示是否拦截当前事件。它是ViewGroup提供的方法,默认返回false。

  • public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗掉当前事件(true表示消耗,false表示不消耗),如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。View和ViewGroup都有该方法,View默认返回true,表示消费了这个事件。

View里,有两个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

ViewGroup里,有三个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

上述三个方法中有什么区别和关系呢?下面用一段伪代码表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}  

通过上面的伪代码大家可能对点击事件的传递规则有了更清楚的认识,即:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true表示它要拦截此事件,接着这个事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截此事件,这是当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。

下面的几张图参考自[eoe]:

  • 图一:ACTION_DOWN都没被消费

  • 图二(一):ACTION_DOWN被View消费了

  • 图二(二):后续ACTION_MOVE和UP在不被拦截的情况下都会去找VIEW

  • 图三:后续的被拦截了

  • 图四:ACTION_DOWN一开始就被拦截

View事件分发源码分析

dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
} 

如果mOnTouchListener != null,(mViewFlags&ENABLED_MASK)==ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。

总结下来onTouch能够得到执行需要两个前提条件(都满足):

 1.设置了OnTouchListener
 2.控件是enable状态

而onTouchEvent能够得到执行满足以下三个条件任意一个即可:

 1.没有设置OnTouchListener
 2.控件不是enable状态
 3.onTouch返回false

再来看一下dispatchTouchEvent的返回值,它其实受onTouch和onTouchEvent函数的返回值控制,也就是说touch事件被成功消费返回true,它也就返回true,说明分发成功,此后的事件序列也会在此被分发,而如果返回false,则认为分发失败,此后的事件序列就不再分发下去了。

onTouchEvent方法

 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
       ...
       return true;
 }

View的onTouchEvent默认都会消耗掉事件(该方法返回true),除非它是不可点击的(clickable和longClickable同时为false)。并且View的longClickable默认为false,clickable属性要分情况,比如Button默认为true,TextView、ImageView默认为false。

public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }
    return false;
} 

这不就是我们熟悉的OnClickListener吗,它原来是在onTouchEvent中被调用的。只要mOnClickListener不是null,就会去调用它的onClick方法。

总结下来onClick能够得到执行需要两个前提条件(都满足):

 1.可以执行到onTouchEvent
 2.设置了OnClickListener

整个View的事件转发流程是:

dispatchEvent->setOnTouchListener->onTouchEvent->setOnClickListener

最后还有一个问题,setOnLongClickListener和setOnClickListener是否只能执行一个?

答:不是的,只要setOnLongClickListener中的onClick返回false,则两个都会执行;返回true则会屏蔽setOnClickListener。

ViewGroup事件分发源码分析

dispatchTouchEvent方法

    ...
           if (disallowIntercept || !onInterceptTouchEvent(ev)) {
               ev.setAction(MotionEvent.ACTION_DOWN);
               final int scrolledXInt = (int) scrolledXFloat;
               final int scrolledYInt = (int) scrolledYFloat;
               final View[] children = mChildren;
               final int count = mChildrenCount;  

               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))  {
                               // Event handled, we have a target now.
                               mMotionTarget = child;
                               return true;
                           }
                       }
                   }
               }
           } 

两种可能会进入if代码段(即事件被分发给子View):

1、当前不允许拦截,即disallowIntercept = true.
2、当前没有拦截,即onInterceptTouchEvent(ev)返回false.

注:disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,可以通过ViewGroup.requestDisallowInterceptTouchEvent(boolean)进行设置;而onInterceptTouchEvent(ev)可以进行复写。

进入if代码段后,通过一个for循环,遍历当前ViewGroup下的所有子View,判断当前遍历的View是不是正在点击的View,如果是的话就会调用该View的dispatchTouchEvent,就进入了View的事件分发流程了,上面有讲。当child.dispatchTouchEvent(ev)返回true,则为mMotionTarget=child;然后return true,说明ViewGroup的dispatchTouchEvent返回值受childView的dispatchTouchEvent返回值影响,子view事件分发成功,ViewGroup的事件分发才成功,此后的事件序列也会在此分发(从上面知:子view的clickable或longClickable为true都能分发成功),而如果ViewGroup事件分发失败或者没有找到子View(点击空白位置),则会走到它的onTouchEvent,以后的事件序列也不会分发下去,直接走onTouchEvent。

整个ViewGroup的事件转发流程是:

dispatchEvent->onInterceptTouchEvent->child.dispatchEvent->(setOnTouchListener->onTouchEvent)

上面的总结都是基于:如果没有拦截;那么如何拦截呢?

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
} 

代码很简单,只有一句,即返回false,ViewGroup默认是不拦截的。如果你需要拦截,只要return true就行了,这样该事件就不会往子View传递了,并且如果你在DOWN return true ,则DOWN,MOVE,UP子View都不会捕获到事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获到事件。

如何不被拦截:

如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;此时子View希望依然能够响应MOVE和UP时该咋办呢?

答:onInterceptTouchEvent是定义在ViewGroup中的,子View无法修改。Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

    @Override
        public boolean dispatchTouchEvent(MotionEvent event)
        {
            getParent().requestDisallowInterceptTouchEvent(true);
            int action = event.getAction();
            switch (action) {
            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);
        }  

getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。

注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是没有办法的捕获事件的!

总结

关于代码流程上面已经总结过了~

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;

好了,那么实际应用中能解决哪些问题呢?

比如你在ScrollView中嵌套了一个EditText,当EditText中文字内容太多超出范围时,你想上下滑动使EditText中文字滚动出来,却发现滚动的是ScrollView。这时我们设置EditText的onTouch事件,在onTouch中设置不让ScrollView拦截我的事件,最后在UP时把状态改回去。

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
        if ((view.getId() == R.id.tousuContentEditText && canVerticalScroll(tousuContentEditText))) {
            view.getParent().requestDisallowInterceptTouchEvent(true);
            if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                view.getParent().requestDisallowInterceptTouchEvent(false);
            }
        }
        return false;
    }

private boolean canVerticalScroll(EditText editText) {
        int scrollY = editText.getScrollY();
        int scrollRange = editText.getLayout().getHeight();
        int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() - editText.getCompoundPaddingBottom();
        int scrollDifference = scrollRange - scrollExtent;
        if (scrollDifference == 0) {
            return false;
        }
        return (scrollY > 0) || (scrollY < scrollDifference - 1);
    }
时间: 2024-08-28 10:13:00

Android事件的分发、拦截和执行的相关文章

图片会说话系列之Android事件的分发传递机制

在一些复杂布局中,经常会遇到事件冲突,事件失效等问题,这就需要我们深入理解Android事件的分发传递机制.最好的方法是自己写一个demo,打印事件相关的日志查看其运行流程,然后再结合源码去深入理解.当然这里不会做这些,这里只有总结性的东西,如果你喜欢这种东西,那么请继续往下看. 老规矩,先上图: 用户的交互一般发生在触摸屏上,因此Android事件的传递必然涉及到ViewGroup和View,ViewGroup与View包含的处理事件传递的方法分别如下: ViewGroup: <span st

Android事件总线分发库EventBus3.0的简单讲解与实践

Android事件总线分发库EventBus的简单讲解与实践 导语,EventBus大家应该不陌生,EventBus是一款针对Android优化的发布/订阅事件总线.主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息.优点是开销小,代码更优雅.以及将发送者和接收者解耦.反正能帮助我们快速开发,这个确实是个好东西,其实鸿洋大神已经对源码作了一个较全面的剖析了 Android EventBus源码解析 带你深入理解Ev

Android事件的分发机制

在分析Android事件分发机制前,明确android的两大基础控件类型:View和ViewGroup.View即普通的控件,没有子布局的,如Button.TextView. ViewGroup继承自View,表示可以有子控件,如Linearlayout.Listview这些.今天我们先来了解View的事件分发机制. 先看下代码,非常简单,只有一个Button,分别给它注册了OnClick和OnTouch的点击事件. 1 btn.setOnClickListener(new View.OnCli

45、Android事件总线分发库的使用

事件总线分发库EventBus和Otto的简介及对比 什么是事件总线管理: a.将事件放到队列里,用于管理和分发b.保证应用的各个部分之间高效的通信及数据.事件分发c.模块间解耦 Event Bus是一个发布 / 订阅的事件总线.Event Bus模式 — 也被称为Message Bus或者发布者/订阅者(publisher/subscriber)模式 — 可以让两个组件相互通信,但是他们之间并不相互知晓. 基于事件总线管理/订阅/分发模式的.事件响应有更多的线程选择,EventBus可以向不同

一步步理解Android事件分发机制

回想一下,通常在Android开发中,我们最常接触到的是什么东西?显然除了Activity以外,就是各种形形色色的控件(即View)了. 与此同时,一个App诞生的起因,终究是根据不同需求完成与用户的各种交互.而所谓的交互,本质就是友好的响应用户的各种操作行为. 所以说,有很多时候,一个控件(View)出现在屏幕当中,通常不会是仅仅为了摆设,而是还要能够负责响应用户的操作. 以最基本的例子而言:现在某一个界面中有一个按钮(Button),而每当用户点击了该按钮,我们的程序将做出一定回应. 那么,

android事件分发流程

1.描述 说到android事件的分发机制,真的是感觉既熟悉又陌生,因为每次需要用到的时候查看相关的源码,总能找到一些所以然来,但是要根据自己理解从头到尾说一遍,却一点都说不上.总结原因吧,感觉是自己不善于总结,过目就忘,并没有把心思放在上面,自然也就没有一点概念咯~~所以在这里主要是把自己理解的一些东西记录下来,不涉及源代码. 好吧,接下来简单说说android事件分发流程吧,说到事件分发,首先应该想到的是两个类,View和ViewGroup,ViewGroup是继承自View实现的,View

android事件之onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent,requestDisallowInterceptTouchEvent

android 的这个事件的分发传递,处理的解决方式, 实质应该是 java设计模式里面的 责任链模式了. 在这里,想用最少的话,最通俗易懂的方式记录 View的方法 // 事件分发,默认返回false public boolean dispatchTouchEvent(MotionEvent event) // 事件处理,默认返回false public boolean onTouchEvent(MotionEvent event) ViewGroup的方法 // 事件分发,默认返回false

android事件如何分发给子view

哈哈,第一次使用markdown,看着挺高大上的啊.如果顺手了,会直接切换默认为markdown. 话说关于android事件分发的博客真的不在少数,基本都是基于源码分析+实例代码的形式讲解.今天的这篇博客呢,主要的侧重点并不是在事件分发上,而是在事件的转换上. 为什么需要事件转换? 打个比方吧: 我们点击一个TextView的左上角,加入这个TextView在它老子的中间位置,那我们点击的x/y应该是多少呢? 在它老子那这两个值可能是100/100,而在TextView上打印就会是1/1了,也

android事件分发,拦截,处理

事件分发 android事件处理的时候 会根据事件发生的坐标,从父容器一直慢慢的发送到相关的所有的view 因此当都不处理的时候 事件传递的流程图 dispatchTouchEvent返回true 但是如果我们在A的dispatchTouchEvent 中返回true,那么也就是事件不进行分发 发现只是调用了ViewGroupA事件的拦截方法,也就是没有将事件进行分发,连自己的onTouchEvent事件都没有进行处理 如果让ViewGroupB的dispatchEvent返回true呢? 当我