深入聊聊Android事件分发机制

作者: zhangke3016 | 来源:发表于2017-02-09 23:13 被阅读463次

    在Android开发的过程中,自定义控件一直是我们绕不开的话题。而在这个话题中事件分发机制也是其中的重点和疑点,特别是当我们处理控件嵌套滑动事件时,正确的处理各个控件间事件分发拦截状态,可以实现更炫酷的控件动画效果。

    一、事件分发机制介绍

    关于Android事件分发,我们主要分ViewGroup和View两个事件处理部分进行介绍,主要研究在处理事件过程中关注最多的三个方法dispatchTouchEventonInterceptTouchEventonTouchEvent,在ViewGroup和View对三个方法的支持如下图所示:

    | 事件种类 | ViewGroup | View |
    | ------------- |: ------------- :| -----:|
    | dispatchTouchEvent | 有 | 有 |
    | onInterceptTouchEvent | 有 | 无 |
    | onTouchEvent | 有 | 有|

    在Android中,当用户触摸界面时系统会把产生一系列的MotionEvent,通过ViewGroup 的dispatchTouchEvent方法开始向下分发事件,在dispatchTouchEvent方法中,会调用onInterceptTouchEvent方法,如果该方法返回true,表明当前控件拦截了该事件,此后事件交由该控件处理并不再调用该控件的onInterceptTouchEvent方法。最后交由该控件的onTouchEvent方法对事件进行处理。如果当前控件在onInterceptTouchEvent方法中返回false,表示不拦截该控件,之后交由其子控件进行判断是否对事件进行拦截处理。可以用如下伪代码来对其进行处理:

    public boolean dispatchTouchEvent(MotionEvent event) {
            boolean consume = false;
            if (onInterceptTouchEvent(event)) {
                consume = onTouchEvent(event);
            } else {
                consume = child.dispatchTouchEvent(event);
            }
            return consume;
        }
    
    事件分发

    先说结论再细分析:

    1. 事件是由其父视图向子视图传递,如图为A->B->C
    1. 如果当前控件需要拦截该事件,则在onInterceptTouchEvent方法中返回true,但真正决定是否处理事件是在onTouchEvent方法中,也就是说如果此时onTouchEvent方法返回了false,则此控件也表示不处理该事件,交由父控件的onTouchEvent方法来判断处理。如图:当事件由A分发至B,B在其onInterceptTouchEvent方法中返回true表示要拦截该事件,此时事件将不会再传给C,但在B的onTouchEvent方法中返回了false,表示不处理该事件,则事件以此向上传递交由A控件的onTouchEvent方法处理。即onInterceptTouchEvent负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent返回true的那个view进行处理。
    2. 一旦控件确定处理该事件,则后续事件序列也会交由该控件处理,同时该控件的onInterceptTouchEvent方法将不再调用。
    3. 由于View没有onInterceptTouchEvent方法,在其dispatchTouchEvent方法中调用onTouchEvent方法处理事件,如果返回false则表示事件不作处理。同时其ACTION_MOVE、ACTION_UP不会得到响应。
    4. View的OnTouchListener优先于onTouchEvent方法执行,如果OnTouchListener方法返回true,那么View的dispatchTouchEvent方法就返回true。而后则onTouchEvent方法得不到执行,同时因为onClick方法在onTouchEvent方法的ACTION_UP中调用,onClick方法也得不到执行。

    情况一、A\B\C onInterceptTouchEvent onTouchEvent均返回false

    | 事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
    | ------------- |: ------------- :| -----:| -----:|
    | onInterceptTouchEvent | false | false | 无 |
    | onTouchEvent | false | false | false |

    事件处理

    当A、B、C同时返回false时,事件传递为A(onInterceptTouchEvent) -->B(onInterceptTouchEvent) -->C(onTouchEvent)-->B(onTouchEvent) -->A(onTouchEvent),也就是事件从A传至C时,都没有拦截和处理事件,则事件再次向上传递调用B和A的onTouchEvent方法。

    看下打印的结果:

    事件分发

    情况二、B onInterceptTouchEvent 方法返回true

    | 事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
    | ------------- |: ------------- :| -----:| -----:|
    | onInterceptTouchEvent | false | true | 无 |
    | onTouchEvent | false | false | false |

    当BonInterceptTouchEvent返回true时表示拦截了事件,C控件就无法响应该事件。

    事件分发 打印结果

    情况三、B onInterceptTouchEventonTouchEvent方法返回true

    | 事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
    | ------------- |: ------------- :| -----:| -----:|
    | onInterceptTouchEvent | false | true | 无 |
    | onTouchEvent | false | true | false |

    当BonInterceptTouchEventonTouchEvent返回true时表示拦截处理了事件,C控件就无法响应该事件,同时事件在B的onTouchEvent之后将不再向上传递,随后事件将不再调用其onInterceptTouchEvent方法。

    事件分发 打印结果

    情况四、C onTouchEvent方法返回true

    | 事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
    | ------------- |: ------------- :| -----:| -----:|
    | onInterceptTouchEvent | false | false | 无 |
    | onTouchEvent | false | false | true |

    当ConTouchEvent返回true时表示处理了该事件,之后事件就交由C控件处理,同时事件在C的onTouchEvent之后将不再向上传递。

    事件分发 打印结果

    情况五、A onInterceptTouchEvent方法返回true

    | 事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
    | ------------- |: ------------- :| -----:| -----:|
    | onInterceptTouchEvent | true | false | 无 |
    | onTouchEvent | false | false | false |

    当AonInterceptTouchEvent返回true时表示拦截了事件,之后事件就交由A的onTouchEvent方法处理,B、C就无法响应该事件。如果AonTouchEvent方法返回false,其ACTION_MOVE、ACTION_UP事件不会得到响应。

    事件分发
    @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e(TAG, "A --- onTouchEvent");
            switch (event.getAction()){
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "A --- onTouchEvent :ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "A --- onTouchEvent :ACTION_UP");
                    break;
            }
            return false;//super.onTouchEvent(event);
        }
    
    打印结果

    二、实现侧滑删除效果

    运用上面的知识学习,我们来实现一下简单的侧滑删除效果吧~

    侧滑删除效果

    其核心代码主要在于对事件的拦截和处理上:

     @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    //        boolean intercepter = false;
            Log.e("TAG", "onInterceptTouchEvent: "+ev.getAction());
    
            boolean intercepter = false;
            if (isMoving)
                intercepter = true;
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    downX = (int) ev.getX();
                    downY = (int) ev.getY();
    
                    if (mVelocityTracker == null)
                        mVelocityTracker = VelocityTracker.obtain();
    
                    mVelocityTracker.clear();
                    break;
                case MotionEvent.ACTION_MOVE:
    
                    moveX = (int) ev.getX();
                    moveY = (int) ev.getY();
    
    
                    Log.e("TAG", "getScrollX: "+getScrollX() );
                    if (Math.abs(moveX - downX) > 0){
                        intercepter = true;
    
                        //Log.e("TAG","onInterceptTouchEvent: ");
                        //scrollBy(moveX - downX,0);
    
                    }else {
                        intercepter = false;
                    }
    
                    downX = moveX;
                    downY = moveY;
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
    
                    intercepter = false;
    
                    break;
            }
    
            //scrollBy(45,0);
            return intercepter;//
            //super.onInterceptTouchEvent(ev);
    
        }
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
    
            Log.e("TAG", "onTouchEvent: "+ev.getAction() );
    
            mVelocityTracker.addMovement(ev);
            switch (ev.getAction()){
                
                case MotionEvent.ACTION_MOVE:
    
                    moveX = (int) ev.getX();
                    moveY = (int) ev.getY();
    
                    mVelocityTracker.computeCurrentVelocity(1000);
                    Log.e("TAG", "getScrollX: "+getScrollX() );
    
                    if (getScrollX()+downX - moveX>=0 && getScrollX()+downX - moveX <= view1.getMeasuredWidth()){
    
                        scrollBy(downX - moveX,0);
                     }
    
                    isMoving = true;
                    downX = moveX;
                    downY = moveY;
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
    
                    Log.e("TAG1", "getXVelocity: "+mVelocityTracker.getXVelocity() );
                    Log.e("TAG1", "getYVelocity: "+mVelocityTracker.getYVelocity() );
                    //
                    if (getScrollX()>=view1.getMeasuredWidth()/2 || mVelocityTracker.getXVelocity() < -ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity()){
                        //scrollTo(view1.getMeasuredWidth(),0);
                        open();
                    }else {
                        //scrollTo(0,0);
                       close();
                    }
    
                    mVelocityTracker.clear();
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                    break;
            }
            return true;//super.onTouchEvent(ev);
        }
    

    这里整个父布局继承自ViewGroup,在onMeasure中测量子控件大小:

     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
        }
    
    

    onFinishInflate方法中获取各个子控件:

    @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
             view = getChildAt(0);
             view1 = getChildAt(1);
            if (mScroller == null)
                mScroller = new Scroller(getContext());
    
            view.setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View mViewm, MotionEvent mMotionEventm) {
                    if (mMotionEventm.getAction() == MotionEvent.ACTION_UP
                            && isOpen){
                        close();
                    }
                    if (mMotionEventm.getAction() == MotionEvent.ACTION_DOWN){
                        if (mOnChangeMenuListener!=null){
                            mOnChangeMenuListener.onStartTouch();
                        }
                    }
                    return false;
                }
            });
        }
    

    并在onLayout方法中布局子控件:

    @Override
        protected void onLayout(boolean mBm, int mIm, int mIm1, int mIm2, int mIm3) {
            if (getChildCount()!=2){
                throw new IllegalArgumentException("必须包含两个子控件");
            }
            Log.e("TAG", "onLayout:getWidth "+view.getWidth() );
                view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());
                view1.layout(view.getMeasuredWidth(),0,view.getMeasuredWidth()+view1.getMeasuredWidth(),view1.getMeasuredHeight());
    
        }
    

    重点在对onInterceptTouchEventonTouchEvent方法的处理,我们在onInterceptTouchEvent中处理是否拦截该事件。如果手指是向左滑动,则表示用户在进行侧滑删除操作,则拦截该事件,需要注意的是,一旦拦截了该事件,之后事件将不调用该控件的onInterceptTouchEvent方法,所以我们将具体的处理逻辑放在onTouchEvent方法中,该方法返回true表示处理该事件,此后事件都由dispatchTouchEvent方法交由onTouchEvent方法处理。在onTouchEvent方法中调用scrollBy方法实现控件左右滑动,从而实现类似侧滑删除效果。

    @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                invalidate();
            }else {
                isMoving = false;
            }
        }
    

    为使滑动效果更自然,用Scroller在手指抬起的时候控制控件打开或者闭合,Scroller的使用也很简单,抬起时调用其startScroll方法并刷新界面,在控件computeScroll方法中判断是否滑动完毕并刷新界面,在invalidate方法中会调用computeScroll从而直到滑动结束。

    好了,总的实现就这么多,希望可以加深对事件分发机制的理解~

    相关文章

      网友评论

      • 快乐小哥:1写的不错 很清楚 受教了 有demo吗?
        快乐小哥:@zhangke3016谢谢拉 受益匪浅
        zhangke3016:嗯,测试demo下载:http://download.csdn.net/detail/zhangke3016/9752543

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

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