四、界面编程(三) View的事件分发机制

事件分发机制是View的一个核心知识点,而且也是一个难点。本文将详细介绍和总结事件分发机制的知识,以便更好更深入的理解。

1.基础知识

1.1 事件分发的对象

  事件分发的对象实际上就是事件,即MotionEvent对象(当用户触摸屏幕时,将产生点击事件(Touch事件),Touch事件的相关细节(包括发生触摸的位置、时间、历史记录和手势动作等)被封装为MotionEvent对象)

主要发生的Touch事件有如下四种:

MotionEvent.ACTION_DOWN :按下View(所有事件的开始)
MotionEvent.ACTION_UP :抬起View(与DOWN事件对应)
MotionEvent.ACTION_MOVE :滑动View
MotionEvent.ACTION_CANCEL :非人为原因结束本次事件

事件列:从手指接触屏幕到手指离开屏幕,这个过程中产生的一系列事件
任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件,如下图:
这里写图片描述

1.2 事件分发的本质

  将点击事件(MotionEvent)向某个View进行传递并最终得到处理,也就是说,当一个点击事件发生之后,系统需要将这个点击事件传递给一个具体的View去处理,这个事件传递的过程就是分发过程。

1.3 事件传递规则

  当一个事件产生之后,传递的顺序是:Activity(Window) –> ViewGroup –> View

2.事件分发机制的流程介绍

2.1概念浅析(场景举例)

  对于事件分发机制,其实可以用一个生活中很常见的场景来对比理解:假设在你的公司,有一个总经理,级别最高,他下面有一个部长,级别次之;最底层就是干活的你,没有级别。现在董事长交给总经理一项任务,总经理将这项任务布置给部长,部长又把任务安排给了你。而当你好不容易干完活了,你就把任务交给部长,部长觉得任务完成的不错,于是就签了他的名字交给总经理,总经理看了也觉得不错,就签了名交给董事会。这样,一个任务就顺利完成了。

2.2流程详解

点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent(),onInterceptTouchEvent()和onTouchEvent()
这里写图片描述

这三个方法的关系可以用如下一段伪代码来表示:

// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {

   //代表是否消耗事件
    boolean consume = false;


   if (onInterceptTouchEvent(ev)) {
      //如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
      //则该点击事件则会交给当前View进行处理
      //即调用onTouchEvent ()方法去处理点击事件
       consume = onTouchEvent (ev) ;
   } else {
     //如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
     //则该点击事件则会继续传递给它的子元素
     //子元素的dispatchTouchEvent()就会被调用,重复上述过程
     //直到点击事件被最终处理为止
     consume = child.dispatchTouchEvent (ev) ;
   }
return consume;
}

  上述伪代码已经将三者的关系表现得淋漓尽致,通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法会被调用,如此反复直到事件被最终处理。
  当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouchListener中的onTouch方法会被回调。这是事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,俺么onTouchEvent方法将不会被调用。由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法就会被调用。可以看出,平时我们用的onClickListener,其优先级最低,即处于事件传递的尾端。

2.3 传递机制的结论

(1)某个View一旦决定拦截事件,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给他的话),并且它的onInterceptTouchEvent不会再被调用。也就是说,当一个View决定拦截一个事件后,那么系统会把同一个事件序列中的其他事件都交给他来处理,因此就不需要再调用这个View的onInterceptTouchEvent去询问它是否要拦截了
(2)正常情况下,一个事件序列只能被一个View拦截并消耗,这条的原因可以参考上一条,因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个时间序列中的事件不能分别友两个View同时处理,但是通过特殊手段也可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
(3)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
(4)如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并而且当前View可以持续收到后续的事件,最终这些消失的点击事件会传(5)ViewGroup默认不拦截任何事件,Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false
(6)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
(7)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false
(8)View的enable属性不影响onTouchEvent的默认返回值。哪怕是一个View是disable状态,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
(9)onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件
(10)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再有父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
递给Activity处理

3.事件分发的源码解析

3.1Activity的事件分发

  当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent进行事件分发,实际上是有Activity的Window来完成的。
Activity的dispatchTouchEvent源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
   //关注点1
   //一般事件列开始都是DOWN,所以这里基本是true
   if (ev.getAction() == MotionEvent.ACTION_DOWN) {
       //关注点2
       onUserInteraction();
   }
   //关注点3
   if (getWindow().superDispatchTouchEvent(ev)) {
       return true;
    }
    return onTouchEvent(ev);
}

关注点1:一般事件序列都是DOWN开始的,所以这里返回为true,执行onUserInteraction( );
关注点2:看一下onUserInteraction()的源码:

/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}

   源码中的注释很长,但是代码实际上只有一行,从源码中我们可以看出,此方法为空方法,主要作用是用于屏保。代码注释中也可以知道,当此activity位于栈顶的时候,屏幕点击home、back、menu等键都会触发此方法。

关注点3:Window类是一个抽象类,PhoneWindow是它的唯一实现类;superDispatchTouchEvent(ev)是抽象方法,返回的window对象;
通过PhoneWindow类看一下superDispatchTouchEvent的作用

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
   //mDecor是DecorView的实例//DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}

接下来看一下mDecor.superDispatchTouchEvent(event)的源码

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
    //DecorView继承自FrameLayout//那么它的父类就是ViewGroup
    而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()
}

   通过源码以及源码中的增加的注释,我们可以知道,执行getWindow( ).superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event)

  结合最初的代码可以知道,一般事件序列都是从DOWN开始,所以基本都会执行getWindow( ).superDispatchTouchEvent(ev)的判断,也就是说,执行Activity.dispatchTouchEvent(ev)实际上就是执行了ViewGroup.dispatchTouchEvent(event),通过这种方式,事件就从Activity传递到了ViewGroup中

总结:
当一个点击事件发生时,调用顺序如下:
(1)事件最先传到Activity的dispatchTouchEvent进行事件分发
(2)调用Window类的实现类PhoneWindow中的superDispatchTouchEvent方法
(3)调用DecorView的superDispatchTouchEvent方法
(4)最终会调用DecorView父类的dispatchTouchEvent,也就是ViewGroup的dispatchTouchEvent方法

3.2 ViewGroup的事件分发机制

ViewGroup的dispatchTouchEvent()源码分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                mMotionTarget = null;
            }

            //看这个If判断语句//第一个判断值disallowIntercept:是否禁用事件拦截的功能(默认是false)
            // 可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。
            // 第二个判断值: !onInterceptTouchEvent(ev):对onInterceptTouchEvent()返回值取反
            //如果我们在onInterceptTouchEvent()中返回false,就会让第二个值为true,从而进入到条件判断的内部
            // 如果我们在onInterceptTouchEvent()中返回true,就会让第二个值为false,从而跳出了这个条件判断。
            // 关于onInterceptTouchEvent()请看下面分析(关注点1)
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                ev.setAction(MotionEvent.ACTION_DOWN);
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;

                //通过for循环,遍历了当前ViewGroup下的所有子View
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);

                        //判断当前遍历的View是不是正在点击的View
                        //如果是,则进入条件判断内部
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

                            //关注点2

                            //条件判断的内部调用了该View的dispatchTouchEvent()方法(具体请看下面的View事件分发机制)
                            //实现了点击事件从ViewGroup到View的传递
                            if (child.dispatchTouchEvent(ev)) {
                                //调用子View的dispatchTouchEvent后是有返回值的
                                //如果这个控件是可点击的话,那么点击该控件时,dispatchTouchEvent的返回值必定是true
                                //因此会导致条件判断成立
                                mMotionTarget = child;
                                //于是给ViewGroup的dispatchTouchEvent方法直接返回了true,这样就导致后面的代码无法执行,直接跳出
                                //即把ViewGroup的touch事件拦截掉
                                return true;
                            }
                        }
                    }
                }
            }
        }
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
        if (isUpOrCancel) {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        final View target = mMotionTarget;

        //关注点3
        // 没有任何View接收事件的情况,即点击空白处情况
        if (target == null) {
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            //调用ViewGroup的父类View的dispatchTouchEvent()
            // 因此会执行ViewGroup的onTouch()、onTouchEvent()
            // 实现了点击事件从ViewGroup到View的传递
            return super.dispatchTouchEvent(ev);
        }


        //之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
            }
            mMotionTarget = null;
            return true;
        }
        if (isUpOrCancel) {
            mMotionTarget = null;
        }
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }
        return target.dispatchTouchEvent(ev);
    }

关注点1:(onInterceptTouchEvent源码分析)
ViewGroup每次在做事件分发的时候,需要调用onInterceptTouchEvent判断是否拦截事件,源码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
     return false;
}

返回false 表示不拦截,默认为false,允许事件继续向子View传递
返回true 表示拦截,需要手动设置,即自己处理该事件,执行自己的onTouchEvent,事件不会继续向下传递

关注点2:
当点击了某个控件,调用过程如下:
(1)调用该View所在布局(ViewGroup)的dispatchTouchEvent
(2)在布局的dispatchTouchEvent中找到被点击的View
(3)再去调用该View的dispatchTouchEvent

这个过程实现了点击事件从ViewGroup到View的传递

总结:
  Android的事件分发机制是先传递到ViewGroup,再由ViewGroup传递到View。
  在ViewGroup中通过onInterceptTouchEvent对事件传递进行拦截,onInterceptTouchEvent方法返回true表示拦截事件,即不允许事件继续向子View传递;onInterceptTouchEvent方法返回false表示不拦截事件,即允许事件继续向子View传递,默认是返回false的;子View如果将传递的事件消费掉,ViewGroup中将无法接收任何事件。

3.3 View的事件分发机制

View的dispatchTouchEvent源码分析:

    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

代码中可以看出需要满足三个判断条件才会返回true,否则都是执行onTouchEvent(event)
第一个条件:mOnTouchEventListener == null

//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) {
   //即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
   mOnTouchListener = l;
}

第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED
这个条件是判断当前点击的空间是否enable,由于大部分View默认都是enable的,因此该条件恒定为true

第三个条件:mOnTouchListener.onTouch(this, event)
这个条件是设置回调控件的onTouch方法

//手动调用设置
button.setOnTouchListener(new OnTouchListener() {
   @Override
   public boolean onTouch(View v, MotionEvent event) {
   return false;
   }
});

  如果在onTouch方法中返回了true,就会让上述三个条件全部成立,从而整个方法都会返回true;如果onTouch方法里面返回了false,就会去执行onTouchEvent方法。

接下来继续看onTouchEvent方法的源码:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
         //如果该控件是可以点击的就会进入到下两行的switch判断中去;
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            //如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    // 在经过种种判断之后,会执行到关注点1的performClick()方法。
                    //请往下看关注点1
                    if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
                            // 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)) {
                                    //关注点1
                                    //请往下看performClick()的源码分析
                                    performClick();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;
                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;
                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;
                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();
                     // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            //如果该控件是可以点击的,就一定会返回true
            return true;
        }
        //如果该控件是不可以点击的,就一定会返回false
        return false;
    }

关注点1:
performClick()的源码分析:

    public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }
        return false;
    }

只要mOnClickListener不为null,就回去调用onClick方法,mOnClickListener的赋值见下面的方法

    public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        mOnClickListener = l;
    }

   也就是所,当我们通过setOnClickListener方法来给控件注册一个点击事件的时候,就会给mOnclickListener赋值,也就会回调onClick方法

总结:
(1)onTouch的执行优先级高于onClick方法
(2)每当控件被点击时,如果回调onTouch里返回了false,就会让dispatchTouchEvent方法返回false,那么就会执行onTouchEvent方法;如果回调onTouch返回了true,就会让dispatchTouchEvent方法里返回true,那么将不会执行onTouchEvent

onTouch()返回false(该事件没被onTouch()消费掉) = dispatchTouchEvent()返回false(继续向下传递) = 执行onTouchEvent() = 执行OnClick()

onTouch()返回true(该事件被onTouch()消费掉) = dispatchTouchEvent()返回true(不会再继续向下传递) = 不会执行onTouchEvent() = 不会执行OnClick()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值