Android事件分发机制 & 面试解析

先总结一下事件分发机制的流程

  • 事件分发从Action_Down开始,最初由Activity的dispatchTouchEvent()方法接收,不拦截不中断的正常分发流程:Activity的disPatchTouchEvent()方法到PhoneWindow的superDispatchTouchEvent方法,再到DecorView的superDispatchTouchEvent方法,再到ViewGroup的dispatchTouchEvent方法,在ViewGroup的dispatchTouchEvent方法中判断是否拦截,若拦截调用ViewGroup的onTouchEvent方法,该ViewGroup消费掉;若不拦截,该ViewGroup遍历子View根据点击的位置等条件判断是否为接收事件的子View,是,则分发给该子View的dispatchTouchEvent()方法,然后会调用View的onTouchEvent方法,在onTouchEvent方法中会判断该子View是否可点击,是,则事件最终传递到View的onClick方法消费;否则,事件返回向上传递,直到消费或者终止。
  • 在dispatchTouchEvent()方法中返回true或者false,事件不向下传递,只用调用super.dispatchTouchEvent方法,事件才会向下传递。
  • 在onTouchEvent()方法中返回true,事件在该方法中消费,不会向下或者向上传递;返回super.onTouchEvent方法,将会调用View onTouchEvent方法,判断长按事件和点击事件的执行条件存不存在,存在则会在点击事件中消费。
  • 在onInterceptTouchEvent()方法中返回true表示拦截事件,事件可能会在该ViewGroup中消费掉;返回false表示事件继续往下传递。

ViewGroup 默认拦截事件吗?

答:默认不拦截任何事件,onInterceptTouchEvent返回的是false。

一旦有事件传递给view,view的onTouchEvent一定会被调用吗?

答:是的,因为view 本身没有onInterceptTouchEvent方法,所以只要事件来到view这里 就一定会走onTouchEvent方法。
并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale
Button的clickable就是true 但是textview是false。

enable是否影响view的onTouchEvent返回值?

答:不影响,只要clickable和longClickable有一个为真,那么onTouchEvent就返回true。

requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?

答:肯定可以,但是down事件干扰不了。

dispatchTouchEvent每次都会被调用吗?

答:是的,onInterceptTouchEvent则不会。

滑动冲突问题如何解决 思路是什么?

要解决滑动冲突核心的方法就是2个 外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。 学会这2种 基本上所有的滑动冲突都是这2种的变种,而且核心代码思想都一样。

  • 外部拦截法:思路就是重写父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制 逻辑是一模一样的。
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            //down事件肯定不能拦截 拦截了后面的就收不到了
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (你的业务需求) {
                    //如果确定拦截了 就去自己的onTouchEvent里 处理拦截之后的操作和效果 即可了
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                //up事件 我们一般都是返回false的 一般父容器都不会拦截他。 因为up是事件的最后一步。这里返回true也没啥意义
                //唯一的意义就是因为 父元素 up被拦截。导致子元素 收不到up事件,那子元素 就肯定没有onClick事件触发了,这里的
                //小细节 要想明白
                intercepted = false;
                break;
            default:
                break;
        }
        return intercepted;
    }
  • 内部拦截法:内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了 就最好,没消耗 自然就转给父容器处理了。

子元素代码:

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (如果父容器需要这个点击事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }//否则的话 就交给自己本身view的onTouchEvent自动处理了
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

父亲容器代码也要修改一下,其实就是保证父亲别拦截down:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return false;

        }
        return true;
    }

怎么解决 ScrollView 嵌套 ScrollView 后内部 ScrollView 无法滑动的问题?

答:根本原因就是因为用户的滑动操作都被外部 ScrollView 拦截并消费了,导致内部 ScrollView 一直无法响应滑动事件。

这里选择使用内部拦截法来解决问题。首先需要让外部 ScrollView 拦截 ACTION_DOWN 之外的任何事件

class ExternalScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {

    override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
        val intercepted: Boolean
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = false
                super.onInterceptTouchEvent(motionEvent)
            }
            else -> {
                intercepted = true
            }
        }
        return intercepted
    }

}

内部 ScrollView 判断自身是否还处于可滑动状态,如果滑动到了最顶部还想再往下滑动,或者是滑动到了最底部还想再往上滑动,那么就将事件都交由外部 ScrollView 处理,其它情况都直接拦截并消费掉事件,这样内部 ScrollView 就可以实现内部滑动了

class InsideScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {

    private var lastX = 0f

    private var lastY = 0f

    override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
        val x = motionEvent.x
        val y = motionEvent.y
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - lastX
                val deltaY = y - lastY
                if (abs(deltaX) < abs(deltaY)) { //上下滑动的操作
                    if (deltaY > 0) { //向下滑动
                        if (scrollY == 0) { //滑动到顶部了
                            parent.requestDisallowInterceptTouchEvent(false)
                        }
                    } else { //向上滑动
                        if (height + scrollY >= computeVerticalScrollRange()) { //滑动到底部了
                            parent.requestDisallowInterceptTouchEvent(false)
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
            }
        }
        lastX = x
        lastY = y
        return super.dispatchTouchEvent(motionEvent)
    }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容