详解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);
 }

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

(0)

相关推荐

  • Android View 事件分发机制详解

    Android开发,触控无处不在.对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑.View事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了.我以前也看过很多人写的这方面的文章,不是说的太啰嗦就是太模糊,还有一些在细节上写的也有争议,故再次重新整理一下这块内容,十分钟让你搞明白View事件的分发机制. 说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),o

  • Android事件分发机制的详解

    Android事件分发机制 我们只考虑最重要的四个触摸事件,即:DOWN,MOVE,UP和CANCEL.一个手势(gesture)是一个事件列,以一个DOWN事件开始(当用户触摸屏幕时产生),后跟0个或多个MOVE事件(当用户四处移动手指时产生),最后跟一个单独的UP或CANCEL事件(当用户手指离开屏幕或者系统告诉你手势(gesture)由于其他原因结束时产生).当我们说到"手势剩余部分"时指的是手势后续的MOVE事件和最后的UP或CANCEL事件. 在这里我也不考虑多点触摸手势(我

  • Android View的事件分发机制

    一.Android View框架提供了3个对事件的主要操作概念. 1.事件的分发机制,dispatchTouchEvent.主要是parent根据触摸事件的产生位置,以及child是否愿意负责处理该系列事件等状态,向其child分发事件的机制. 2.事件的拦截机制,onInterceptTouchEvent.主要是parent根据它内部的状态.或者child的状态,来把事件拦截下来,阻止其进一步传递到child的机制. 3.事件的处理机制,onTouchEvent.主要是事件序列的接受者(可以是

  • Android事件分发机制(上) ViewGroup的事件分发

    综述 Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理.在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View.对于事件可以通过ViewGroup下发到它的子View并交由子View进行处理,而ViewGroup本身也能够对事件做出处理.下面就来详细分析一下ViewGroup对时间的分发处理. MotionEvent 当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成.

  • Android View事件分发机制详解

    准备了一阵子,一直想写一篇事件分发的文章总结一下,这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等,这些子View的外层还有ViewGroup,如RelativeLayout,LinearLayout.作为一个开发者,我们会思考,当点击一个按钮,Android系统是怎样确定我点的就是按钮而不是TextView的?然后还正确的响应了按钮的点击事件.内部经过了一系列什么过程呢? 先铺垫一些知识能更加清晰的理解事件分发机制: 1. 通过setC

  • Android事件分发机制(下) View的事件处理

    综述 在上篇文章Android中的事件分发机制(上)--ViewGroup的事件分发中,对ViewGroup的事件分发进行了详细的分析.在文章的最后ViewGroup的dispatchTouchEvent方法调用dispatchTransformedTouchEvent方法成功将事件传递给ViewGroup的子View.并交由子View进行处理.那么现在就来分析一下子View接收到事件以后是如何处理的. View的事件处理 对于这里描述的View,它是ViewGroup的父类,并不包含任何的子元

  • Android Touch事件分发过程详解

    本文以实例形式讲述了Android Touch事件分发过程,对于深入理解与掌握Android程序设计有很大的帮助作用.具体分析如下: 首先,从一个简单示例入手: 先看一个示例如下图所示: 布局文件 : <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id=&

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

    在平常的开发中,我们经常会遇到点击,滑动之类的事件.有时候不同的view之间也存在各种滑动冲突.比如布局的内外两层都能滑动的话,那么就会出现冲突了.这个时候我们就需要了解Android的事件分发机制. Android的触摸事件分发过程由三个很重要的方法来共同完成:dispatchTouchEvent.onInterceptTouchEvent.onTouchEvent.我先将这三个方法大体的介绍一下. •public boolean dispatchTouchEvent(MotionEvent

  • 详解 Android中Libgdx使用ShapeRenderer自定义Actor解决无法接收到Touch事件的问题

    详解 Android中Libgdx使用ShapeRenderer自定义Actor解决无法接收到Touch事件的问题 今天在项目中实现了一个效果,主要是画一个圆.为了后续使用方便,将这个圆封装在一个自定义Actor(CircleActot)中,后续想显示一个圆的时候,只要创建一个CircleActor中即可. 部分代码如下所示: package com.ef.smallstar.unitmap.widget; import android.content.res.Resources; import

  • 详解Android Libgdx中ScrollPane和Actor事件冲突问题的解决办法

    详解Android Libgdx中ScrollPane和Actor事件冲突问题的解决办法 在Libgdx的使用过程中,经常会用到ScrollPane这个widget,来实现滑动效果, 如下所示: 但是如果想在上面的效果上添加一点扩展,比如ScrollPane中的Actor可以从ScrollPane中移出来,并添加到Stage中,则需要添加额外的逻辑 具体代码参考如下: /** * Created by Danny.姜 on 17/7/26. */ public class TestAdapter

  • 详解Android中的Service

    Service简介: Service是被设计用来在后台执行一些需要长时间运行的操作. Android由于允许Service在后台运行,甚至在结束Activity后,因此相对来说,Service相比Activity拥有更高的优先级. 创建Service: 要创建一个最基本的Service,需要完成以下工作:1)创建一个Java类,并让其继承Service 2)重写onCreate()和onBind()方法 其中,onCreate()方法是当该Service被创建时执行的方法,onBind()是该S

  • 详解Android中获取软键盘状态和软键盘高度

    详解Android中获取软键盘状态和软键盘高度 应用场景 在Android应用中有时会需要获取软键盘的状态(即软键盘是显示还是隐藏)和软键盘的高度.这里列举了一些可能的应用场景. 场景一 当软键盘显示时,按下返回键应当是收起软键盘,而不是回退到上一个界面,但部分机型在返回键处理上有bug,按下返回键后,虽然软键盘会自动收起,但不会消费返回事件,导致Activity还会收到这次返回事件,执行回退操作,这时就需要判断,如果软键盘刚刚由显示变为隐藏状态,就不执行回退操作. 场景二 当软键盘弹出后,会将

  • 详解Android中fragment和viewpager的那点事儿

    在之前的博文<Android 中使用 ViewPager实现屏幕页面切换和页面轮播效果>和<详解Android中Fragment的两种创建方式>以及<Android中fragment与activity之间的交互(两种实现方式)>中我们介绍了ViewPager以及Fragment各自的使用场景以及不同的实现方式. 那如果将他们两结合起来,会不会擦出点火花呢,答案是肯定的.之前在介绍ViewPager时,我们实现了多个ImageView的切换,并配合更新导航原点的状态.那我

  • 详解Android 基于TCP和UDP协议的Socket通信

    本来想讲一下基础的网络通信方面的知识点,发现太枯燥乏味了,不过笔试中也经常会问到这方面的问题,所以关于通信方面的知识点,小编会放到面试中去,因为实战中也就面试会用到这方面知识点 Android与服务器的通信方式主要有两种,一是Http通信,一是Socket通信.两者的最大差异在于,http连接使用的是"请求-响应方式",即在请求时建立连接通道,当客户端向服务器发送请求后,服务器端才能向客户端返回数据. 而Socket通信中基于TCP/IP协议的通信则是在双方建立起连接后就可以直接进行数

  • vue组件详解之使用slot分发内容

    一.什么是slot 在使用组件时,我们常常要像这样组合它们: <app> <app-header></app-header> <app-footer></app-footer> </app> 当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot , 这个过程叫作内容分发( transclusion ). 注意两点: 1.< app>组件不知道它的挂载点会有什么内容.挂载点的内容是由<app >

  • 详解Android项目多服务端接口适配(超简单)

    现状 Android项目如果是多服务端接口时,一般怎么弄呢? 方法1:服务器地址放在Header中 把服务器地址放在接口Header中,然后通过拦截器来动态修改请求地址而实现的.除了默认服务器的接口,其它都要加一个Header,有点麻烦.看起来也不爽,不简洁. interface ApiHeaderCase { /************************** server A ****************************/ @Headers("host:$SERVER_HOS

  • 详解Android观察者模式的使用与优劣

    一.简介 观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象.这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己.该模式一个重要作用就是解耦,将被观察者和观察者进行解耦,使他们之间的依赖性更小 二.使用场景 关联行为场景,需要注意的是关联行为是可拆分的而不是"组合"关系 事件多级触发场景 跨系统的消息交换场景,如消息队列.事件总线的处理机制 三.简单实

随机推荐