美文网首页Android开发经验谈Android开发Android技术知识
Android 之 View 事件分发深入源码分析 [ 三 ]

Android 之 View 事件分发深入源码分析 [ 三 ]

作者: __Y_Q | 来源:发表于2020-08-17 18:08 被阅读0次

    Android 事件分发之源码分析系列
    Android 之事件分发基础篇 [ 一 ]
    Android 之 ViewGroup 事件分发深入源码分析 [ 二 ]
    Android 之 View 事件分发深入源码分析 [ 三 ]
    Android 之 View 事件分发深入源码分析 [ 总结 ]

    上一篇文章学习了 ViewGroup 的事件分发, 对于 ViewGroup 中的事件分发来说最重要的就是 dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent 这三个方法了.

    ViewActivity 拥有 dispatchTouchEvent, onTouchEvent 两个方法.

    并且在上一篇文章的第五点 dispatchTransformedTouchEvent 中, 最后都调用了 super.dispatchTouchEvent(event); 接下来一起看一下, 事件到 View 中的执行过程吧.

    虽然 ViewGroupView 的之类, 但是这里所说的 View, 专指除 ViewGroup 外的 View 控件, 例如 TextView, Button, CheckBox 等控件本身已经是最小的单位, 不能再作为其他 View 的容器.

    这次依然开始先抛出几个问题. 我们将在源代码中寻找答案.

    1. 在什么时候会调用 onTouch 方法.
    2. onTouch 和 onTouchEvent 谁会先执行.
    3. onClick 方法会在什么时候被调用.
    4. 默认情况下, 控件都是 enable 的, 我们重写了 onClick, onTouch, 但如果设置了 setEnable(false), 会怎么样

    1. View.dispatchTouchEvent

    View.java 12478行

    public boolean dispatchTouchEvent(MotionEvent event) {
        //通过触摸事件判断是否应该被有焦点的 View 处理事件, 如果同时存在拥有焦点的 View, 则设置为 False
        if (event.isTargetAccessibilityFocus()) {
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            event.setTargetAccessibilityFocus(false);
        }
        //设置默认的返回值
        boolean result = false;
        //输入一致性校验, 在上一篇 4.1 中的分析 2 有说明
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        //获取事件类型, 在上一篇 4.1 中的分析 6-7 中有说明.
        final int actionMasked = event.getActionMasked();
        //如果当前事件类型为 按下事件
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 停止正在进行的滑动或者依赖滑动
            stopNestedScroll();
        }
        //是否要过滤掉本次触摸事件. 若窗口被遮挡,返回 false.
        if (onFilterTouchEventForSecurity(event)) {
           //如果当前 view 没有被禁用, 并且没有将事件作为滚动事件.
           //handleScrollBarDragging. 如果将时间作为滚动处理, 返回 true, 否则为 false. 这里会返回 false.
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //那么就来到了这一步. 
            //ListenerInfo 是view的一个内部类, 里面有各种各样的listener,
            //例如OnClickListener,OnLongClickListener,OnTouchListener等等
            ListenerInfo li = mListenerInfo;
          
            //条件 1: li 对象是否为 null. 
            //条件 2: 判断是否通过 setOnTouchListener 设置了监听,即是否有实现 OnTouchListener,
            //条件 3: 判断当前 view 的状态是不是启用的,
            //条件 4: 判断实现的 OnTouchListener 中我们重写的 onTouch 是否返回 true,(在这里调用了 onTouch 方法, 返回 true, 表示处理了事件),
            //上面 4 个条件全部为true,则整个方法直接返回 true.
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //如果上一判断不成立,才会去调用 onTouchEvent 方法.
            //这里证明了, onTouchEvent() 方法调用优先级低于 onTouch() 方法.
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        return result;
    }
    

    问题1 和问题 2, 都在 View.dispatchTouchEvent 得到了答案.

    1. 在什么时候会调用 onTouch 方法.

    ViewdispatchTouchEvent 中判断的时候, 先判断 ListenerInfo 对象是否为 null, 再判断是否实现了 OnTouchListener, 接着判断 View 是否停用. 最后才来调用我们重写的 onTouch 方法.

    1. onTouch 和 onTouchEvent 谁会先执行.

    在调用 onTouch 方法的那个判断条件中, 如果判断不成立, 才会调用 onTouchEvent 方法, 也就是说, onTouch 方法的优先级高, 如果在调用 onTouch 方法中, 返回了 true, 也就是处理了事件, 那么就不会有 onTouchEvent 的什么事了. 不会再调用 onTouchEvent.

    那么接下来是 View.onTouchEvent 的分析.


    2. View.onTouchEvent

    View.java 13718行.

    public boolean onTouchEvent(MotionEvent event) {
         ...
        //判断是否是可点击或者长按,CLICKABLE 有的控件默认是 true, 有的默认是 false.
        //控件的 flag 一般为点击和长按, 都表示为可点击.
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //如果控件为 disabled, 即调用了 setEnable(false), 这里直接就返回出去了.
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            //可点击的禁用视图会消耗触摸事件, 只是不会响应.
            return clickable;
        }
        //如果设置的有委托, 则先调用委托的 onTouchEvent, 如果委托的 onTouchEvent 返回 true, 则表示消耗了事件.
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //重点.
        //分析 1
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                   // 分析 2
                   if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                   }
                   if (!post(mPerformClick)) {
                        performClickInternal();
                   }
                    ...
                    break;
    
                case MotionEvent.ACTION_DOWN:
                    ...
                        break;
    
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
    
            return true;
        }
    
        return false;
    }
    
    • 分析 1
      如果当前控件是可点击的,或者可以悬停和长按显示工具提示.就会进入 if 内部逻辑判断, 在逻辑内一定是会 return true 的. 代表消费掉本次事件., 否则直接 return false 不消费本次事件. 结合 dispatchTouchEvent 和这里的这个判断, 得出, 如果控件是可点击的, 并且没有被过滤掉的, 那么这个控件一定可以消费掉事件.

    • 分析 2
      走到这一步 无论进入到哪一个 if 内, 都会调用 performClickInternal() 方法.
      可以点进 new PerformClick 类看一下, 它实现了 Runnable 接口, 在 run 方法内调用的也是 performClickInternal() 方法.
      而在 performClickInternal() 方法内部又调用了 performClick(), 下面接着看 performClick()


    3. View.performClick()

    View.java 6588 行.

    public boolean performClick() {
        ...
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //和 View.dispatchTouchEvent 中判断类似, dispatchTouchEvent 中判断的是 mOnTouchListener, 这里判断是 mOnClickListener
        //条件 1: ListenerInfo 对象不为 null
        //条件 2: 判断是否通过 setOnClickListener 设置了监听,即是否有实现 OnClickListener,
        if (li != null && li.mOnClickListener != null) {
            //播放声音, 即点击音效.
            playSoundEffect(SoundEffectConstants.CLICK);
            //开始响应点击事件,回调我们重写的 onClick 事件. 
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
            ...
        return result;
    }
    

    这次问题 3 也得到解答了

    1. onClick 方法会在什么时候被调用.

    onTouchEvent() 方法中对 ACTION_UP 的处理过程中, 才会调用 onClick 方法. 如果 onTouch 中直接返回了 true, 那么不会调用 onTouchEvent 方法, 就更不会调用 onClick 了.

    知识点:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    
    可以看到, 如果这个控件本身是不可点击的, 那么会将它先变成可点击的,  所以一般我们只要调用了 `setOnClickListener` 方法, 之前设置的当前控件不可点击就会失效.  
    返回看 `onTouchEvent` 方法, 我们得出了问题 4 的答案.
    
    1. 默认情况下, 控件都是 enable 的, 我们重写了 onClick, onTouch, 但如果设置了 setEnable(false), 会怎么样

    首先在 dispatchTouchEvent 方法中, 不会执行 onTouch, 因为判断中的 && (mViewFlags & ENABLED_MASK) == ENABLED 这个条件不成立. 所以不会调我们重写的 onTouch.
    接着会调用 onTouchEvent , 但是在 onTouchEvent 方法中, 执行到 if ((viewFlags & ENABLED_MASK) == DISABLED) 这个判断的时候, 就会进入内部逻辑然后 return clickable 了, 这个 clickable 状态是 true, 因为我们设置了 setOnClickListener 监听, 所以不会继续向下执行 ACTION_UP 内的逻辑, 也就不会执行 onClick.

    结论: 控件都是 enable 的, 我们重写了 onClick, onTouch, 但如果设置了 setEnable(false), 只会调用 onTouchEvent, 不会调用 onTouch 和 onClick


    后面一篇文章会对 Android 事件分发进行一个简单的总结.

    相关文章

      网友评论

        本文标题:Android 之 View 事件分发深入源码分析 [ 三 ]

        本文链接:https://www.haomeiwen.com/subject/rmnfjktx.html