欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android事件分发机制理解

程序员文章站 2022-05-14 15:27:09
...

前言:

       最近花了一周的时间看了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方法。

Android事件分发机制理解

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;
    }
Android事件分发机制理解


三.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点击打开链接 。


相关标签: 事件分发