【Android】onInterceptTouchEvent 方法收不到ACTION_MOVE事件
文章目录
- 问题实例Demo
- 观察log
- 真的收不到嘛?
- Button 与 TextView的区别在哪?
- 另外一种分析
- `mFirstTouchTarget` 为何与点击事件有关
- 总结
- 参考
问题是这样的:
我自定义一个父布局,继承自FrameLayout,然后重写其中的onInterceptTouchEvent方法和onTouchEvent方法。但是onInterceptTouchEvent 方法就是收不到ACTION_MOVE事件。
问题实例Demo
先看MainActivity。为了说明问题,这里demo做了简化。如下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}
再来看R.layout.activity_main:
<?xml version="1.0" encoding="utf-8"?>
其中的MyFrameLayout
是我自定义的,来看下:
public class MyFrameLayout extends FrameLayout { private static final String TAG = "MyFrameLayout"; public MyFrameLayout(Context context) { super(context); } public MyFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onInterceptTouchEvent: MyFrameLayout MotionEvent.ACTION_DOWN "); break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onInterceptTouchEvent: : MyFrameLayout MotionEvent.ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(TAG, "onInterceptTouchEvent: : MyFrameLayout MotionEvent.ACTION_UP"); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onTouchEvent:: MyFrameLayout MotionEvent.ACTION_DOWN "); break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent:: MyFrameLayout MotionEvent.ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent:: MyFrameLayout MotionEvent.ACTION_UP"); break; } return super.onTouchEvent(event); }}
以上只是加了log信息而已。
观察log
D/MyFrameLayout: onInterceptTouchEvent: MyFrameLayout MotionEvent.ACTION_DOWN D/MyFrameLayout: onTouchEvent:: MyFrameLayout MotionEvent.ACTION_DOWN
只收到了onInterceptTouchEvent
方法中的MotionEvent.ACTION_DOWN
事件。为了复习了很多事件分发机制的问题。但是关于这块的知识点,本来已经比较熟悉。所以再去翻书,看网上的资料,基本都是千篇一律。可仍没有解决我这个问题: 为什么onInterceptTouchEvent
方法收不到ACTION_MOVE
事件?
一开始我竟然觉得,收不到ACTION_MOVE
事件是正常的,因为按照我理解的事件分发机制,就是这个样子的。以至于我更加困惑。因为别的项目中是可以收到ACTION_MOVE
事件的。
真的收不到嘛?
不绕弯子了,先看效果。我把布局中的TextView
改成Button
,看下结果如何。
<?xml version="1.0" encoding="utf-8"?>
在看log信息:
D/MyFrameLayout: onInterceptTouchEvent: MyFrameLayout MotionEvent.ACTION_DOWN D/MyFrameLayout: onInterceptTouchEvent: : MyFrameLayout MotionEvent.ACTION_MOVE D/MyFrameLayout: onInterceptTouchEvent: : MyFrameLayout MotionEvent.ACTION_MOVE D/MyFrameLayout: onInterceptTouchEvent: : MyFrameLayout MotionEvent.ACTION_UP
却收到了MotionEvent.ACTION_MOVE
事件。
所以,这肯定是跟子控件有关。之前没注意这个问题,不是我没有想到子控件这一因素。而是我所用到的子控件都是系统默认的,没有进行自定义,更没有进行重写onInterceptTouchEvent
和onTouchEvent
,也没有重写dispatchTouchEvent
,所以我就没有考虑到子控件的影响。
Button 与 TextView的区别在哪?
我在另外一篇文章单独介绍二者区别。
Button与TextView的区别?
不仅如此,Button
和 ImageView
也是类似上面的区别。
分析到这,根据Button
和 TextView
所表现的Log信息不同,看出clickable
与 onTouchEvent
的关系。(原因可参考Button与TextView的区别?)
之所以onInterceptTouchEvent
方法收不到ACTION_MOVE
事件,简单说,就是子控件在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
另外一种分析
来,我们先回到问题本身:
为什么
onInterceptTouchEvent
方法收不到ACTION_MOVE
事件?
突然想起《穷查理宝典》一书中的一句话,“反过来想,总是反过来想”。所以,我们就要问,什么时候onInterceptTouchEvent
会收到ACTION_MOVE
事件?
看一下源码:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //省略无关代码 final boolean intercepted; //检查当前ViewGroup是否想要拦截触摸事件 // // 是的话,设置intercepted为true;否则intercepted为false。 // 如果是"按下事件(ACTION_DOWN)" 或者 mFirstTouchTarget不为null;就执行if代码块里面的内容。 ( `mFirstTouchTarget`是"接受触摸事件的View"所组成的单链表。) // 否则的话,设置intercepted为true。 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //只有执行到这个if条件之后,才会执行下面的onInterceptTouchEvent // 检查禁止拦截标记:FLAG_DISALLOW_INTERCEPT // 如果调用了requestDisallowInterceptTouchEvent()标记的话,则FLAG_DISALLOW_INTERCEPT会为true。 // 例如,ViewPager在处理触摸事件的时候,就会调用requestDisallowInterceptTouchEvent() // ,禁止它的父类对触摸事件进行拦截 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //这里进入 ,disallowIntercept为false intercepted = onInterceptTouchEvent(ev); //这里就是执行onInterceptTouchEvent方法的位置 ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } }
执行onInterceptTouchEvent
的条件在于
actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
所以,MotionEvent.ACTION_DOWN
肯定可以执行进去。所以Log信息中包含了MotionEvent.ACTION_DOWN
那么mFirstTouchTarget != null
呢? mFirstTouchTarget
是"接受触摸事件的View"所组成的单链表。
根据上面的源码注释,MotionEvent.ACTION_DOWN
没有获取到, 肯定是因为mFirstTouchTarget == null
,而mFirstTouchTarget
是跟点击事件相关的。
细心的你,可能注意到“
mFirstTouchTarget
跟点击事件有关”这一点在行文中跳跃性比较大。这一点我在下面说明下。
这里也可以解释,Button
和 TextView
所表现的Log信息不同。因为Button
是clickable
的。
如果想要使用TextView,可以有两种改法。
第一,在TextView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行。
第二,在布局文件里面给TextView增加一个android:clickable="true"的属性,这样TextView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
mFirstTouchTarget
为何与点击事件有关
上面我们提到
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
而且分析到
只有执行到这个if条件之后,才会执行下面的onInterceptTouchEvent
MotionEvent.ACTION_DOWN
没有获取到, 肯定是因为mFirstTouchTarget == null
所以,问题是
Button
与TextView
分别作为子控件,为什么前者中,mFirstTouchTarget != null
,后者反之呢?
而我们知道Button与TextView的主要区别在于Button是clickable的,那mFirstTouchTarget
为何与点击事件有关?
下面来分析下这个问题:
首先,clickable
为true
的话,onTouchEvent
将返回true
。
public boolean onTouchEvent(MotionEvent event) { //省略无关代码 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_CANCEL case MotionEvent.ACTION_MOVE: } return true; } return false; }
可以看到,clickable == true
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)
成立,最终返回 return true;
否则,返回false
可以看出,clickable
关系着onTouchEvent
的返回值,也就是当前View是否消费该事件。
其次,mFirstTouchTarget
何时赋值?在ViewGroup
的dispatchTouchEvent
方法,其中的#2662行代码有一下代码,
//如果找到处理这一事件的child view,给mFirstTouchTarget赋值。 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // ..... //给mFirstTouchTarget赋值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; }
如果if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
成立,则通过addTouchTarget(child, idBitsToAssign)
赋值。
第三,if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
何时成立?
在ViewGroup
中,看这个dispatchTransformedTouchEvent
方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Perform any necessary transformations and dispatch. if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); }//关注这,会执行以下代码 handled = child.dispatchTouchEvent(transformedEvent); } return handled; }
关注child.dispatchTouchEvent(transformedEvent)
即可。
进入child.dispatchTouchEvent(transformedEvent)
方法,child
变量是一个View
,进入View
的dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent event) { //省略无关代码 if (!result && onTouchEvent(event)) { result = true; } return result; }
结合第一点,clickable
为true
的话,onTouchEvent
将返回true
。那么onTouchEvent
返回true
的话,dispatchTouchEvent
也返回true
,那么在ViewGroup
中,这个dispatchTransformedTouchEvent
方法也也返回true
。
所以,第二点(向前翻一下,看第二点的分析)就执行addTouchTarget
。(比较枯燥 囧~~)
第四,addTouchTarget
中将mFirstTouchTarget
赋值,使其不为空。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
至此解释了这两个问题。有疑问可及时联系我哦
Button
与TextView
分别作为子控件,为什么前者中,mFirstTouchTarget != null
,后者反之呢?
mFirstTouchTarget
为何与点击事件有关?
总结
-
之所以
onInterceptTouchEvent
方法收不到ACTION_MOVE
事件,简单说,就是子控件在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。 -
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
-
Button
和TextView
所表现的Log信息不同。因为Button
是clickable
的。 -
如果想要使用TextView,可以有两种改法。
第一,在TextView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行。
第二,在布局文件里面给TextView增加一个android:clickable="true"的属性,这样TextView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
参考
Android 触摸事件机制(四) ViewGroup中触摸事件详解
Button与TextView的区别?
android 事件分发
更多相关文章
- Android(安卓)MVVM模式入门
- Android获取设备唯一ID的方法
- 生成appcompat_v7(兼容包)并报错的解决方法
- LruCache源码解析
- RxJava 学习笔记(四)
- Android(安卓)SQLiteOpenHelper的使用
- android-----AsyncTask源码分析
- 面试总结(6):ScheduledExecutorService的使用
- ANDROID STUDIO&&Eclipse Android项目缺少R文件解决方法(完解)