深入聊聊Android事件分发机制

Posted zhangke3016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入聊聊Android事件分发机制相关的知识,希望对你有一定的参考价值。

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

一、事件分发机制介绍

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

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

当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)
onInterceptTouchEventfalsetrue
onTouchEventfalsefalsefalse

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

情况三、B onInterceptTouchEventonTouchEvent方法返回true

事件种类A(ViewGroup)B(ViewGroup)C(View)
onInterceptTouchEventfalsetrue
onTouchEventfalsetruefalse

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

情况四、C onTouchEvent方法返回true

事件种类A(ViewGroup)B(ViewGroup)C(View)
onInterceptTouchEventfalsefalse
onTouchEventfalsefalsetrue

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

情况五、A onInterceptTouchEvent方法返回true

事件种类A(ViewGroup)B(ViewGroup)C(View)
onInterceptTouchEventtruefalse
onTouchEventfalsefalsefalse

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

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

测试Demo下载

以上是关于深入聊聊Android事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android事件分发机制

Android事件分发机制理解

浅谈Android 事件分发机制

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

Android View深入解析事件分发机制