美文网首页
Android事件分发机制

Android事件分发机制

作者: 我就是非主流 | 来源:发表于2019-01-26 21:15 被阅读0次

    事件分发机制主要是指触摸事件在Activity、ViewGroup、View之间传递并消费的机制,分发顺序为 Activity > ViewGroup > View;

    主要方法:
    VIewGroup相关:onInterceptTouchEvent()、dispatchTouchEvent()、onTouchEvent()
    View相关:dispatchTouchEvent()、onTouchEvent()

    1. Activity事件分发机制

    一般情况下用户按下Activity时,Activity会执行dispatchTouchEvent方法,开始分发流程,我们看下dispatchTouchEvent()方法源码:

        //activity的事件分发方法
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    dispatchTouchEvent方法里就两个处理:

    (1)MotionEvent.ACTION_DOWN事件执行时会调用onUserInteraction()方法;我们看看这个方法做了啥;

        public void onUserInteraction() {
        }
    

    这个方法实现是空的,不过我们可以从注释和其他途径可以了解到,该方法主要的作用是实现屏保功能,并且当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等都会触发这个方法。

    (2)接下来是if()语句执行了getWindow().superDispatchTouchEvent(ev)方法,能看出来他是获取了Window并调用了其superDispatchTouchEvent方法,但你会发现Window是个抽象类,superDispatchTouchEvent是个抽象方法,看不出任何东西,然后通过源码追踪你会发现其实getWindow获取的是Window的子类PhoneWindow;

        final void attach(){
        ...
        mWindow = new PhoneWindow(this, window,     activityConfigCallback);
        ...
        }
    

    所以我们来看看PhoneWindow的superDispatchTouch方法:

        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    直接调用了DecorView的superDispatchTouchEvent方法,DecorView继承自FrameLayout,是顶层View,所有界面的父类,而FrameLayout是ViewGroup的子类,所以最终调用的是ViewGroup的dispatchTouchEvent方法。

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
        ...
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
        ...
    }
    
    2. ViewGroup的事件分发机制

    我们来看看ViewGroup的dispatchTouchEvent方法(Android5.0前的方法,5.0之后的方法比较复杂,但原理一样):

    public boolean dispatchTouchEvent(MotionEvent ev) { 
        ... // 仅贴出关键代码
            // 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
                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;  
    
            // 重点分析2
                // 通过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;  
    
            // 重点分析3
            // 若点击的是空白处(即无任何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()
            } 
    
            ... 
    }
    

    (1)ViewGroup分发事件时首先判断当前Viewgroup有没有拦截事件onInterceptTouchEvent(ev),若返回值为true则表明已经拦截了此事件,跳出判断,事件不会往下传递,若是flase则相反,事件会往下传递。
    (2)获取到ViewGroup的所有子View及子ViewGroup,通过for循环遍历;先判断次子View是否是正在点击的View(通过位置判断),如果是的话调用此子View的dispatchTouchEvent方法,这个方法是有返回值的,如果返回值为true就表明事件已被消费,跳出整个循环,不再往下传递,事件到此结束。
    (3)如果点击的是空白处或事件被拦截的情况下会调用父类的dispatchTouchEvent()方法,即View.dispatchTouchEvent(),自己处理该事件,不会再往下传递。

    分发流程: ViewGroup分发流程
    3. View事件的分发机制

    View的分发机制也是从dispatchTouchEvent()开始的,我们分析下源码;

    public boolean dispatchTouchEvent(MotionEvent event) {  
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                    mOnTouchListener.onTouch(this, event)) {  
                return true;  
            } 
            return onTouchEvent(event);  
      }
    

    方法里有一个if判断,里面有三个条件,若满足了这三个条件会返回true,即消费了该事件,否则执行onTouchEvent(),我们逐一分析这三个条件;
    (1)mOnTouchListener != null,需要再View.setOnTouchListener()里注册Touch事件;
    (2)(mViewFlags & ENABLED_MASK) == ENABLED,判断当前View是否enable,很多View默认enable;
    (3)mOnTouchListener.onTouch(this, event) 回调以注册的Touch方法:

    button.setOnTouchListener(new OnTouchListener() {  
            @Override  
            public boolean onTouch(View v, MotionEvent event) {  
         
                return false;  
            }  
        });
    

    若onTouch返回了true,上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
    若onTouch返回了false,上述三个条件不成立,执行onTouchEvent(event);

    4. 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;  
            }
    
    /**
      * 分析1:performClick()
      */  
        public boolean performClick() {  
    
            if (mOnClickListener != null) {  
                playSoundEffect(SoundEffectConstants.CLICK);  
                mOnClickListener.onClick(this);  
                return true;  
                // 只要我们通过setOnClickListener()为控件View注册1个点击事件
                // 那么就会给mOnClickListener变量赋值(即不为空)
                // 则会往下回调onClick() & performClick()返回true
            }  
            return false;  
        }  
    

    如果View是可点击的就返回true,否则返回false。


    控件被点击

    可以看出onTouch()的执行优先于onClick(),所以一旦在onTouch()消费了事件onClick()就不会执行。

    5. 分发流程
    分发流程
    6. 总结

    事件分发从activity的dispatchTouchEvent方法开始往下遍历子View逐个寻找消费控件,如果有控件或布局消费了此事件,会执行这个控件或布局的onTouchEvent()方法,到此事件结束,不会再往下传递,如果没有控件或布局消费此事件,view层级从里到外逐个调用onTouchEvent()方法,直到activity自己消费此事件为止。

    相关文章

      网友评论

          本文标题:Android事件分发机制

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