Android事件分发机制理解

前言:

       最近花了一周的时间看了Android事件分发原理方面的知识,我就把自己所学到的和自己的理解整理出来,如果有理解不当的地方希望有朋友指出来,公共成长。Android事件指的Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象。

一.Android事件分发的核心方法。

1.disPatchTouchEvent方法

用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的OnTouchEvent和下级View的dispatchTouchEvent方法的影响表示是否消耗当前事件。

2.onInterceptTouchEvent方法

此方法表示的是否拦击当前事件,如果当前的view一旦拦截了事件,在同一个事件序列中,该方法不会被再次调用。

3.onTouchEvent方法

该方法用来处理点击事件,在dispatchTouchEvent方法中被调用。返回的结果表示是否消耗当前事件,如果不消耗,当前view将无法再接收到当前事件

二.事件分发的顺序

事件传递的顺序:Activity->ViewGroup->view

1.Activity事件传递源码解析

当一个点击事件触发,一定会调用dispatchTouchEvent方法

/**
  * 源码分析:Activity.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) {
            // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {

                onUserInteraction();
                // ->>分析1

            }

            // ->>分析2
            if (getWindow().superDispatchTouchEvent(ev)) {

                return true;
                // 若getWindow().superDispatchTouchEvent(ev)的返回true
                // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
                // 否则:继续往下调用Activity.onTouchEvent

            }
            // ->>分析4
            return onTouchEvent(ev);
        }


/**
  * 分析1:onUserInteraction()
  * 作用:实现屏保功能
  * 注:
  *    a. 该方法为空方法
  *    b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
  */
      public void onUserInteraction() { 

      }
      // 回到最初的调用原处

/**
  * 分析2:getWindow().superDispatchTouchEvent(ev)
  * 说明:
  *     a. getWindow() = 获取Window类的对象
  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象
  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
  */
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {

        return mDecor.superDispatchTouchEvent(event);
        // mDecor = 顶层View(DecorView)的实例对象
        // ->> 分析3
    }

/**
  * 分析3:mDecor.superDispatchTouchEvent(event)
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup 所以将事件传递到了ViewGroup
  */
    public boolean superDispatchTouchEvent(MotionEvent event) {

        return super.dispatchTouchEvent(event);
        // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
        // 即 将事件传递到ViewGroup去处理,详细请看ViewGroup的事件分发机制

    }
    // 回到最初的调用原处

/**
  * 分析4:Activity.onTouchEvent()
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
  public boolean onTouchEvent(MotionEvent event) {

        // 当一个点击事件未被Activity下任何一个View接收 / 处理时
        // 应用场景:处理发生在Window边界外的触摸事件
        // ->> 分析5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        
        return false;
        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
    }

/**
  * 分析5:mWindow.shouldCloseOnTouch(this, event)
  */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    // 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
            && isOutOfBounds(context, event) && peekDecorView() != null) {
        return true;
    }
    return false;
    // 返回true:说明事件在边界外,即 消费事件
    // 返回false:未消费(默认)
}
// 回到分析4调用原处

总结:事件分发首页会传递到Activity的dispatchTouchEvent上,然后会根据getWindow.superDispatchTouchEvent()是否返回true,返回true代表消费了事件,然后调用Activity的TouchEvent()方法。getWindow.superDispatchTouchEvent()是调用window的DispatchTouchEvent,应为window类是一个抽象类,DispatchTouchEvent也是window的抽象方法,所以我们必须找到window的实现类才行,window的实现类只有一个就是PhoneWindow。通过源码我们知道PhoneWindow将事件传递给了DecorView,我们可以通过(ViewGroup)getWindow.getDecorView().findViewById(android.R.id.content)).getAt()(这个获取的其实就是我们setContentView()中的view)获取当前的view,而DecorView是继承自frameLayout其父类也是View,所以最终还是会传递到setContentView中的view里面去,这个view一般都是顶级view也是根view,顶级view一般都是ViewGroup。所以事件就由Activity传递到了ViewGroup。


2.ViewGroup事件传递源码解析

      上面部分我详细阐述了Activity事件分发的过程,这里我们介绍ViewGroup的事件分发。当事件到达顶级View(ViewGroup)后首页调用的也是dispatchTouchEvent方法,然后会调用View的OnInterceptTouchEvent如果返回true,这代表拦截事件,事件就交给了ViewGroup处理,否则事件交到下级View处理。在ViewGroup拦截事件过程中,如果ViewGroup的mOntouchListener被是设置了将调用ViewGroup的OnTouch,否则才会调用View的OnTouchEvent方法。这里说明OnTOuch的优先级要比OntouchEvent方法优先级高。在整个以下的层级View中都会重复上面的传递过程,直到整个事件传递完成。

  // Check for interception.这里我截取的是部分代码,因为ViewGroup的DispatchTouchEvent方法很长。从下面代码可以看出是否拦截出现了两种情况
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
//1.mFirstTouchTarget在第一步就决定来了是否拦截,全篇搜索发现他是在当ViewGroup的子view成功处理时被赋值,
并指向子view,换句话意思就是ViewGroup不拦截事件交给子View处理mFirstTouchTarget 就不会为null,
反过来如果ViewGroup拦截了事件就会为null,所以就不会再掉用onInterceptTouchEvent(ev)

  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

    这里还要着重说下FLAG__DISALLOW_INTERCEPT,可以通过requestDisallowInterceptTouchEvent方法设置,一般用于子view中,一旦设置成功后ViewGroup将无法拦截除了Action_Down以外的其他事件。在滑动事件冲突解决中我们可以在子view中调requestDisallowInterceptTouchEvent方法,让ViewGroup不在拦截事件。最下面额View的滑动冲突中我还会详细介绍。

从上面源码可以看出,ViewGroup一旦拦截了事件,将不会在调用OnInterceptTouchEvent方法。


3.View事件传递源码解析

        我们也是直接看view的dispatchTouchEvent方法。

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
	//这里可以看出虽然view处于不可点击的状态,但是他还是会消耗事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }

从代码可以看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为True,那么他就会消耗事件,onTouchEvent就返回true。然后在ACTION_UP生成时就会调用performClick方法,这里就出现了我们平时给一个控件设置OnClickListener事件,那么performClick方法内部就会调用onClick方法,代码如下所示:

 public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);//这里就是我们传的mOnClickListener然后出发的点击事件
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }


三.View的滑动冲突处理

我们开发中一般会遇到的滑动冲突有三种:

1.ViewPage+fragment方式的上下,和左右的冲突

2.ScllowView+内部放一个RecycleView,内外上下的滑动冲突

3.上面一二的结合,比如我们的商品详情页

一.滑动冲突的处理规则

1.根据滑动的线与水平之间的夹角

2.根据滑动水平方向与垂直方向的距离差

3.根据滑动水平方向与垂直方向的速度差来判断

二.滑动冲突的解决方式

1.外部拦截法

通过上面的原理分析我们知道我们可以在dispatchTouchEvent的时候不分发事件或者onInterceptTouchEvent时候拦截事件,实际上onInterceptTouchEvent方法是一个空方法,是android专门提供给我们处理touch事件拦截的方法,所以这里我们在onInterceptTouchEvent方法中拦截touch事件。

具体做法就是当你不想把事件传递给子控件的时候在onInterceptTouchEvent方法中返回true即可拦截事件,这时候子控件将不会再接收到这一次的touch事件流(所谓touch事件流是以ACTION_DOWN开始,中间包含若干个ACTION_MOVE,以ACTION_UP结束的一连串事件)。伪代码如下:

@Override  
   public boolean onInterceptTouchEvent(MotionEvent ev) {  
       boolean intercepted = false;  
       int x = (int) ev.getX();  
       int y = (int) ev.getY();  
       switch (ev.getAction()) {  
           case MotionEvent.ACTION_DOWN:  
               //down事件不拦截,否则无法传给子元素  
               intercepted = false;  
               if (!mScroller.isFinished()) {  
                   mScroller.abortAnimation();  
                   intercepted = true;  
               }  
               break;  
           case MotionEvent.ACTION_MOVE:  
               int deltaX = x - mLastXIntercept;  
               int deltaY = y - mLastYIntercept;  
               //水平滑动则拦截  
               if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {  
                   intercepted = true;  
               } else {  
                   intercepted = false;  
               }  
               break;  
           case MotionEvent.ACTION_UP:  
               //不拦截,否则子元素无法收到  
               intercepted = false;  
               break;  
       }  
       //因为当ViewGroup中的子View可能消耗了down事件,在onTouchEvent无法获取,  
       // 无法对mLastX赋初值,所以在这里赋值一次  
       mLastX = x;  
       mLastY = y;  
       mLastYIntercept = y;  
       mLastXIntercept = x;  
       return intercepted;  
   }  
在down事件不需要拦截,返回false,否则的话子view无法收到事件,将全部会由父容器处理,这不是希望的;up事件也要返回false,否则最后子view收不到

看看move事件,当水平滑动距离大于竖直距离时,代表水平滑动,返回true,由父类来进行处理,否则交由子view处理。这里move事件就是主要的拦截条件判断,如果你遇到的不是水平和竖直的条件这么简单,就可以在这里进行改变,比如,ScrollView嵌套了RecycleView,条件就变成,当RecycleView滑动到底部或顶部时,返回true,交由父类滑动处理,否则自身RecycleView动。

在onTouchEvent中主要是做的滑动切换的处理

@Override  
    public boolean onTouchEvent(MotionEvent event) {  
        mVelocityTracker.addMovement(event);  
        int x = (int) event.getX();  
        int y = (int) event.getY();  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                if (!mScroller.isFinished()) {  
                    mScroller.abortAnimation();  
                }  
                break;  
            case MotionEvent.ACTION_MOVE:  
                int deltaX = x - mLastX;  
                int deltaY = y - mLastY;  
                if (getScrollX() < 0) {  
                    scrollTo(0, 0);  
                }  
                scrollBy(-deltaX, 0);  
                break;  
            case MotionEvent.ACTION_UP:  
                int scrollX = getScrollX();  
                mVelocityTracker.computeCurrentVelocity(1000);  
                float xVelocityTracker = mVelocityTracker.getXVelocity();  
                if (Math.abs(xVelocityTracker) > 50) {//速度大于50则滑动到下一个  
                    mChildIndex = xVelocityTracker > 0 ? mChildIndex - 1 : mChildIndex + 1;  
                } else {  
                    mChildIndex = (scrollX + mChildWith / 2) / mChildWith;  
                }  
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));  
                int dx = mChildIndex * mChildWith - scrollX;  
                smoothScrollBy(dx, 0);  
                mVelocityTracker.clear();  
                break;  
        }  
        mLastY = y;  
        mLastX = x;  
        return true;  
    }  
在这个嵌套一个普通的ListView,这样就可以解决水平和竖直滑动冲突的问题了。

2.内部拦截法

内部拦截法是父容器不拦截任何事件,所有事件都传递给子view,如果需要就直接消耗掉,不需要再传给父容器处理

下面重写一个ListView,只需要重写一个dispatchTouchEvent方法就OK

public class ListViewEx extends ListView {  
  
    private static final String TAG = "lzy";  
    private int mLastX;  
    private int mLastY;  
  
    public ListViewEx(Context context) {  
        super(context);  
    }  
  
    public ListViewEx(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) {  
        super(context, attrs, defStyleAttr);  
    }  
  
  
    @Override  
    public boolean dispatchTouchEvent(MotionEvent ev) {  
        int x = (int) ev.getX();  
        int y = (int) ev.getY();  
  
        switch (ev.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                //子View的所有父ViewGroup都会跳过onInterceptTouchEvent的回调  
                getParent().requestDisallowInterceptTouchEvent(true);  
                break;  
            case MotionEvent.ACTION_MOVE:  
                int deltaX = x - mLastX;  
                int deltaY = y - mLastY;  
                if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑动,使得父类可以执行onInterceptTouchEvent  
                    getParent().requestDisallowInterceptTouchEvent(false);  
                }  
                break;  
        }  
        mLastX = x;  
        mLastY = y;  
        return super.dispatchTouchEvent(ev);  
    }  
}  

在down事件调用getParent().requestDisallowInterceptTouchEvent(true),这句代码的意思是使这个view的父容器都会跳过onInterceptTouchEvent,在move中判断如果是水平滑动就由父容器去处理,父容器只需要把之前的onInterceptTouchEvent改为下面那样,其他不变

@Override  
    public boolean onInterceptTouchEvent(MotionEvent ev) {  
        int x = (int) ev.getX();  
        int y = (int) ev.getY();  
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
            mLastX = x;  
            mLastY = y;  
            if (!mScroller.isFinished()) {  
                mScroller.abortAnimation();  
                return true;  
            }  
            return false;  
        } else {  
            //如果是非down事件,说明子View并没有拦截父类的onInterceptTouchEvent  
            //说明该事件交由父类处理,所以不需要再传递给子类,返回true  
            return true;  
        }  
    }  

推荐使用外部拦截法,因为内部拦截法比较复杂,更难操作。

四.总结

事件分发全部过程已经总结完了,关于事件分发在原理的基础上去理解。在此特别感谢任玉刚的安卓开发艺术,还有 Carson_Ho点击打开链接 。


猜你喜欢

转载自blog.csdn.net/fdadala/article/details/79636045
今日推荐