View 点击事件的分发机制

这里面的代码以及文字来自 任玉刚的 <<android开发艺术探索>> 此处仅作为个人笔记使用

点击事件的传递规则

 /**
     * 用来进行事件的分发,如果事件能够传递给当前view,则此方法一定会被调用,返回结果受onTouchEvent
     * 和下级的dispatchTouchEvent影响
     * @param event
     * @return 表示是否消耗了该点击事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean consume=false;
        if (onInterceptTouchEvent(event)){
            consume=onTouchEvent(event);
        }else {
            consume= child.dispatchTouchEvent(event);
        }
        return consume;
    }

    /**
     * 在dispatchTouchEvent内部调用,用来判断是否拦截某个某个事件,如果当前View 拦截了某个事件,
     * 那么在同一个事件序列中,此方法不会被再次调用
     * @param ev
     * @return 表示是否拦截当前事件.
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        throw new RuntimeException("Stub!");
    }

    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

viewgroup的事件处理如图所示

view的点击处理逻辑

点击事件的传递过程

activity->window->view

顶级的view接收到点击事件以后,就会按照分发机制一层层的分发事件.如果其中的某一个view的ontouchevent返回了false,那么它父容器的ontouchevent将会被调用

结论:

  1. 同一事件序列包括down,move,up
  2. 正常情况,一个事件序列全部被一个view消耗.特殊情况是view强行使用onTouchEvent传递
  3. view一旦决定拦截,那么这个事件序列全部归它,并且onInterceptTouchEvent不会再被调用
  4. view不消耗down事件,那么事件序列其他事件也与它无缘.会再次给父控件onTouchEvent
  5. 如果view只消耗down事件,那么这个点击事件会消失,会直接传递给activiy
  6. viewGroup默认不拦截任何事件
  7. view没有onInterceptTouchEvent,只要收到事件就调用onTouchEvent
  8. view的onTouchEvent默认返回true.如果clickable和longClick同时为false,那么返回false.
  9. view的enable不影响onTouchEvent
  10. onclick发生的条件 a.view可点击 b.收到了down和up
  11. 事件传递由外向内.子view可以通过requestDisallowInterceptTouchEvent干涉父元素除了down事件以外的其他事件 

实践

好了经过上面的理论讨论,我们来实践一下.我们新建两个自定义控件,一个是继承LinearLayout一个继承Button.具体代码如下

public class TestLayout extends LinearLayout {
    private static final String TAG = "TestLayout";
    public TestLayout(Context context) {
        super(context);
    }

    public TestLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public TestLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 用来进行事件的分发,如果事件能够传递给当前view,则此方法一定会被调用,返回结果受onTouchEvent
     * 和下级的dispatchTouchEvent影响
     * @param event
     * @return 表示是否消耗了该点击事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent: "+event.getAction());
        return  super.dispatchTouchEvent(event);
    }

    /**
     * 在dispatchTouchEvent内部调用,用来判断是否拦截某个某个事件,如果当前View 拦截了某个事件,
     * 那么在同一个事件序列中,此方法不会被再次调用
     * @param ev
     * @return 表示是否拦截当前事件.
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: "+ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: "+event.getAction());
        return super.onTouchEvent(event);
    }
}
public class TestBtn extends Button{
    private static final String TAG = "TestBtn";
    public TestBtn(Context context) {
        super(context);
    }

    public TestBtn(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public TestBtn(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 用来进行事件的分发,如果事件能够传递给当前view,则此方法一定会被调用,返回结果受onTouchEvent
     * 和下级的dispatchTouchEvent影响
     * @param event
     * @return 表示是否消耗了该点击事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent: "+event.getAction());
        return super.dispatchTouchEvent(event);
    }


    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: "+event.getAction());
        return super.onTouchEvent(event);
    }


}

剩下activity和布局的代码太简单这里我们就不贴了,我们直接运行一下.然后点击一下按钮.

可以得到以下运行结果

 1. down事件:

  TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 2. move事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 3. up事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 4. button的click事件

 以上我们可以得到以下结论:

  1.一开始我们的流程是对的

  2.验证了上面结论的1,2,6,7

  3.click事件是最后触发的

我们现在再修改代码,将testBtn的onTouch返回值更改为false

    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: "+event.getAction());
        boolean isTouch=super.onTouchEvent(event);
        Log.d(TAG, "onTouchEvent: "+isTouch);
        return false;
    }

然后再运行一下代码,看一下获得的结果

1. down事件:

  TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent->TestLayout:onTouchEvent

 然后就没了,对,没了,没有move事件,也没有up事件,也没有点击事件

 我们可以得到以下结论:

  1.首先down事件正常传递,当传递到testBtn的时候,testBtn的onTouchEvent返回false,代表了没有对点击事件进行处理.那么按照上面我们总结的结论的第4条,很自然的就调用了父控件也就是TestLayout的onTouchEvent方法.由于viewgroup的onTouchEvent的返回值为

false.所以最终这个事件序列,他们两个都木有处理,所以这个事件序列的其他事件自然与他们无关了

  2.并且从这里我们也可以看出,如果testBtn只收到down没有收到up,是不会调用onclick方法的

 

现在我们进一步修改代码,我们将testLayout的onTouchEvent的返回值由false更改为true.看看会发生什么

    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: "+event.getAction());
        boolean isTouch=super.onTouchEvent(event);
        Log.d(TAG, "onTouchEvent: "+isTouch);
        return true;
    }

好的,我们运行一下程序,看一下会发生什么

 1. down事件:

  TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent->TestLayout:onTouchEvent

 2. move事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onTouchEvent

 3. up事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onTouchEvent

 总结

  其实这次获得的结论和上面的测试获得的结论是一样的.只是更新验证了我们的结论.当然我们也可以看到,如果下面的view没有消耗事件流的down事件,那么后面的事件都和它无关.并且我们也可以看到上面结论3也被证实了.

 好了,我们看一下之前总结的结论,除了5,8,9.其他的,都得到了证实.那么我们接下来就来验证结论5,8,9.

 先是结论5

  我们将testLayout的onTouchEvent恢复成原来的样子,返回值更改为false

  将testBtn的onTouchEvent改成下面的样子

    /**
     * 在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,
     * 则在同一个事件序列中,当前view无法再次接收到事件.
     * @param event
     * @return 表示是否消耗当前事件,
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: "+event.getAction());

        boolean isTouch=super.onTouchEvent(event);
        Log.d(TAG, "onTouchEvent: "+isTouch);
        if (event.getAction()==MotionEvent.ACTION_DOWN){
            return true;
        }else {
            return false;
        }

    }

 运行后我们会获取结果:

 1. down事件:

  TestLayout:dispatchTouchEvent -> TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 2. move事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 3. up事件:

  TestLayout:dispatchTouchEvent ->TestLayout:onInterceptTouchEvent->TestBtn:dispatchTouchEvent->TestBtn:onTouchEvent

 4. button的click事件

乍一看这个结果,咦,不是和第一个button和layout什么都不改不是一样的嘛.其实就是一样的.但是你发现没有,其实move事件和up事件,我们没有任何控件去消耗它.它们就这样消失了,即没有被testButton消耗,也没有被testLayout消耗

 

下面我们再把现场恢复到默认的样子.

然后在activity中添加一个

testBtn.setClickable(false);

但是请注意,这个设置一定要在setOnClickListener以后调用,源码嘛,看源代码

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

好的,现在我们运行以后,很明显的就能看到运行结果,testBtn的onTouchEvent的返回值编程了false,很轻易的验证了,我们的结论8.同时也知道了为什么TextView的onclickable为false,还能添加监听


posted @ 2018-10-10 14:41  蓝冷然  阅读(2165)  评论(0编辑  收藏  举报