关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android ViewGroup 事件分发机制详解

    Android ViewGroup 事件分发机制详解

    2014-09-10 00:00:00 出处:博客园
    分享

    上一篇已经完整的解析了Android View的事件分发机制,今天给大家代码viewGroup事件分发的源码解析~~凡是自定义ViewGroup实现各种滑动效果的,不可避免的会出现很多事件的冲突,对ViewGroup事件分发机制的了解,也有益于大家了解冲突产生的原因,以及对冲突进行处理~

    1、案例

    首先我们接着上一篇的代码,在代码中添加一个自定义的LinearLayout:

    package com.example.zhy_event03;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.widget.LinearLayout;
    
    public class MyLinearLayout extends LinearLayout
    {
    	private static final String TAG = MyLinearLayout.class.getSimpleName();
    
    	public MyLinearLayout(Context context, AttributeSet attrs)
    	{
    		super(context, attrs);
    	}
    
    	@Override
    	public boolean dispatchTouchEvent(MotionEvent ev)
    	{
    		int action = ev.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(ev);
    	}
    
    	@Override
    	public boolean onTouchEvent(MotionEvent event)
    	{
    
    		int action = event.getAction();
    
    		switch (action)
    		{
    		case MotionEvent.ACTION_DOWN:
    			Log.e(TAG, "onTouchEvent ACTION_DOWN");
    			break;
    		case MotionEvent.ACTION_MOVE:
    			Log.e(TAG, "onTouchEvent ACTION_MOVE");
    			break;
    		case MotionEvent.ACTION_UP:
    			Log.e(TAG, "onTouchEvent ACTION_UP");
    			break;
    
    		default:
    			break;
    		}
    
    		return super.onTouchEvent(event);
    	}
    
    	@Override
    	public boolean onInterceptTouchEvent(MotionEvent ev)
    	{
    
    		int action = ev.getAction();
    		switch (action)
    		{
    		case MotionEvent.ACTION_DOWN:
    			Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
    			break;
    		case MotionEvent.ACTION_MOVE:
    			Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
    			break;
    		case MotionEvent.ACTION_UP:
    			Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
    			break;
    
    		default:
    			break;
    		}
    
    		return super.onInterceptTouchEvent(ev);
    	}
    
    	@Override
    	public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
    	{
    		Log.e(TAG, "requestDisallowInterceptTouchEvent ");
    		super.requestDisallowInterceptTouchEvent(disallowIntercept);
    	}
    
    }

    继承LinearLayout,然后复写了与事件分发机制有关的代码,添加上了日志的打印~

    然后看我们的布局文件:

    <com.example.zhy_event03.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity" >
    
        <com.example.zhy_event03.MyButton
            android:id="@+id/id_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="click me" />
    
    </com.example.zhy_event03.MyLinearLayout>

    MyLinearLayout中包含一个MyButton,MyButton都上篇博客中已经出现过,这里就不再贴代码了,不清楚可以去查看~

    然后MainActivity就是直接加载布局,没有任何代码~~~

    直接运行我们的代码,然后点击我们的Button,依然是有意的MOVE一下,不然不会触发MOVE事件,看一下日志的输出:

    09-06 09:57:27.287: E/MyLinearLayout(959): dispatchTouchEvent ACTION_DOWN
    09-06 09:57:27.287: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_DOWN
    09-06 09:57:27.287: E/MyButton(959): dispatchTouchEvent ACTION_DOWN
    09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_DOWN
    09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_MOVE
    09-06 09:57:27.327: E/MyLinearLayout(959): dispatchTouchEvent ACTION_MOVE
    09-06 09:57:27.327: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_MOVE
    09-06 09:57:27.337: E/MyButton(959): dispatchTouchEvent ACTION_MOVE
    09-06 09:57:27.337: E/MyButton(959): onTouchEvent ACTION_MOVE
    09-06 09:57:27.457: E/MyLinearLayout(959): dispatchTouchEvent ACTION_UP
    09-06 09:57:27.457: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_UP
    09-06 09:57:27.457: E/MyButton(959): dispatchTouchEvent ACTION_UP
    09-06 09:57:27.457: E/MyButton(959): onTouchEvent ACTION_UP

    可以看到大体的事件流程为:

     

    MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

    可以看出,在View上触发事件,最先捕获到事件的为View所在的ViewGroup,然后才会到View自身~

    下面我们按照日志的输出,进入源码~

    2、源码分析

    ViewGroup - dispatchTouchEvent

    1、ViewGroup - dispatchTouchEvent – ACTION_DOWN

    首先是ViewGroup的dispatchTouchEvent方法:

     @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (!onFilterTouchEventForSecurity(ev)) {
                return false;
            }
    
            final int action = ev.getAction();
            final float xf = ev.getX();
            final float yf = ev.getY();
            final float scrolledXFloat = xf + mScrollX;
            final float scrolledYFloat = yf + mScrollY;
            final Rect frame = mTempRect;
    
            boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    
            if (action == MotionEvent.ACTION_DOWN) {
                if (mMotionTarget != null) {
                    // this is weird, we got a pen down, but we thought it was
                    // already down!
                    // XXX: We should probably send an ACTION_UP to the current
                    // target.
                    mMotionTarget = null;
                }
                // If we're disallowing intercept or if we're allowing and we didn't
                // intercept
                if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                    // reset this event's action (just to protect ourselves)
                    ev.setAction(MotionEvent.ACTION_DOWN);
                    // We know we want to dispatch the event down, find a child
                    // who can handle it, start with the front-most child.
                    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)) {
                                // offset the event to the view's coordinate system
                                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;
                                }
                                // The event didn't get handled, try the next view.
                                // Don't reset the event's location, it's not
                                // necessary here.
                            }
                        }
                    }
                }
            }                                                                                                                                                  ....//other code omitted

    代码比较长,决定分段贴出,首先贴出的是ACTION_DOWN事件相关的代码。

    16行:进入ACTION_DOWN的处理

    17-23行:将mMotionTarget置为null

    26行:进行判断:if(disallowIntercept || !onInterceptTouchEvent(ev))

    两种可能会进入IF代码段

    1、当前不允许拦截,即disallowIntercept =true,

    2、当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;

    注:disallowIntercept 可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置,后面会详细说;而onInterceptTouchEvent(ev)可以进行复写。

    36-57行:开始遍历所有的子View

    41行:进行判断当前的x,y坐标是否落在子View身上,假如在,47行,执行child.dispatchTouchEvent(ev),就进入了View的dispatchTouchEvent代码中了,假如不了解请参考:Android View的事件分发机制,当child.dispatchTouchEvent(ev)返回true,则为mMotionTarget=child;然后return true;

    ViewGroup的ACTION_DOWN分析结束,总结一下:

    ViewGroup实现捕获到DOWN事件,假如代码中不做TOUCH事件拦截,则开始查找当前x,y是否在某个子View的区域内,假如在,则把事件分发下去。

    按照日志,接下来到达ACTION_MOVE

    2、ViewGroup - dispatchTouchEvent – ACTION_MOVE

    首先我们源码进行删减,只留下MOVE相关的代码:

     @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            final int action = ev.getAction();
            final float xf = ev.getX();
            final float yf = ev.getY();
            final float scrolledXFloat = xf + mScrollX;
            final float scrolledYFloat = yf + mScrollY;
            final Rect frame = mTempRect;
    
            boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    
           //...ACTION_DOWN
    
           //...ACTIN_UP or ACTION_CANCEL
    
            // The event wasn't an ACTION_DOWN, dispatch it to our target if
            // we have one.
    	final View target = mMotionTarget;
    
            // if have a target, see if we're allowed to and want to intercept its
            // events
            if (!disallowIntercept && onInterceptTouchEvent(ev)) {
                //....
            }
            // finally offset the event to the target's coordinate system and
            // dispatch the event.
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            ev.setLocation(xc, yc);
    
            return target.dispatchTouchEvent(ev);
        }

    18行:把ACTION_DOWN时赋值的mMotionTarget,付给target ;

    23行:if (!disallowIntercept && onInterceptTouchEvent(ev)) 当前允许拦截且拦截了,才进入IF体,当然了默认是不会拦截的~这里执行了onInterceptTouchEvent(ev)

    28-30行:把坐标系统转化为子View的坐标系统

    32行:直接return target.dispatchTouchEvent(ev);

    可以看到,正常流程下,ACTION_MOVE在检测完是否拦截以后,直接调用了子View.dispatchTouchEvent,事件分发下去;

    最后就是ACTION_UP了

    3、ViewGroup - dispatchTouchEvent – ACTION_UP

     public boolean dispatchTouchEvent(MotionEvent ev) {
            if (!onFilterTouchEventForSecurity(ev)) {
                return false;
            }
    
            final int action = ev.getAction();
            final float xf = ev.getX();
            final float yf = ev.getY();
            final float scrolledXFloat = xf + mScrollX;
            final float scrolledYFloat = yf + mScrollY;
            final Rect frame = mTempRect;
    
            boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    
            if (action == MotionEvent.ACTION_DOWN) {...}
    
    	boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                    (action == MotionEvent.ACTION_CANCEL);
    
    	if (isUpOrCancel) {
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            }
    	final View target = mMotionTarget;
    	if(target ==null ){...}
    	if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}
    
            if (isUpOrCancel) {
                mMotionTarget = null;
            }
    
            // finally offset the event to the target's coordinate system and
            // dispatch the event.
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            ev.setLocation(xc, yc);
    
            return target.dispatchTouchEvent(ev);
        }

    17行:判断当前是否是ACTION_UP

    21,28行:分别重置拦截标志位以及将DOWN赋值的mMotionTarget置为null,都UP了,当然置为null,下一次DOWN还会再赋值的~

    最后,修改坐标系统,然后调用target.dispatchTouchEvent(ev);

     

    正常情况下,即我们上例整个代码的流程我们已经走完了:

    1、ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,假如没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent

    2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,假如没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

    3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,假如没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

    当然了在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,然后传给child;

    3、关于拦截

    1、如何拦截

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

    复写ViewGroup的onInterceptTouchEvent方法:

    @Override
    	public boolean onInterceptTouchEvent(MotionEvent ev)
    	{
    		int action = ev.getAction();
    		switch (action)
    		{
    		case MotionEvent.ACTION_DOWN:
    			//假如你觉得需要拦截
    			return true ; 
    		case MotionEvent.ACTION_MOVE:
    			//假如你觉得需要拦截
    			return true ; 
    		case MotionEvent.ACTION_UP:
    			//假如你觉得需要拦截
    			return true ; 
    		}
    
    		return false;
    	}

    默认是不拦截的,即返回false;假如你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且假如你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;假如你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。

    原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

    2、如何不被拦截

    假如ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;

    此时子View希望依然能够响应MOVE和UP时该咋办呢?

    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 MOVE和UP拦截的源码是这样的:

    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
                final float xc = scrolledXFloat - (float) target.mLeft;
                final float yc = scrolledYFloat - (float) target.mTop;
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                ev.setAction(MotionEvent.ACTION_CANCEL);
                ev.setLocation(xc, yc);
                if (!target.dispatchTouchEvent(ev)) {
                    // target didn't handle ACTION_CANCEL. not much we can do
                    // but they should have.
                }
                // clear the target
                mMotionTarget = null;
                // Don't dispatch this event to our own view, because we already
                // saw it when intercepting; we just want to give the following
                // event to the normal onTouchEvent().
                return true;
            }

    当我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了~

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

    4、假如没有找到合适的子View

    我们的实例,直接点击ViewGroup内的按钮,当然直接很顺利的走完整个流程;

    但是有两种特殊情况

    1、ACTION_DOWN的时候,子View.dispatchTouchEvent(ev)返回的为false ;

    假如你仔细看了,你会注意到ViewGroup的dispatchTouchEvent(ev)的ACTION_DOWN代码是这样的

      if (child.dispatchTouchEvent(ev))  {
                 // Event handled, we have a target now.
                 mMotionTarget = child;
                 return true;
      }

    只有在child.dispatchTouchEvent(ev)返回true了,才会认为找到了能够处理当前事件的View,即mMotionTarget = child;

    但是假如返回false,那么mMotionTarget 依然是null

    mMotionTarget 为null会咋样呢?

    其实ViewGroup也是View的子类,假如没有找到能够处理该事件的子View,或者干脆就没有子View;

    那么,它作为一个View,就相当于View的事件转发了~~直接super.dispatchTouchEvent(ev);

    源码是这样的:

     final View target = mMotionTarget;
            if (target == null) {
                // We don't have a target, this means we're handling the
                // event as a regular view.
                ev.setLocation(xf, yf);
                if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                }
                return super.dispatchTouchEvent(ev);
            }

    我们没有一个能够处理该事件的目标元素,意味着我们需要自己处理~~~就相当于传统的View~

    2、那么什么时候子View.dispatchTouchEvent(ev)返回的为true

    假如你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件一定返回true~~

    源码是这样的:

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

    5、总结

     

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

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

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

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

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

    比如你需要写一个类似SlidingMenu的左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~ 你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单,假如是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理,这样自己的onTouchEvent就可以顺利展现出菜单栏了。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    SQL汉字转换为拼音的函数

    windows 7系统无法运行Photoshop CS3的解决方法

    巧用MySQL加密函数对Web网站敏感数据进行保护

    MySQL基础知识简介

    Windows7和WinXP下如何实现不输密码自动登录系统的设置方法介绍

    windows 7系统ip地址冲突怎么办?windows 7系统IP地址冲突问题的

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。