Android事件分发机制分析

来源:互联网 发布:0基础软件开发 编辑:程序博客网 时间:2024/06/05 06:56

本文基于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 dispatchTouchEventACTION_DOWN: Center dispatchTouchEventACTION_DOWN: In dispatchTouchEventACTION_DOWN: In onTouchEvent=============================================ACTION_MOVE: Out dispatchTouchEventACTION_MOVE: Center dispatchTouchEventACTION_MOVE: In dispatchTouchEventACTION_MOVE: In onTouchEvent=============================================ACTION_UP: Out dispatchTouchEventACTION_UP: Center dispatchTouchEventACTION_UP: In dispatchTouchEventACTION_UP: In onTouchEvent=============================================

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

=============================================ACTION_DOWN: Out dispatchTouchEventACTION_DOWN: Center dispatchTouchEventACTION_DOWN: In dispatchTouchEvent=======================ACTION_DOWN: In onTouchEventACTION_DOWN: Center onTouchEventACTION_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_DOWNMainActivity: onTouch_ACTION_MOVEMainActivity: 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_DOWNMainActivity: onTouch_ACTION_MOVEMainActivity: 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