深入理解事件分发机制

1.事件分发的顺序
即事件传递的顺序:Activity->ViewGroup->View
事件分发过程由哪些方法协作完成
答:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()
dispatchTouchEvent():分发点击事件,当点击事件能够传递给当前View,该方法就会调用
onTouchEvent():处理点击事件,在dispatchTouchEvent()内部调用
onInterceptTouchEvent():判断是否拦截了某个事件,只存在于ViewGroup中,普通的View无法使用,在ViewGroup的dispatchTouchEvent()内部调用
本着知其然,知其所以然的追求,我从源码分析,一步一步解开谜团
源码分析
Activity.dispatchTouchEvent()
在这里插入图片描述Activity.dispatchTouchEvent里会执行getWindow().superDispatchTouchEvent()方法,如果此方法为true,说明ViewGroup/子View有消费DOWN事件,那么dispatchTouchEvent()也返回true,那么流程结束,如果getWindow.superDispatchTouchEvent为false,说明ViewGroup/子View都不消费,那么执行Activity的onTouchEvent(),这也就是说Activity消不消费事件要看ViewGroup/View两位大佬的脸色。
产生的问题
1.getWindow()获取的对象是什么?
2.Activity.onTouchEvent()里面是什么?
Activity.onTouchEvent()

public boolean onTouchEvent(MotionEvent event) {

    // 当一个点击事件未被Activity下任何一个View接收 / 处理时

    // 应用场景:处理发生在Window边界外的触摸事件

    if (mWindow.shouldCloseOnTouch(this, event)) {

        finish();

        return true;

    }

    return false;

    // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕

}

onTouchEvent()并没有做太多的事情,只是进行了边界外的触摸事件判断,不管onTouchEvent()返回true还是false,Activity.dispatchTouchEvent()都会结束事件

getWindow.superDispatchTouchEvent()

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

    return mDecor.superDispatchTouchEvent(event);

    // mDecor = 顶层View(DecorView)的实例对象

}

public boolean superDispatchTouchEvent(MotionEvent event) {

    return super.dispatchTouchEvent(event);

    // 调用父类的方法 = ViewGroup的dispatchTouchEvent()

    // 即 将事件传递到ViewGroup去处理,详细请看ViewGroup的事件分发机制

}

首先要说明,getWindow()获取的是Window类的对象,Window类是抽象类,其唯一实现类是PhoneWindow类;所以getWindow()实际获取的是PhoneWindow类对象;然后调用了mDecor.superDispatchTouchEvent(),这里要说明,mDecor是顶层DecorView的实例对象,DecorView类是phoneWindow类的一个内部类,DecorView继承自FrameLayout,是所有界面的父类,FrameLayout是ViewGroup的子类,所以DecorView的间接父类是ViewGroup。在mDecor.superDispatchTouchEvent()中又调用了super.dispatchTouchEvent(),也就是父类ViewGroup的方法,由此可知,事件传递由Activity传递到了ViewGroup中。
产生的问题
1.ViewGroup.dispatchTouchEvent()是什么?
ViewGroup.dispatchTouchEvent()
ViewGroup.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {

... // 仅贴出关键代码

        if (disallowIntercept || !onInterceptTouchEvent(ev)) { 

        // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改

        // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反

                // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部

                // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断

                // c. 关于onInterceptTouchEvent() ->>分析1

            ev.setAction(MotionEvent.ACTION_DOWN); 

            final int scrolledXInt = (int) scrolledXFloat; 

            final int scrolledYInt = (int) scrolledYFloat; 

            final View[] children = mChildren; 

            final int count = mChildrenCount; 

        // 通过for循环,遍历了当前ViewGroup下的所有子View

        for (int i = count - 1; i >= 0; i--) { 

            final View child = children[i]; 

            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE 

                    || child.getAnimation() != null) { 

                child.getHitRect(frame); 

                // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View

                // 若是,则进入条件判断内部

                if (frame.contains(scrolledXInt, scrolledYInt)) { 

                    final float xc = scrolledXFloat - child.mLeft; 

                    final float yc = scrolledYFloat - child.mTop; 

                    ev.setLocation(xc, yc); 

                    child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 

                    // 条件判断的内部调用了该View的dispatchTouchEvent()

                    // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)

                    if (child.dispatchTouchEvent(ev))  {

                    mMotionTarget = child; 

                    return true;

                    // 调用子View的dispatchTouchEvent后是有返回值的

                    // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立

                    // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出

                    // 即把ViewGroup的点击事件拦截掉

                            } 

                        } 

                    } 

                } 

            } 

        } 

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || 

                (action == MotionEvent.ACTION_CANCEL); 

        if (isUpOrCancel) { 

            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; 

        } 

        final View target = mMotionTarget; 

    // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)

    if (target == null) { 

        ev.setLocation(xf, yf); 

        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { 

            ev.setAction(MotionEvent.ACTION_CANCEL); 

            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 

        } 

        return super.dispatchTouchEvent(ev);

        // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()

        // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())

        // 此处需与上面区别:子View的dispatchTouchEvent()

    }

    ...

}

/**

  • 作用:是否拦截事件

  • 说明:

  • a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)

  • b. 返回false = 不拦截(默认)

*/

public boolean onInterceptTouchEvent(MotionEvent ev) {

return false;

}

在ViewGroup.dispatchTouchEvent()中,需要调用onInterceptTouchEvent()来询问是否拦截事件,首先要说明,onInterceptTouchEvent()是ViewGroup特有的方法,普通的View是没有的。从源码可知onInterceptTouchEvent()默认是返回false的,也就是说默认是不拦截的。程序继续执行,会遍历当前ViewGroup的所有子View,判断是否存在当前点击的子View,如果存在会执行child.dispatchTouchEvent()方法,也就是子View.dispatchTouchEvent(),这也就是事件分发从ViewGroup向View的传递;如果child.dispatchTouchEvent()返回true,那么ViewGroup.dispatchTouchEvent()返回true;如果child.dispatchTouchEvent()返回false,说明不消费,ViewGroup执行super.dispatchTouchEvent(),也就是View.dispatchTouchEvent(),实际在super.dispatchTouchEvent中会执行onTouch->onTouchEvent->onClick,下面会分析。由此可知,ViewGroup是否可以消费事件,还要看子View这位大佬的脸色啊。
产生的问题
1.ViewGroup调用的super.dispatchTouchEvent(),也就是View.dispatchTouchEvent()是什么?
View.dispatchTouchEvent()
/**

  • 源码分析:View.dispatchTouchEvent()

*/

public boolean dispatchTouchEvent(MotionEvent event) {

    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && 

            mOnTouchListener.onTouch(this, event)) { 

        return true; 

    }

    return onTouchEvent(event); 

}

// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()

// 1. mOnTouchListener != null

// 2. (mViewFlags & ENABLED_MASK) == ENABLED

// 3. mOnTouchListener.onTouch(this, event)

// 下面对这3个条件逐个分析

/**

  • 条件1:mOnTouchListener != null

  • 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值

*/

public void setOnTouchListener(OnTouchListener l) {

mOnTouchListener = l; 

// 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)

}

/**

  • 条件2:(mViewFlags & ENABLED_MASK) == ENABLED

  • 说明:

  • a. 该条件是判断当前点击的控件是否enable

  • b. 由于很多View默认enable,故该条件恒定为true

*/

/**

  • 条件3:mOnTouchListener.onTouch(this, event)

  • 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)

*/

button.setOnTouchListener(new OnTouchListener() { 

    @Override 

    public boolean onTouch(View v, MotionEvent event) { 

        return false; 

    } 

});

// 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束

// 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

在View.dispatchTouchEvent()中,首先会执行判断控件是否注册了OnTouchListener,如果没有注册就执行View.onTouchEvent();如果注册了,还要判断onTouch方法里面返回的是true还是false,如果是true不会继续往下执行View.onTouchEvent(),如果是false会继续执行View.onTouchEvent();所以由此看出,控件注册了OnTouchListener监听,并且实现的方法onTouch()返回了true,是不会执行的View.onTouchEvent()的。
产生的问题
1.假如没有注册OnTouchListener监听器或者onTouch()返回了false,执行了View.onTouchEvent(),那么该方法会是什么呢?
View.onTouchEvent()

/**

  • 源码分析:View.onTouchEvent()

*/

public boolean onTouchEvent(MotionEvent event) {

final int viewFlags = mViewFlags; 

if ((viewFlags & ENABLED_MASK) == DISABLED) { 

    return (((viewFlags & CLICKABLE) == CLICKABLE || 

            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); 

} 

if (mTouchDelegate != null) { 

    if (mTouchDelegate.onTouchEvent(event)) { 

        return true; 

    } 

} 

// 若该控件可点击,则进入switch判断中

if (((viewFlags & CLICKABLE) == CLICKABLE || 

        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 

            switch (event.getAction()) {

                // a. 若当前的事件 = 抬起View(主要分析)

                case MotionEvent.ACTION_UP: 

                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; 

                        ...// 经过种种判断,此处省略

                        // 执行performClick() ->>分析1

                        performClick(); 

                        break; 

                // b. 若当前的事件 = 按下View

                case MotionEvent.ACTION_DOWN: 

                    if (mPendingCheckForTap == null) { 

                        mPendingCheckForTap = new CheckForTap(); 

                    } 

                    mPrivateFlags |= PREPRESSED; 

                    mHasPerformedLongPress = false; 

                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 

                    break; 

                // c. 若当前的事件 = 结束事件(非人为原因)

                case MotionEvent.ACTION_CANCEL: 

                    mPrivateFlags &= ~PRESSED; 

                    refreshDrawableState(); 

                    removeTapCallback(); 

                    break;

                // d. 若当前的事件 = 滑动View

                case MotionEvent.ACTION_MOVE: 

                    final int x = (int) event.getX(); 

                    final int y = (int) event.getY(); 

                    int slop = mTouchSlop; 

                    if ((x < 0 - slop) || (x >= getWidth() + slop) || 

                            (y < 0 - slop) || (y >= getHeight() + slop)) { 

                        // Outside button 

                        removeTapCallback(); 

                        if ((mPrivateFlags & PRESSED) != 0) { 

                            // Remove any future long press/tap checks 

                            removeLongPressCallback(); 

                            // Need to switch from pressed to not pressed 

                            mPrivateFlags &= ~PRESSED; 

                            refreshDrawableState(); 

                        } 

                    } 

                    break; 

            } 

            // 若该控件可点击,就一定返回true

            return true; 

        } 

        // 若该控件不可点击,就一定返回false

        return false; 

    }

public boolean performClick() { 

    if (mOnClickListener != null) { 

        playSoundEffect(SoundEffectConstants.CLICK); 

        mOnClickListener.onClick(this); 

        return true; 

        // 只要我们通过setOnClickListener()为控件View注册1个点击事件

        // 那么就会给mOnClickListener变量赋值(即不为空)

        // 则会往下回调onClick() & performClick()返回true

    } 

    return false; 

}

在View.onTouchEvent()中,若控件可点击一定返回true,不可点击就一定返回false,并且在switch语句中执行了performClick(),在performClick()方法中执行onClick方法,由此可以看出整个事件对于是否消费,View的决定权是最大的,如果View不消费会让ViewGroup去决定是否消费,ViewGroup如果不消费再由Activity去决定是否消费。

总结:
1.一个事件后,首页传递给Activity,然后一层一层往下传,从上往下调用dispatchTouchEvent方法传递事件
2.如果事件传递给最下层的View还没有被消费,就会按照反方向传给Activity,从下往上调用onTouchEvent方法,最后回到Activity的onTouchEvent方法,如果Activity也没有消费处理这个事件,这个事件就会被抛弃
3.dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费还是继续往下分给子控件处理,返回true表示不继续分发,事件没有被消费;返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent方法进行判断是否拦截该事件
4.onTouchEvent方法用于事件的处理,返回true表示消费当前事件,返回false则不处理,交给子控件进行继续分发
5.onInterceptTouchEvent是ViewGroup中才有的方法,普通View是没有的,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而Android子View是不能再包含子View的
6.上层View既可以直接拦截该事件,自己处理,也可以先询问子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理,既保证了事件的有序性,又非常的灵活
7.事件由父View传递给子View,ViewGroup可以通过onInterceptTouchEvent方法对事件拦截,停止其向子View传递
8.如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来,也就是ACTION_DOWN必须是true,之后的事件才会传递进来

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值