LinearLayout里面放一个Button
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
tools:context=".ButtonActivity">
<com.view.MyLL
android:id="@+id/ll"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.view.MyButton
android:id="@+id/b1"
android:text="按钮"
android:background="@color/colorAccent"
android:layout_width="100dp"
android:layout_height="100dp" />
</com.view.MyLL>
</LinearLayout>
MyButton.java
public class MyButton extends android.support.v7.widget.AppCompatButton {
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
private String msg = "";
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(T.TAG, "MyButton dispatchTouchEvent: "+msg+ Utils.getAction(ev));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(T.TAG, "MyButton onTouchEvent: "+msg+ Utils.getAction(event));
return super.onTouchEvent(event);
}
}
MyLL.java
public class MyLL extends LinearLayout {
public MyLL(Context context) {
super(context);
}
public MyLL(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLL(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(T.TAG, "MyLL dispatchTouchEvent: "+ Utils.getAction(ev));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(T.TAG, "MyLL onInterceptTouchEvent: "+ Utils.getAction(ev));
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(T.TAG, "MyLL onTouchEvent: "+ Utils.getAction(event));
return super.onTouchEvent(event);
}
}
1 手指点击MyLL中的白色区域再松开,回调日志是什么?
I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN
你以为是我截取的时候少日志了?是不是还在疑惑不应该还有一个ACTION_UP事件么?
其实并没有ACTION_UP事件!
ACTION_DOWN这个事件是事件分发的源头,ACTION_DOWN这个事件最终被谁消费,那么后续的事件正常情况下也会交给谁处理,直到处理完最后一次ACTION_UP事件
回过头来看看日志,我们可以猜测是这样的原因:
MyLL在接受ACTION_DOWN事件之后发现自己无法消费该事件,告知父布局自己无法消费,父布局得知以后不再把ACTION_MOVE ACTION_UP等事件交给MyLL去做而是自己处理
这样的猜想合理不合理呢?我们看下源码吧super.onTouchEvent(event)调用的是View的onTouchEvent(event)
View源码onTouchEvent:
源码1.0
public boolean onTouchEvent(MotionEvent event) {
//....
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//...
return true;
}
return false;
}
默认情况下,我们的MyLL回调最后结果是返回false,即不消费此事件那么父布局将不再把后续的触摸事件分发给MyLL
1.1如果我们这个时候将xml文件增加一句话 android:clickable=“true”
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
tools:context=".ButtonActivity">
<com.view.MyLL
android:id="@+id/ll"
android:clickable="true" //加这段代码
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.view.MyButton
android:id="@+id/b1"
android:text="按钮"
android:background="@color/colorAccent"
android:layout_width="100dp"
android:layout_height="100dp" />
</com.view.MyLL>
</LinearLayout>
再看下点击的结果
2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.108 3620-3620/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:42:20.108 3620-3620/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_UP
这样就没问题了,确实可以看到完整的事件分发了
1.2 如果我们给MyLL设置监听setOnClickListener
myLL.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(T.TAG, "onClick: myLL");
}
});
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.170 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:48:51.170 3923-3923/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:48:51.174 3923-3923/com.justtest I/liangchaojie: onClick: myLL
发现也可以正常的进行事件分发了?为什么?为啥设置了监听就可以走事件分发了呢?
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
我不管你之前是不是clickable,只要你设置了监听我就让你可以点击,也就是说这里其实还是用1.1的这种方法
如果你足够细心的话你会发现ACTION_DOWN事件走了三个回调方法dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent可是ACTION_UP只走了dispatchTouchEvent和onTouchEvent,你想问onInterceptTouchEvent去哪里了呢?
我在之前的博客里面提到过这个问题,如果一个DOWN事件被一个ViewGroup消费了,那么后续事件就不再执行onInterceptTouchEvent方法询问是否拦截了,因为他之前已经知道了自己要干而不是交给子view去干,这样想起来也合理
2 手指点击MyButton区域再松开,回调日志是什么?
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP
这个就是最正常的逻辑,也是基本上大部分事件分发的回调情况
3 手指点击MyButton红色区域不松手慢慢移动到白色MyLL区域再松手,回调日志是什么?
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.089 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.090 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
....这里省略太多重复的ACTION_MOVE.......
2019-06-27 13:17:42.518 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.518 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.527 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.527 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.528 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.528 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP
这里就产生有一个问题了,我手都离开了MyButton但是为什么MyButton还在回调触摸事件呢?
#####做android三年了,如果不做这个实验,我还会认为手指离开MyButton,MyButton就不会回调分发事件
我们再回顾上面问题1的结论
ACTION_DOWN这个事件是事件分发的源头,ACTION_DOWN这个事件最终被谁消费,那么后续的事件正常情况下也会交给谁处理,直到处理完最后一次ACTION_UP事件
我们触摸MyButton的时候ACTION_DOWN事件最终是交给MyButton处理的,所以只要没有ACTION_UP事件那么后续的事件都是交给MyButton处理,所以才会看到上面日志的情况,第二次看到这个理论是不是加深了对其的理解呢?
#####3.1那我们现在再考虑一个问题:
手指从Button1点击然后不松手慢慢移动到Button2再松手,这个时候会走谁的onClickListener事件?还是都不走或者都走?
首先Button2是不会走的,因为他压根就没有接收到触摸事件,因为Button2没有ACTION_DOWN事件传入,并没有对其分发触摸事件,在ACTION_MOVE的过程中他的角色只是LinearLayout的一部分而已。
那么Button1走了吗?这个是很多人困惑的问题。
你可能会说:不走,因为Button1没有接收到ACTION_UP事件,只有接收了ACTION_UP事件,走一个完整的事件分发流程才可以执行onClickListener回调。
Button1没有接收到ACTION_UP事件吗?其实不是!他接收到了,我们看到问题3的日志就知道,即使不在button区域但是也可以收到回调事件,因为ACTION_DOWN是button消费的,只要事件没有结束那么就继续交给其处理,所以手指离开Button1之后,不管你是否move回到button1,其实button1都是可以接收到ACTION_DOWN ACTION_UP事件的。
你也可能会说:走,因为在button1上进行了一个完整的ACTION_DOWN和ACTION_UP操作,是可以满足条件的
那么真相是如何呢?
....之前的太多就不展示了
2019-06-27 16:49:05.588 8161-8161/com.justtest I/liangchaojie: onTouch: mButton
2019-06-27 16:49:05.588 8161-8161/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: onTouch: mButton
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP
#####Android手机的话Button1不走,你要是换成ios手机是走的,毕竟系统不一样
为什么不走呢?确实是把完整的事件流走了一遍呢!我们这个时候再看看View的onTouchEvent源码
###源码1.1
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
//注意mIgnoreNextUpEvent这个字段
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//这段代码就是我们进行onClick操作的代码
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
//.........................
mIgnoreNextUpEvent这个字段是这个问题的核心,这个字段表示是否应该忽略下一次ACTION_UP事件,为什么需要忽略呢?
首先这个字段默认值是false
private boolean mIgnoreNextUpEvent;
当mIngoreNextUpEvent=true的时候我们将不进行onClick操作
当mIngoreNextUpEvent=false的时候进行onClick操作
那我们看下什么时候mIngoreNextUpEvent=true呢?
case MotionEvent.ACTION_BUTTON_PRESS:
if (isContextClickable() && !mInContextButtonPress && !mHasPerformedLongPress
&& (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
|| actionButton == MotionEvent.BUTTON_SECONDARY)) {
if (performContextClick(event.getX(), event.getY())) {
mInContextButtonPress = true;
setPressed(true, event.getX(), event.getY());
removeTapCallback();
removeLongPressCallback();
return true;
}
}
break;
case MotionEvent.ACTION_BUTTON_RELEASE:
if (mInContextButtonPress && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
|| actionButton == MotionEvent.BUTTON_SECONDARY)) {
mInContextButtonPress = false;
mIgnoreNextUpEvent = true; //整段代码仅此一处设置为了true
}
break;
ACTION_BUTTON_PRESS???我们知道isPressed()代表了按钮是否被按压,难道是系统判断了button没有被按压直接mIgnoreNextUpEvent设置成了true?
没错,打了日志你会发现,当你手指按压和不按压的时候isPressed()返回不同的值
//第一个默认是false
2019-06-27 17:06:52.563 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
//按压之后显示true
2019-06-27 17:06:52.754 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.771 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.788 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.805 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.824 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.839 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.857 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
//手指离开button 按压返回false
2019-06-27 17:06:52.874 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.891 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.908 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
//....下面手指返回button但还是返回false
2019-06-27 17:06:52.925 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.942 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
真相大白!原来是这个mIgnoreNextUpEvent和press搞的鬼,可是在哪里press无效了呢?
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
//如果触摸事件超出了view之外
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//设置按压效果无效
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
可是如果mIgnoreNextUpEvent被设置成了true是不是下次点击事件就无效了呢???
仔细看源码1.1
case MotionEvent.ACTION_UP:
//...
mIgnoreNextUpEvent = false;
break;
最后分发事件结束的时候会重置其状态,所以下次再点击是可以点击的。
至此就把View的分发又重新学习了一遍,感谢阅读到这里,欢迎提出宝贵意见和建议!