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将会被调用
结论:
- 同一事件序列包括down,move,up
- 正常情况,一个事件序列全部被一个view消耗.特殊情况是view强行使用onTouchEvent传递
- view一旦决定拦截,那么这个事件序列全部归它,并且onInterceptTouchEvent不会再被调用
- view不消耗down事件,那么事件序列其他事件也与它无缘.会再次给父控件onTouchEvent
- 如果view只消耗down事件,那么这个点击事件会消失,会直接传递给activiy
- viewGroup默认不拦截任何事件
- view没有onInterceptTouchEvent,只要收到事件就调用onTouchEvent
- view的onTouchEvent默认返回true.如果clickable和longClick同时为false,那么返回false.
- view的enable不影响onTouchEvent
- onclick发生的条件 a.view可点击 b.收到了down和up
- 事件传递由外向内.子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,还能添加监听