您的位置:首页 > 移动开发 > Android开发

Android事件分发传递机制的领悟和理解

2017-06-13 16:43 323 查看
 


(此文章是以发表日期的两年前所写,但至今来看仍不过时,所以再在此发表)

这两天在做这个美女图片软件时,为了实现一个需求,遇到了由于事件分发传递机制引起的种种异常、难题和BUG,对事件分发传递有了进一步的理解,悟出一种重写事件分发的最佳实践(个人认为的最佳方法)。。

需求

如图,主界面是由三个ListView和一个标题栏组成的,三个ListView都可以自由上下滑动,现有一个需求:
当手指处于中间ListView的上半部分滑动时,旁边两个ListView也要向相同方向跟随中间的ListView滑动。

看似简单的需求,却可以引发种种BUG,这里就记录下这些问题产生的原因和解决的方法。。

首先,我们要明白Android事件传递的流程
dispatchTouchEvent(MotionEvent ev)  →  onInterceptTouchEvent(MotionEvent ev)  →  onTouchEvent(MotionEvent
ev)
由父控件向子控件一层一层向下传递,经过 (事件分发  →  事件拦截  →  事件处理)流程,这里不对此作详细介绍

解决方案

这里简称左侧ListView为lv1,中间ListVIew为lv2,右边ListView为lv3
最容易想到的解决方案就是重写dispatchTouchEvent()方法,手动为三个ListVIew分发事件
在Activirt中重写dispatchTouchEvent()方法,代码如下:

 
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 判断触摸点坐标x值如果小于lv1的宽度,则手指触摸的是lv1,这个事件应分发给lv1进行处理
        if (ev.getRawX() < lv1.getWidth()) {
            lv1.dispatchTouchEvent(ev);
        // 判断触摸点坐标x如果小于lv1的宽度 * 2,则手指触摸的是lv3,这个事件应分发给lv3进行处理
        } else if (ev.getRawX() > lv1.getWidth() * 2) {
            lv3.dispatchTouchEvent(ev);
        } else {
            // 否则触摸的就是lv2,事件将被分发给lv2处理
            lv2.dispatchTouchEvent(ev);
            // 如果此时触摸点y坐标小于lv的高度的一半,则说明触摸点处于lv2的上半部分
            // 事件需要再分发给lv1和lv3
            if (ev.getRawY() < lv1.getHeight() / 2) {
                lv1.dispatchTouchEvent(ev);
                lv3.dispatchTouchEvent(ev);
            }
        }
        // 返回true,消耗此事件
        return true;
    }
 
 测试确实也没有问题,解决了需求。但是就此引发了琢磨几天的一连串BUG....
 
lv2和lv3无法接收点击事件
预期效果:点击listview中的美女条目,跳转该美女的图片集

lv1正常,达到预期;lv2和lv3可以正常滑动,但是无法响应点击。

问题产生原因分析

重写dispatchTouchEvent()方法,给子ListView传递事件时,事件对象ev被原封不动的传递了下去。
比如用户点击lv2的一个条目,此时假设每个lv的宽度为100,点击点ev的x坐标为150,经过上面判断,150既不小于100,也不大于200,则进入else代码块,执行lv2.dispatchTouchEvent(ev);,ev被原封不动的传递给lv2

会造成什么样的后果呢?
lv2在判断点击点位于哪个条目的时候,ev的x坐标为150,而lv2的宽度一共只有100,这个x坐标超出了lv2的宽度,也就是说点到了lv2的外面,那么这个点击当然就无法正确触发条目点击事件了。lv3也是同理。
问题解决方案

 当事件需要分发给lv2时,需要将点击点的x坐标减去lv的宽度,重新设置给ev,再传递下去。
  当事件需要分发给lv3时,需要将点击点的x坐标减去lv的宽度 * 2,重新设置给ev,再传递下去。

 
上方标题栏可以接收触摸事件

在上方标题栏中触摸上下拖动和点击,也会造成ListView滑动和触发lv1的点击事件

问题产生原因分析

要处理三个ListView的事件分发,应该重写它们的父布局LinearLayout的dispatchTouchEvent()方法,而不是重写Activity的事件分发方法,这样会影响到Activirt上的所有控件。

问题解决方案

创建自定义View继承LinearLayout,重写dispatchTouchEvent()方法

BUG解决
 
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 获取触摸点的x坐标
        int x = (int) ev.getRawX();
        // 获取触摸点的y坐标
        int y = (int) ev.getRawY();
        // 获取lv的宽度,三个lv是等宽的,所以随便取谁的都一样
        int width = lv1.getWidth();
        // 判断触摸点x坐标如果小于lv1的宽度,则手指触摸的是lv1,这个事件应分发给lv1进行处理
        if (x < width) {
            lv1.dispatchTouchEvent(ev);
        // 判断触摸点x坐标如果小于lv1的宽度 * 2,则手指触摸的是lv3,这个事件应分发给lv3进行处理
        } else if (x > width * 2) {
            
            // 此时事件应该被分发给lv3,需要重新设置x坐标,减去两个lv的宽度
            ev.setLocation(x - (width * 2), y);
            
            lv3.dispatchTouchEvent(ev);
        } else {
            
            // 此时事件应该被分发给lv2,需要重新设置x坐标,减去lv的宽度
            ev.setLocation(x - width, y);
            
            // 否则触摸的就是lv2,事件将被分发给lv2处理
            lv2.dispatchTouchEvent(ev);
            // 如果此时触摸点y坐标小于lv的高度的一半,则说明触摸点处于lv2的上半部分
            // 事件需要再分发给lv1和lv3
            if (y < lv1.getHeight() / 2) {
                lv1.dispatchTouchEvent(ev);
                lv3.dispatchTouchEvent(ev);
            }
        }
        // 返回true,消耗此事件
        return true;
    }
 

追求优雅的代码

上述代码中,去掉了系统的super.dispatchTouchEvent(ev)方法,造成很多东西需要自己处理,判断了三种情况,管理了三个ListView的事件分发
完全可以优化,其实实现需求只需要判断一种情况:当滑动点位于lv2的上半部分的时候,将事件同时也分发给另外两个ListVIew

优化后,LinearLayout的dispatchTouchEvent()方法

 
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 第一个子条目,也就是ListView1
        View view = getChildAt(0);
        if (MotionEvent.ACTION_DOWN == ev.getAction()) {
            downY = (int) ev.getY();
        }
        int x = (int) ev.getX();
        int width = view.getWidth();
        // 如果x处于中间的那个listview,并且y处于listview的上半部分
        if (x > width && x < width * 2 && downY < view.getHeight() / 2) {
            //将事件分发给另外两个ListView
            view.dispatchTouchEvent(ev);
            getChildAt(2).dispatchTouchEvent(ev);
        }
        return super.dispatchTouchEvent(ev);
    }

不但没有各种BUG,代码长度也减少了这么多。。

经过这次事件,总结出如下几点经验:
1、没事千万不要删掉return super.dispatchTouchEvent(ev),系统为我们做了N多事情,包括事件该如何传递,传递下去该如何控制xy坐标,等等等等...
2、事件的传递,尽量不要重写Activity的,它的处理逻辑和ViewGroup的略有不同,应该重写目标控件的父控件。
3、如必须手动向下级子View传递事件,则需要计算并重新设置好x、y坐标,再传递
4、重写dispatchTouchEvent、onTouchEvent、方法时,尽量不要直接return true;或者return false,除非系统对事件的处理与我们的预期有冲突。
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 事件分发
相关文章推荐