Android View的事件分发

达芬奇密码2018-07-25 13:10

Android View的事件分发

最近刚看完android-Ultra-Pull-To-Refresh下拉刷新的源码,发现在写自定义控件时,对于View的事件的传递总是搞不太清楚,而View事件的分发机制,又是解决可能出现的滑动冲突的基础,因此专门学习了一下,结合一个例子来好好分析分析。

先上Demo,很简单,就是一个自定义ViewGroup和一个自定义View。View分发机制中,有三个很重要的方法是:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,它们分别对应着事件的分发、拦截和消耗,因此我在自定义View的这三个方法里加上Log,根据打印的日志来分析下具体的流程。

public class CustomViewGroup extends LinearLayout {
    private static final String TAG = "Mylog";

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "customViewGroup.dispathcTouchEvent "+ev.toString());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "customViewGroup.onInterceptTouchEvent "+ev.toString());
        return super.onInterceptTouchEvent(ev);
//        return  true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "customViewGroup.onTouchEvent "+event.toString());
        return super.onTouchEvent(event);
//        return  true;
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
    }
}

public class CustomView extends View {
    private static final String TAG = "Mylog";

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

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

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "customview.dispatchtouchevent "+event.toString());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "customview.onTouchEvent "+event.toString());
//        return  true;
        return super.onTouchEvent(event);
    }
}

      customView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "customView.onTouch "+event.toString());
                return false;
            }
        });

        customViewGroup.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                Log.d(TAG, "customViewGroup.onTouch "+event.toString());
                return false;
            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "Activity.dispatchTouchEvent "+ev.toString());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "Activity.onTouchEvent "+event.toString());
        return super.onTouchEvent(event);
    }

在Activity界面,我对CustomView和CustomViewGroup设定了onTouch监听。

下图是界面的一个示意图,Activity的界面包含了一个ViewGroup,而ViewGroup里包含了一个View,最简单的线性布局。

事件传递的基本原则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给自身,并调用其dispatchTouchEvent方法,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着调用onTouchEvent来处理这个事件;如果这个ViewGroup不拦截,则将事件传递给子元素,子元素调用其onInterceptTouchEvent方法,如此反复直到最终被处理。

同一事件序列:指手指接触屏幕起,到手指离开屏幕的那一刻,在这个过程中产生的一系列动作。一般以down事件开始,一系列move事件,并以一个up事件结束。

可以表示为:[down, move,move,move......up]

在上述的基本原则下,我们来根据例子分析下View的事件分发过程。

  • 在默认情况下,点击区域3 View,Activity消耗事件

打印出来的Log如上,事件ACTION_DOWN先从Activity的dispatchTouchEvent开始传递,向下分发,由于Activity和ViewGroup默认都是不拦截事件的,因此不会拦截,一直传递到最内侧的View,注意到View的onTouch方法比其onTouchEvent方法先调用,这表示onTouch具有更高的优先级。由于我默认设置view也不处理事件,因此事件最终回到了Activity的onTouchEvent中,并接受了ACTION_UP事件。这表明一旦一个View接受了ACTION_DOWN事件,那么接下来的同一序列的事件都将交给它处理。结合下图可以更好的来理解。

  • 内部View消耗ACTION_DOWN事件

将代码中,CustomView的onTouchEvent返回值为true,则表示可以对接受到的事件进行消耗。我们点击区域3,并滑动一段距离,此时打印log如下:

可以看到,当View的onTouchEvent返回true时,当ACTION_DOWN事件分发到View后,就不在会向父层转发,表示此View将会对事件经行处理,之后的ACTION_MOVE等同一序列的事件,都将沿着之前的路径转发给此View。

 

  • 外部ViewGroup消耗ACTION_DOWN事件

将CustomView的onTouchEvent返回改成super.onTouchEvent,并将CustomViewGroup中的onTouchEvent和onInterceptTouchEvent返回均设为true,表示将由ViewGroup来拦截事件,并进行处理。我们点击内部的区域3 View:得到的打印logo如下:

从log中,我们可以发现,一旦ViewGroup准备拦截事件,则事件将不再被转发给View,因此View的dispatchTouchEvent等函数均不会被触发,另外,在之后的同一序列事件中,ViewGroup的onInterceptTouchEvent函数也不会被触发,这表示:一旦某个ViewGroup决定拦截事件,则系统不再会询问接下来的事件ViewGroup是否要拦截。

  • 点击区域2 ViewGroup

这种情况比较简单,此时因为点击的区域中,不包含其他View,因此ViewGroup将不会把事件转发给ViewGroup中的其他子View。而在Activity和ViewGroup中正常的进行事件分发,这里给出打印的logo,流程图示意省略。

  • 事件经过View后,在由ViewGroup处理

这种情况相对少见一点。我们需要将ViewGroup的onInterceptTouchEvent设为super,onTouchEvent设为true,View的onTouchEvent设为super. 这保证了事件将会被传递给子View,但子View不会去消耗这个事件。同样点击区域3 View后,logo如下:

 

和预期的一致,事件正常的分发到了View当中,但是子View并没有处理事件,而是交回到ViewGroup来处理。值得注意的是,之后同一序列的其他事件,都不在会分发给View,而是直接交给ViewGroup的onTouch方法去处理。这意味着一个View一旦接收了事件的转发而不处理,那么系统将不会把同一序列的其他事件转发给它。

 

大概情况就这么几种,常见的一般是直接由ViewGroup来消耗或者View消耗事件。

这里主要还是从应用层表面来分析了一些View事件的分发过程,如果想深入了解各个函数的实现,可以去阅读一下View的源码,从而更好的证实上述的结果。另外,注意到,View(不是ViewGroup)是不包含onInterceptTouchEvent方法的,这表示只要有事件传递给View,其onTouchEvent方法就会被触发,而ViewGroup是默认不拦截任何事件的,其onInterceptTouchEvent方法默认返回false。

在了解了View的事件分发机制后,就可以对一些出现滑动冲突的情况进行自己的处理,之后的文章中会继续写写学习心得。


本文来自网易实践者社区,经作者周龙授权发布。