详解Android事件的分发、拦截和执行_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);
 }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索Android事件分发
, Android事件拦截
Android事件执行
android 事件分发拦截、android事件分发机制、事件分发机制、android事件分发、view事件分发机制,以便于您获取更多的相关知识。

时间: 2025-01-20 13:46:31

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

详解Android中Handler的内部实现原理_Android

本文主要是对Handler和消息循环的实现原理进行源码分析,如果不熟悉Handler可以参见博文<详解Android中Handler的使用方法>,里面对Android为何以引入Handler机制以及如何使用Handler做了讲解. 概括来说,Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制.我们在使用Handler的时候与Message打交道最多,Message是Hanlder机制向开发人员暴露出来的相关类,可以通过Message类完成大部分操作Handler的功

详解Android Material Design自定义动画的编写_Android

新的动画Api,让你在UI控件里能创建触摸反馈,改变View的状态,切换activity的一系列自定义动画 具体有: 响应View的touch事件的触摸反馈动画 隐藏和显示View的循环展示动画 两个Activity间的切换动画 更自然的曲线运动的动画 使用View的状态更改动画,能改变一个或多个View的属性 在View的状态更改时显示状态列表动画 这些new animations Api,已内置在标准Widget中,如Button.在自定义view时也可使用这些api 动画在Material

详解Android首选项框架的使用实例_Android

首选项这个名词对于熟悉Android的朋友们一定不会感到陌生,它经常用来设置软件的运行参数. Android提供了一种健壮并且灵活的框架来处理首选项.它提供了简单的API来隐藏首选项的读取和持久化,并且提供了一个优雅的首选项界面. 首先,我们来看下面这款软件的首选项界面: 这款软件使用了好几种类型的首选项,每一种首选项都有其独特的用法,下面我们来了解一下几种常见的首选项: CheckBoxPreference:用来打开或关闭某个功能 ListPreference:用来从多个选项中选择一个值: E

详解Android冷启动实现APP秒开的方法_Android

一.前言 在阅读这篇文章之前,首先需要理解几个东西: 1.什么是Android的冷启动时间?       冷启动时间是指用户从手机桌面点击APP的那一刻起到启动页面的Activity调用onCreate()方法之间的这个时间段. 2.在冷启动的时间段内发生了什么?       首先我们要知道当打开一个Activity的时候发生了什么,在一个Activity打开时,如果该Activity所属的Application还没有启动,那么系统会为这个Activity创建一个进程(每创建一个进程都会调用一次

详解Android应用中DialogFragment的基本用法_Android

DialogFragment的基本用法 1. 创建DialogFragment public class DialogA extends DialogFragment implements DialogInterface.OnClickListener { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder

详解Android数据存储之SQLCipher数据库加密_Android

前言: 最近研究了Android Sqlite数据库以及ContentProvider程序间数据共享,我们清晰的知道Sqlite数据库默认存放位置data/data/pakage/database目录下,对于已经ROOT的手机来说的没有任何安全性可以,一旦被利用将会导致数据库数据的泄漏,所以我们该如何避免这种事情的发生呢?我们尝试这对数据库进行加密. 选择加密方案:  1.)第一种方案  我们可以对数据的数据库名,表名,列名就行md5,对存储的数据进行加密,例如进行aes加密(Android数据

详解Android开发中ContentObserver类的使用_Android

ContentObserver--内容观察者,目的是观察(捕捉)特定Uri引起的数据库的变化,继而做一些相应的处理,它类似于 数据库技术中的触发器(Trigger),当ContentObserver所观察的Uri发生变化时,便会触发它.触发器分为表触发器.行触发器, 相应地ContentObserver也分为"表"ContentObserver."行"ContentObserver,当然这是与它所监听的Uri MIME Type有关的. 熟悉Content Prov

详解Android中实现热更新的原理_Android

这篇文章就来介绍一下Android中实现热更新的原理. 一.ClassLoader 我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoader其实实现的就是简单的从文件系统中加载类文件.PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法

实例详解Android解决按钮重复点击问题_Android

 为了防止用户或者测试MM疯狂的点击某个button,写个方法防止按钮连续点击.具体实例代码如下所示: public class BaseActivity extends Activity { protected boolean isDestroy; //防止重复点击设置的标志,涉及到点击打开其他Activity时,将该标志设置为false,在onResume事件中设置为true private boolean clickable=true; @Override protected void o