Android事件分发机制分析

本文基于Android事件分发机制完全解析,带你从源码的角度彻底理解从Android源码的角度理解应用开发(1)-Touch机制进行编写的,加入自己的理解。方便自己理清思路和便于以后的查看。

①首先我们先写一个小Demo,Demo源码下载, 如下图所示
这里写图片描述
布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lingchen="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.yitong.mytouchevent.MainActivity">

    <com.yitong.mytouchevent.view.TouchView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_dark"
        android:gravity="center"
        lingchen:viewName="Out"
        >

        <com.yitong.mytouchevent.view.TouchView
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@android:color/holo_green_dark"
            android:gravity="center"
            lingchen:viewName="Center">

            <com.yitong.mytouchevent.view.TouchView
                android:id="@+id/main_in"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:background="@android:color/holo_red_dark"
                lingchen:viewName="In"/>

        </com.yitong.mytouchevent.view.TouchView>

    </com.yitong.mytouchevent.view.TouchView>
</RelativeLayout>

当In_View设置了android:clickable=”true”时,当点击了In_View时。事件处理经过如下:

=============================================
ACTION_DOWN: Out dispatchTouchEvent
ACTION_DOWN: Center dispatchTouchEvent
ACTION_DOWN: In dispatchTouchEvent
ACTION_DOWN: In onTouchEvent
=============================================
ACTION_MOVE: Out dispatchTouchEvent
ACTION_MOVE: Center dispatchTouchEvent
ACTION_MOVE: In dispatchTouchEvent
ACTION_MOVE: In onTouchEvent
=============================================
ACTION_UP: Out dispatchTouchEvent
ACTION_UP: Center dispatchTouchEvent
ACTION_UP: In dispatchTouchEvent
ACTION_UP: In onTouchEvent
=============================================

当In_View没有设置android:clickable=”true”时,当点击了In_View时。事件处理经过如下:

=============================================
ACTION_DOWN: Out dispatchTouchEvent
ACTION_DOWN: Center dispatchTouchEvent
ACTION_DOWN: In dispatchTouchEvent
=======================
ACTION_DOWN: In onTouchEvent
ACTION_DOWN: Center onTouchEvent
ACTION_DOWN: Out onTouchEvent
=============================================

②接着我们测试下当view的touch和onClick事件的关系,比较触发点击事件时,那个事件先执行

mainIn.setOnTouchListener(new View.OnTouchListener() {
        @Override
         public boolean onTouch(View v, MotionEvent event) {
             switch(event.getAction()) {
                 case MotionEvent.ACTION_DOWN:
                     Log.d(Constants.name + TAG, "onTouch_ACTION_DOWN");
                     break;

                 case MotionEvent.ACTION_MOVE:
                     Log.d(Constants.name + TAG, "onTouch_ACTION_MOVE");
                     break;

                 case MotionEvent.ACTION_UP:
                     Log.d(Constants.name + TAG, "onTouch_ACTION_UP");
                     break;
             }
             return false;
         }
     });

     mainIn.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d(Constants.name + TAG, "onClick");
         }
     });

当In_View设置了android:clickable=”true”时,当点击了In_View时。会先执行onTouch方法,再执行onClick

=============================================
MainActivity: onTouch_ACTION_DOWN
MainActivity: onTouch_ACTION_MOVE
MainActivity: onTouch_ACTION_UP
=======================
MainActivity: onClick
=============================================

当In_View的onTouch的返回值返回为true,则不会执行onClick事件了

mainIn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                ...
                return false;
            }
        });
        mainIn.setOnClickListener(new View.OnClickListener() {
        ...
     });

=============================================
MainActivity: onTouch_ACTION_DOWN
MainActivity: onTouch_ACTION_MOVE
MainActivity: onTouch_ACTION_UP
=============================================

几点重要总结:

  1. 只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的。如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。onClick的调用是在onTouchEvent(event)方法中。

  2. 如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

  3. 当In_View的onTouch的返回值返回为false,并且In_View没有设置了android:clickable=”true”时,只会去执行onTouch中的ACTION_DOWN方法
    MainActivity: onTouch_ACTION_DOWN

  4. onTouch和onTouchEvent区别:这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

    ==================至此关于事件分发机制介绍完毕=====================

接下几种情况带你回顾事件分发机制:

①为什么图片轮播器里的图片使用Button而不用ImageView?

  • 就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

②为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?

  • 滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动,因此解决办法就是在onTouch方法里返回false。

③touch监听器没被调用到?

  • 如果是事件被这个View的viewparent拦截了。可以修改这个view的parent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()。

④设置了onClickListener后,点击View没有反应?

  • 可能覆盖了onTouchEvent(),需要在覆盖的方法调用super.onTouchEvent()或者手动调用performClick()

⑤点击两下View才调用onClickListener的bug?

  • 这个其实是安卓的设计,当某个View调用了setFocusableInTouchMode(true)后,第一次点击会引起这个View的focus,第二次点击才会调用onClickListener,只需要设置setFocusableInTouchMode(false)即可。

=========让我们梳理一下============
这里写图片描述
1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

===============又是一个分水岭=======================

接下来我们就实战一下:

①写一个不能左右滑动的ViewPager,我们知道ViewPager是可以左右滑动的,当我们的ViewPager中嵌套一个轮播图(又一个ViewPager)。这时如果我们滑动轮播图,但是ViewPager把事件给拦截了。

public class NoScrollViewPager extends ViewPager {

    public NoScrollViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NoScrollViewPager(Context context) {
        super(context);
    }

    /**
     * 表示事件是否拦截, 返回false表示不拦截, 可以让嵌套在内部的viewpager相应左右划的事件
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent arg0) {
        return false;
    }

    /**
     * 重写onTouchEvent事件,什么都不用做
     */
    @Override
    public boolean onTouchEvent(MotionEvent arg0) {
        return false;
    }
}

②我们使用手机的新闻客户端,在主页面总体用的是一个ViewPager,可以查看娱乐、社会、实时…,当滑动到第一个主题(最左边)时,我们想再次滑动就可以打开菜单栏。这时单纯的ViewPager就不能满足。

public class HorizontalViewPager extends ViewPager {

    public HorizontalViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HorizontalViewPager(Context context) {
        super(context);
    }

    /**
     * 事件分发, 请求父控件及祖宗控件是否拦截事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (getCurrentItem() == 0) {// 第一个主题,再次滑动打开菜单栏
            getParent().requestDisallowInterceptTouchEvent(false);// 请求父控件拦截我的事件,让父控件去处理该事件

        } else {// 如果不是第一个主题,ViewPager自身处理
            getParent().requestDisallowInterceptTouchEvent(true);// 请求父控件不要拦截我的事件
        }
        return super.dispatchTouchEvent(ev);
    }
}

③上面第一个不能左右滑动的NoScrollViewPager显然功能太弱,我们想要当里面的ViewPager滑动到第一个主题和最后一个主题的时候能够触发外面的ViewPager。

public class TopNewsViewPager extends ViewPager {

    int startX;
    int startY;

    public TopNewsViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TopNewsViewPager(Context context) {
        super(context);
    }

    /**
     * 事件分发, 请求父控件及祖宗控件是否拦截事件 1. 右划, 而且是第一个页面, 需要父控件拦截 2. 左划, 而且是最后一个页面, 需要父控件拦截
     * 3. 上下滑动, 需要父控件拦截
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);// 不要拦截,
                                                                    // 这样是为了保证ACTION_MOVE调用
            startX = (int) ev.getRawX();
            startY = (int) ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:

            int endX = (int) ev.getRawX();
            int endY = (int) ev.getRawY();

            if (Math.abs(endX - startX) > Math.abs(endY - startY)) {// 左右滑动
                if (endX > startX) {// 右划
                    if (getCurrentItem() == 0) {// 第一个页面, 需要父控件拦截
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                } else {// 左划
                    if (getCurrentItem() == getAdapter().getCount() - 1) {// 最后一个页面,
                                                                            // 需要拦截
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }
            } else {// 上下滑动
                getParent().requestDisallowInterceptTouchEvent(false);
            }

            break;

        default:
            break;
        }

        return super.dispatchTouchEvent(ev);
    }

}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值