Android自定义View

Android事件分发机制详解:有这一篇就够了

2020-09-11  本文已影响0人  Vurtex

前言


目录

目录

1. 基础认知

1.1 事件分发的对象是谁?

答:事件

即当一个MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理,

1.2 事件分发的本质

答:将点击事件(MotionEvent)向某个View进行传递并最终得到处理

即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。

1.3 事件在哪些对象之间进行传递?

答:Activity、ViewGroup、View

一个点击事件产生后,传递顺序是:Activity(Window) -> ViewGroup -> View

1.4 事件分发过程由哪些方法协作完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()

事件分发相关方法

下文会对这3个方法进行详细介绍

1.5 总结

经过上述3个问题,相信大家已经对Android的事件分发有了感性的认知,接下来,我将详细介绍Android事件分发机制。


2. 事件分发机制方法&流程介绍

方法详细介绍 事件分发机制详细流程

其中:

接下来,我将详细介绍这3个方法及相关流程。

2.1 dispatchTouchEvent()

属性 介绍
使用对象 Activity、ViewGroup、View
作用 分发点击事件
调用时刻 当点击事件能够传递给当前View时,该方法就会被调用
返回结果 是否消费当前事件,详细情况如下:

1. 默认情况:根据当前对象的不同而返回方法不同

对象 返回方法 备注
Activity super.dispatchTouchEvent() 即调用父类ViewGroup的dispatchTouchEvent()
ViewGroup onIntercepTouchEvent() 即调用自身的onIntercepTouchEvent()
View onTouchEvent() 即调用自身的onTouchEvent()
流程解析

2. 返回true

流程图

3. 返回false

2.2 onTouchEvent()

属性 介绍
使用对象 Activity、ViewGroup、View
作用 处理点击事件
调用时刻 在dispatchTouchEvent()内部调用
返回结果 是否消费(处理)当前事件,详细情况如下:

与dispatchTouchEvent()类似

1. 返回true

2. 返回false(同默认实现:调用父类onTouchEvent())

2.3 onInterceptTouchEvent()

属性 介绍
使用对象 ViewGroup(注:Activity、View都没该方法)
作用 拦截事件,即自己处理该事件
调用时刻 在ViewGroup的dispatchTouchEvent()内部调用
返回结果 是否拦截当前事件,详细情况如下:
返回结果 流程图

2.4 三者关系

下面将用一段伪代码来阐述上述三个方法的关系和点击事件传递规则

// 点击事件产生后,会直接调用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;
   }

2.5 总结

3. 事件分发场景介绍

下面我将利用例子来说明常见的点击事件传递情况

3.1 背景描述

我们将要讨论的布局层次如下:


布局层次

假设用户首先触摸到屏幕上View C上的某个点(如图中黄色区域),那么Action_DOWN事件就在该点产生,然后用户移动手指并最后离开屏幕。

3.2 一般的事件传递情况

一般的事件传递场景有:

3.2.1 默认情况

流程图

注:虽然ViewGroup B的onInterceptTouchEvent方法对DOWN事件返回了false,后续的事件(MOVE、UP)依然会传递给它的onInterceptTouchEvent()

这一点与onTouchEvent的行为是不一样的。

3.2.2 处理事件

假设View C希望处理这个点击事件,即C被设置成可点击的(Clickable)或者覆写了C的onTouchEvent方法返回true。

最常见的:设置Button按钮来响应点击事件

事件传递情况:(如下图)

流程图

3.2.3 拦截DOWN事件

假设ViewGroup B希望处理这个点击事件,即B覆写了onInterceptTouchEvent()返回true、onTouchEvent()返回true。
事件传递情况:(如下图)

流程图

3.2.4 拦截DOWN的后续事件

假设ViewGroup B没有拦截DOWN事件(还是View C来处理DOWN事件),但它拦截了接下来的MOVE事件。

特别注意:

3.3 总结


4. Android事件分发机制源码分析

事件分发机制详细流程

其中:

所以,要想充分理解Android分发机制,本质上是要理解:

接下来,我将通过源码分析详细介绍Activity、View和ViewGroup的事件分发机制


4.1 Activity的事件分发机制

4.1.1 源码分析

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键等都会触发此方法
所以onUserInteraction()主要用于屏保
关注点3

Window类是抽象类,且PhoneWindow是Window类的唯一实现类
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)
再回到最初的代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //关注点2
            onUserInteraction();
        }
        //关注点3
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

由于一般事件列开始都是DOWN,所以这里返回true,基本上都会进入getWindow().superDispatchTouchEvent(ev)的判断

4.1.2 汇总

当一个点击事件发生时,调用顺序如下

  1. 事件最先传到Activity的dispatchTouchEvent()进行事件分发
  2. 调用Window类实现类PhoneWindow的superDispatchTouchEvent()
  3. 调用DecorView的superDispatchTouchEvent()
  4. 最终调用DecorView父类的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

4.1.3 结论

4.1.4 疑问

那么,ViewGroup的dispatchTouchEvent()什么时候返回true,什么时候返回false?

答:请继续往下看 ViewGroup事件的分发机制


4.2 ViewGroup事件的分发机制

在讲解ViewGroup事件的分发机制之前我们先来看个Demo

4.2.1 Demo讲解

布局如下:

布局层次

结果测试
只点击Button

只点击Button

再点击空白处


点击空白处

从上面的测试结果发现:

接下来,我们开始进行ViewGroup事件分发的源码分析

4.2.2 源码分析

ViewGroup的dispatchTouchEvent()源码分析

  1. 详情请看注释
  2. Android 5.0后ViewGroup的dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;
  3. 本文为了让读者更好理解dispatchTouchEvent()源码分析,所以采用Android 5.0前的版本
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;  
} 

关注点2

当点击了某个控件:

  1. 调用该控件所在布局(ViewGroup)的dispatchTouchEvent()
  2. 在布局的dispatchTouchEvent()中找到被点击的相应控件
  3. 再去调用该控件的dispatchTouchEvent()
    1. 实现了点击事件从ViewGroup到View的传递
    2. 此处是关于View.dispatchTouchEvent()的分析,详情请看下面的View事件分发机制。

结论


4.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);  
}  

从上面可以看出:

只有以下三个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent(event)方法
第一个条件:mOnTouchListener != null;
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED;
第三个条件:mOnTouchListener.onTouch(this, event);

第一个条件:mOnTouchListener!= null
//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) { 
//即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
    mOnTouchListener = l;  
}  
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED
第三个条件:mOnTouchListener.onTouch(this, event)
//手动调用设置
button.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        return false;  
    }  
});
接下来,我们继续看:onTouchEvent(event)的源码分析

1.详情请看注释
2.Android 5.0后View的onTouchEvent()的源码发生了变化(更加复杂),但原理相同;
3.本文为了让读者更好理解onTouchEvent()源码分析,所以采用Android 5.0前的版本

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;  
}  
public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
} 

结论

  1. onTouch()的执行高于onClick()
  2. 每当控件被点击时:

下面我将用Demo验证上述的结论

Demo论证

1. Demo1:在回调onTouch()里返回true

Demo流程
//设置OnTouchListener()
   button.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("执行了onTouch(), 动作是:" + event.getAction());

                return true;
            }
        });

//设置OnClickListener
    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("执行了onClick()");
            }
        });

点击Button,测试结果如下:


测试结果

2. Demo2:在回调onTouch()里返回false

Demo流程
//设置OnTouchListener()
   button.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("执行了onTouch(), 动作是:" + event.getAction());

                return false;
            }
        });

//设置OnClickListener
    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("执行了onClick()");
            }
        });

点击Button,测试结果如下:

测试结果

总结:onTouch()返回true就认为该事件被onTouch()消费掉,因而不会再继续向下传递,即不会执行OnClick()。

如果你看到此处,那么恭喜你,你已经能非常熟悉掌握Android的事件分发机制了(Activity、ViewGroup、View的事件分发机制)


5. 思考点

5.1 onTouch()和onTouchEvent()的区别

//&&为短路与,即如果前面条件为false,将不再往下执行
//所以,onTouch能够得到执行需要两个前提条件:
//1\. mOnTouchListener的值不能为空
//2\. 当前点击的控件必须是enable的。
mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)

5.2 Touch事件的后续事件(MOVE、UP)层级传递

从上面对事件分发机制分析知:

请记住:接收了ACTION_DOWN事件的函数不一定能收到后续事件(ACTION_MOVE、ACTION_UP)

这里给出ACTION_MOVE和ACTION_UP事件的传递结论

流程讲解 流程讲解
上一篇 下一篇

猜你喜欢

热点阅读