讲讲Android事件拦截机制

简介

什么是触摸事件?顾名思义,触摸事件就是捕获触摸屏幕后产生的事件。当点击一个按钮时,通常会产生两个或者三个事件——按钮按下,这是事件一,如果滑动几下,这是事件二,当手抬起,这是事件三。所以在Android中特意为触摸事件封装了一个类MotionEvent,如果重写onTouchEvent()方法,就会发现该方法的参数就是这样的一个MotionEvent,在一般重写触摸相关的方法中,参数一般都含有MotionEvent,可见它的重要性。

那么MotionEvent到底是什么东东呢,它包含了几种类型。

  • Action_Down:手指刚接触屏幕
  • Action_Move:手指在屏幕上移动
  • Action_Up:手指从屏幕上松开的一瞬间

在正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:

  • 点击屏幕后离开松开,事件序列为Down->Up
  • 点击屏幕滑动一会再松开,事件序列为Down->Move->......>Move->Up

那么,在MotionEvent里面封装了不少好东西,比如触摸点的坐标,可以通过event.getX()方法和event.getRawX(),这两者区别也很简单,getX()返回的是相对于当前View左上角的x坐标,getRawY()返回是相对于手机屏幕左上角的x坐标,同理,y坐标也是可以获取的,getY()和getRawY()方法,MotionEvent获得点击事件的类型,可以通过不同的Action来进行区分,并实现不同的逻辑。

例子

如此看来,触摸事件还是简单的,其实就是一个动作类型加坐标而已。但是我们知道,Android的View结构是树形结构,也就是说,View可以放在ViewGroup里面,通过不同的组合来实现不同的样式,那么如果View放在ViewGroup里面,这个ViewGroup又嵌套在另一个ViewGroup里面,甚至还有可能继续嵌套,一层层的叠加起来呢,我们先看一个例子,是通过一个按钮点击的。

XML文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/mylayout">
    <Button
        android:id="@+id/my_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="click test"/>
</LinearLayout>

Activity文件

public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
    private LinearLayout mLayout;
    private Button mButton;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        mLayout = (LinearLayout) this.findViewById(R.id.mylayout);
        mButton = (Button) this.findViewById(R.id.my_btn);

        mLayout.setOnTouchListener(this);
        mButton.setOnTouchListener(this);

        mLayout.setOnClickListener(this);
        mButton.setOnClickListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
        return false;
    }

    @Override
    public void onClick(View v) {
        Log.i(null, "OnClickListener--onClick--"+v);
    }
}

以上代码很简单,Activity中有一个LinearLayout(ViewGroup的子类,ViewGroup是View的子类)布局,布局中包含一个按钮(View的子类),然后分别对这两个控件设置了Touch与Click的监听事件,具体运行结果如下:
1,当稳稳的点击Button时

点击Button

2,当稳稳的点击除过Button以外的其他地方时: 
点击Button其他地方

3,当收指点击Button时按在Button上晃动了一下松开后
点击Button晃动几下

我们看下onTouch和onClick,从参数都能看出来onTouch比onClick强大灵活,毕竟多了一个event参数。这样onTouch里就可以处理ACTION_DOWN、ACTION_UP、ACTION_MOVE等等的各种触摸。现在来分析下上面的打印结果;在1中,当我们点击Button时会先触发onTouch事件(之所以打印action为0,1各一次是因为按下抬起两个触摸动作被触发)然后才触发onClick事件;在2中也同理类似1;在3中会发现onTouch被多次调运后才调运onClick,是因为手指晃动了,所以触发了ACTION_DOWN->ACTION_MOVE…->ACTION_UP。

onTouch会有一个返回值,而且在上面返回了false。你可能会疑惑这个返回值有啥效果?那就验证一下吧,我们将上面的onTouch返回值改为ture。如下:

	@Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
        return true;
    }

显示结果:
onTouch返回True

此时onTouch返回true,则onClick不会被调运了。

好了,经过这个简单的实例验证你可以总结发现:

  1. Android控件的Listener事件触发顺序是先触发onTouch,其次onClick。
  2. 如果控件的onTouch返回true将会阻止事件继续传递,返回false事件会继续传递。

事件流程

看上面的例子是不是有点困惑,为何OnTouch返回True,onClick就不执行,事件传递就中断,在这里需要引进一个场景,这样解释起来就更形象生动。

首先,请想象一下生活中常见的场景:假如你所在的公司,有一个总经理,级别最高,它下面有个部长,级别次之,最底层就是干活的你,没有级别。现在总经理有一个任务,总经理将这个业务布置给部长,部长又把任务安排给你,当你完成这个任务时,就把任务反馈给部长,部长觉得这个任务完成的不错,于是就签了他的名字反馈给总经理,总经理看了也觉得不错,就也签了名字交给董事会,这样,一个任务就顺利完成了。这其实就是一个典型的事件拦截机制。

在这里我们先定义三个类:
一个总经理—MyViewGroupA,最外层的ViewGroup

一个部长—MyViewGroupB,中间的ViewGroup

一个你—MyView,在最底层

根据以上的场景,我们可以绘制以下流程图:
流程图
从图中,我们可以看到在ViewGroup中,比View多了一个方法—onInterceptTouchEvent()方法,这个是干嘛用的呢,是用来进行事件拦截的,如果被拦截,事件就不会往下传递了,不拦截则继续。

如果我们稍微改动下,如果总经理(MyViewGroupA)发现这个任务太简单,觉得自己就可以完成,完全没必要再找下属,因此MyViewGroupA就使用了onInterceptTouchEvent()方法把事件给拦截了,此时流程图:
流程图A
我们可以看到,事件就传递到MyVewGroupA这里就不继续传递下去了,就直接返回。

如果我们再改动下,总经理(MyViewGroupA)委托给部长(MyViewGroupB),部长觉得自己就可以完成,完全没必要再找下属,因此MyViewGroupB就使用了onInterceptTouchEvent()方法把事件给拦截了,此时流程图:
流程图B
我们可以看到,MyViewGroupB拦截后,就不继续传递了,同理如果,到干货的我们上(MyView),也直接返回True的话,事件也是不会继续传递的,如图:
流程图C

源码

分析Android View事件传递机制之前有必要先看下源码的一些关系,如下是几个继承关系图:

源码1

源码2
看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。

1,从View的dispatchTouchEvent方法说起

在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中),所以我们先来看下View的dispatchTouchEvent方法,如下:

  public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

dispatchTouchEvent的代码有点长,但可以挑几个重点讲讲,if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住等,然后定义ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句就是重点,首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到:

/**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

li.mOnTouchListener是不是null取决于控件(View)是否设置setOnTouchListener监听,在上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null,接着通过位与运算确定控件(View)是不是ENABLED 的,默认控件都是ENABLED 的,接着判断onTouch的返回值是不是true。通过如上判断之后如果都为true则设置默认为false的result为true,那么接下来的if (!result && onTouchEvent(event))就不会执行,最终dispatchTouchEvent也会返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句有一个为false则if (!result && onTouchEvent(event))就会执行,如果onTouchEvent(event)返回false则dispatchTouchEvent返回false,否则返回true。

这下再看前面的实例部分明白了吧?控件触摸就会调运dispatchTouchEvent方法,而在dispatchTouchEvent中先执行的是onTouch方法,所以验证了实例结论总结中的onTouch优先于onClick执行道理。如果控件是ENABLE且在onTouch方法里返回了true则dispatchTouchEvent方法也返回true,不会再继续往下执行;反之,onTouch返回false则会继续向下执行onTouchEvent方法,且dispatchTouchEvent的返回值与onTouchEvent返回值相同

2,dispatchTouchEvent总结

在View的触摸屏传递机制中通过分析dispatchTouchEvent方法源码我们会得出如下基本结论:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
  3. 如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
  4. 如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
  5. 如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。

3,onTouchEvent方法

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

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 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;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    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) {
                            // 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();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    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);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

首先地6到14行可以看出,如果控件(View)是disenable状态,同时是可以clickable的则onTouchEvent直接消费事件返回true,反之如果控件(View)是disenable状态,同时是disclickable的则onTouchEvent直接false。多说一句,关于控件的enable或者clickable属性可以通过java或者xml直接设置,每个view都有这些属性。

接着22行可以看见,如果一个控件是enable且disclickable则onTouchEvent直接返回false了;反之,如果一个控件是enable且clickable则继续进入过于一个event的switch判断中,然后最终onTouchEvent都返回了true。switch的ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,接着到手抬起来ACTION_UP时你会发现,首先判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。具体如下:

 public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:

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

控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。

4,onTouchEvent小结

  1. onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听。
  2. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action。

小结

通过以上总结,Android中的事件拦截机制,其实跟我们生活中的上下级委托任务很像,领导可以处理掉,也可以下发给下属员工处理,如果员工处理的好,领导才敢给你下发任务,如果你处理不好,则领导也不敢把任务交给你,这就像在中途把下发的任务的中途拦截掉了。通过流程和源码的分析,相信大家能比较容易了解事件的分发、拦截、处理事件的流程。在弄清楚顺序机制之后,再配合源码看,你会更加深入的理解,为什么流程会是这样的,最先对流程有一个大致的认识之后,再去理解,这样就不会一头雾摸不着头脑,进而会有更大的学习乐趣,毕竟在学习过程中,保持好奇心是很重要的。

阅读扩展

源于对掌握的Android开发基础点进行整理,罗列下已经总结的文章,从中可以看到技术积累的过程。
1,Android系统简介
2,ProGuard代码混淆
3,讲讲Handler+Looper+MessageQueue关系
4,Android图片加载库理解
5,谈谈Android运行时权限理解
6,EventBus初理解
7,Android 常见工具类
8,对于Fragment的一些理解
9,Android 四大组件之 " Activity "
10,Android 四大组件之" Service "
11,Android 四大组件之“ BroadcastReceiver "
12,Android 四大组件之" ContentProvider "
13,讲讲 Android 事件拦截机制
14,Android 动画的理解
15,Android 生命周期和启动模式
16,Android IPC 机制
17,View 的事件体系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 启动过程分析
21,Service 启动过程分析
22,Android 性能优化
23,Android 消息机制
24,Android Bitmap相关
25,Android 线程和线程池
26,Android 中的 Drawable 和动画
27,RecylerView 中的装饰者模式
28,Android 触摸事件机制
29,Android 事件机制应用
30,Cordova 框架的一些理解
31,有关 Android 插件化思考
32,开发人员必备技能——单元测试

posted @ 2016-10-25 17:17  cryAllen  阅读(12770)  评论(0编辑  收藏  举报