×

深入聊聊Android事件分发机制

96
zhangke3016
2017.02.09 23:13* 字数 1771

在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从而直到滑动结束。

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

android进阶之路
Web note ad 1