文章目录

    • 问题实例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事件。

所以,这肯定是跟子控件有关。之前没注意这个问题,不是我没有想到子控件这一因素。而是我所用到的子控件都是系统默认的,没有进行自定义,更没有进行重写onInterceptTouchEventonTouchEvent,也没有重写dispatchTouchEvent,所以我就没有考虑到子控件的影响。

Button 与 TextView的区别在哪?

我在另外一篇文章单独介绍二者区别。

Button与TextView的区别?

不仅如此,ButtonImageView 也是类似上面的区别。

分析到这,根据ButtonTextView 所表现的Log信息不同,看出clickableonTouchEvent的关系。(原因可参考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跟点击事件有关”这一点在行文中跳跃性比较大。这一点我在下面说明下。

这里也可以解释,ButtonTextView 所表现的Log信息不同。因为Buttonclickable的。

如果想要使用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


所以,问题是

ButtonTextView分别作为子控件,为什么前者中,mFirstTouchTarget != null,后者反之呢?

而我们知道Button与TextView的主要区别在于Button是clickable的,那mFirstTouchTarget 为何与点击事件有关?


下面来分析下这个问题:
首先,clickabletrue的话,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 何时赋值?在ViewGroupdispatchTouchEvent方法,其中的#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,进入ViewdispatchTouchEvent方法。

    public boolean dispatchTouchEvent(MotionEvent event) {    //省略无关代码            if (!result && onTouchEvent(event)) {                result = true;            }        return result;    }

结合第一点,clickabletrue的话,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;    }

至此解释了这两个问题。有疑问可及时联系我哦

ButtonTextView分别作为子控件,为什么前者中,mFirstTouchTarget != null,后者反之呢?

mFirstTouchTarget 为何与点击事件有关?


总结

  1. 之所以onInterceptTouchEvent 方法收不到ACTION_MOVE事件,简单说,就是子控件在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。

  2. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

  3. ButtonTextView 所表现的Log信息不同。因为Buttonclickable的。

  4. 如果想要使用TextView,可以有两种改法。

    第一,在TextView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行。

    第二,在布局文件里面给TextView增加一个android:clickable="true"的属性,这样TextView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

参考

Android 触摸事件机制(四) ViewGroup中触摸事件详解
Button与TextView的区别?
android 事件分发

更多相关文章

  1. Android(安卓)MVVM模式入门
  2. Android获取设备唯一ID的方法
  3. 生成appcompat_v7(兼容包)并报错的解决方法
  4. LruCache源码解析
  5. RxJava 学习笔记(四)
  6. Android(安卓)SQLiteOpenHelper的使用
  7. android-----AsyncTask源码分析
  8. 面试总结(6):ScheduledExecutorService的使用
  9. ANDROID STUDIO&&Eclipse Android项目缺少R文件解决方法(完解)

随机推荐

  1. android实现图片平铺效果&WebView多点触
  2. 在android系统命令行中执行arm linux程序
  3. Android NDK 调用C
  4. Android的图像处理
  5. Android(安卓)Studio(3)---Android(安卓)St
  6. android访问NFC的SE
  7. Android通用UI封装----“我的”页面Item
  8. 【android笔记】之 android studio (一)
  9. TextView 实现跑马灯效果
  10. android:layout_gravity 居中布局