美文网首页
Android事件分发机制(View篇)

Android事件分发机制(View篇)

作者: Charay | 来源:发表于2017-10-30 17:26 被阅读637次

    纸上得来终觉浅,看了很多别人写的有关View的事件分发机制的博客,但别人的终究
    是别人的,把自己的理解写下来,才是自己的,但万变不离其宗。本篇将从另外一个
    角度带你理解View的事件分发机制。

    序言

    关于 ViewViewGroup 的事件分发机制 我打算用两篇博客来写,
    本篇主要讲述 View 的事件分发机制,下一篇讲述ViewGroup的事件分发机制

    View 的事件分发和领导派发任务是很相似的,所以我们先通过这个生活中常见的场景引入,以便大家更好的理解View的事件分发

    场景描述:

    当老板有一个问题需要解决的时候,他可以自己直接解决,也可以将问题交给经理去解决,当然老板不可能所有问题都
    要自己解决,那样还要经理和员工干什么,所以老板一般会将任务派发给经理,一般经理也不会自己解决,他会将问题
    派发给员工去解决,员工嘛就是干活的,否则就得走人了,当然不排除有些问题员工没有能力解决那只能将问题返回给
    经理,经理有能力解决就解决了,如果经理也解决不了,那就把这个问题扔回给老板,最后只能由老板自己解决了。
    

    类似地:

    我们的View的事件分发也是这样的,当我们手接触到手机屏幕的时候,屏幕接收到一个Touch事件,系统会将这Touch
    事件封装成一个对象MotionEvent,之后所有的事件处理都将与这个MotionEvent相关,和领导派发任务一样,我们的
    Touch事件会首先到达最外层的ViewGroup(Activity),然后再一层一层地向子View派发,最终会到达最内层的View,
    最内层的View可以处理这个Touch事件,也可以将这个Touch事件扔回给自己的父View
    

    对于View和ViewGroup我们需要关注下面的个方法:

    View 两个方法:

    • dispatchTouchEvent (MotionEvent event)//负责事件分发
    • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

    ViewGroup 三个方法:

    • dispatchTouchEvent (MotionEvent event)//负责事件分发
    • onInterCeptTouchEvent(MotionEvent event)//处理是否拦截当前事件
    • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

    我们注意到上面的方法都参数都是一个 MotionEvent 对象,ViewViewGroup少了一个onInterCeptTouchEvent()方法,这时因为对于View来讲,它是不能有子View的,所以不需要拦截事件的方法

    正文:

    我们先从Android中两种不同类型的控件开始介绍

    • 可被点击的: Button、ImageButton... //即本身CLICKABLE为true
    • 不可被点击的: ImageView、TextView...//即本身CLICKABLE为false

    注意:上面两点很重要,大家一定要记在心里。也可能你会有疑问我们平常用的ImageViewTextView都能点击呀,请注意我说的是本身可被点击的,ImageViewTextView能被点击是因为我们给他们设置了setOnClickListener(),这样就把这两种ViewCLICKABLE置为true了,关于这一点大家可以通过调用这个方法前后添加log日志去验证。

    我们先新建一个project: ViewDemo

    ViewDemo 很简单:在布局文件中添加一个 ImageView 和一个 Button 控件
    MainActivity中初始化并给两个控件都设置setOnTouchListener

    先看布局文件

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            />
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="按钮"/>
    </LinearLayout>
    

    MainActivity中初始化ImageViewButton并设置setOnTouchListener
    onTouch()方法中添加log日志

    MainActivity主要代码

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        mImageView = findViewById(R.id.imageView);
        mButton = findViewById(R.id.button);
    
        mImageView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
                return false;
            }
        });
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
                return false;
            }
        });
    
    }
    

    运行程序依次点击 ImageViewButton

    其中 motionEvent.getAction() 的值代表的含义:

    0:ACTION_DOWN
    2:ACTION_MOVE
    1:ACTION_UP
    

    打印log如下:

    10-30 11:50:06.439 10517-10517/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
    
    10-30 11:50:07.276 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
    10-30 11:50:07.285 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
    10-30 11:50:07.313 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
    

    我们可以看到点击ImageView的时候日志打印了一次,点击Button的时候日志打印了三次
    如果我们把两个onTouch()方法的返回值都返回true即:

    mImageView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
            return true;
        }
    });
    mButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
            return true;
        }
    });
    
    }
    

    分别点击 ImageViewButton 再来看日志:

    10-30 12:55:16.427 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
    10-30 12:55:16.442 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
    10-30 12:55:16.487 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
    
    10-30 12:55:19.514 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
    10-30 12:55:19.533 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
    10-30 12:55:19.574 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
    

    此时可以看到两个控件的日志打印都是三次,DOWNMOVEUP

    一分钟时间思考一下这是为什么?

    下面让我带大家从源码角度分析一下为什么会这样:
    前面我们提到过对于View来讲我们关注两个方法:

    • dispatchTouchEvent (MotionEvent event)//负责事件分发
    • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

    我们需要知道,当一个View接收到Touch事件的时候,首先被调用的是当前ViewdispatchTouchEvent()方法:
    下面我们看一下View源码中的这个方法:dispatchTouchEvent()

    为了便于理解,我省略了部分干扰代码,只保留了有效的部分

    dispatchTouchEvent(MotionEvent event)

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        ......
            
            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;
            }
        ......
    
        return result;
    }
    

    从方法的注释我们可以知道:这个方法是处理事件分发的,如果return true说明当前事件被消费了,便不再继续分发,否则继续分发

    我们主要看方法中的两个if判断:先看第一个

    li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

    && li.mOnTouchListener.onTouch(this, event)

    因为我们给控件设置了onTouchLishener所以mListenerInfo不为空,li != nulltrue,li.mOnTouchListener != null也为true,这个mOnTouchListener就是我们mImageView.setOnTouchListener(new View.OnTouchListener())的时候new出来的,第三个条件主要取决于mViewFlags,在Android系统中所有控件默认都是enable的,除非我们对一个控件设置view.setEnable(false),所以(mViewFlags & ENABLED_MASK) == ENABLED也为true,那么能不能进入到if()内部关键看li.mOnTouchListener.onTouch(this, event),这个方法的返回值就是我们最开始setOnTouchListener中重写的onTouch()方法的返回值,默认为false,后来被我们改成了true

    onTouch()方法中默认false,也就是说此处的第一个if()条件不成立,那么将到第二个if()判断:!resulttrue,那么我们主要看onTouchEvent(event)的返回值,那么这个onTouchEvent(event)有时什么东东呢?我们进入到这个方法中(干扰代码已省略):

    onTouchEvent(MotionEvent event)

     /*
      * @param event The motion event.
      * @return True if the event was handled, false otherwise.
      */
    public boolean onTouchEvent(MotionEvent event) {
       ......
    
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                      ......
                        if (!post(mPerformClick)) {
                             performClick();
                         }
                      ......
                    break;
    
                case MotionEvent.ACTION_DOWN:
    
                    ......
    
                    break;
    
                case MotionEvent.ACTION_CANCEL:
    
                    ......
    
                    break;
    
                case MotionEvent.ACTION_MOVE:
    
                    ......
    
                    break;
            }
    
            return true;
        }
    
        return false;
    }
    

    从方法中我们可以看到这个方法的返回值也是 boolean 类型的,看到这里不知道大家能不能想起我在开头说的Android中两种不同类型,一种是默认可以点击的,一种是默认不可以点击,我们的 ImageViewButton 的可点击与否的作用就在这个方法中的到了体现,让我们来看一下这个 if() 条件判断吧

    先说默认条件下两个Touch方法都返回false

    - ImageView

    如果是 ImageView不用想,默认就是不可点击的,也就是说CLICKABLELONG_CLICKABLE都为false,那么好,这个条件我们是进不去的,再看最后return值是false,也就是说只要if()判断进不去,全都会返回false,回到我们之前的dispatchTouchEvent()中,既然onTouchEvent()也返回了false,那么最终dispatchTouchEvent()的返回值也是false,说明这个TouchDOWN事件没有被消费,既然DOWN事件没有被消费,也就没有后面的MOVEUP,所以最初的ImageView值打印了一行log,

    - Button

    如果是Button默认就是可点击的CLICKABLELONG_CLICKABLE都为true,也就是说if()判断能够进入,那么就说明,里面的DOWNMOVEUP事件都会响应,另外我们再看,进入if()后最后一句: return true;这句返回了true,也就是说,只要进入了这个if()判断,就回返回true,也就是我们的dispatchTouchEvent()中的第二个if()判断会返回true,那么我们的dispatchTouchEvent()最终也就回返回true,也就是说我们的Touch事件被消费了。当然DOWNMOVEUP事件都会响应,所以我们的Button打印了三行log

    再说之后我们把两个Touch方法都返回true

    当这两个onTouch()方法都返回true的时候,就更简单了,还是看我们的dispatchTouchEvent(),即第一个if()判断中mOnTouchListener.onTouch(this, event)true,前面我们已经判断了,if()中其他的都为true,所以第一个if()判断可以进入,最终返回了truedispatchTouchEvent()的返回值为true,根本就走不到第二个if()条件判断,所以跟后面的onTouchEvent()就没什么关系了。所以最终这个Touch事件在dispatchTouchEvent()中的回调的onTouch()方法中得消费掉了。

    注意:不知道有没有同学发现我上面onTouchEvent()方法中除了省略的代码之外,还留下了一行这个方法就与我们的Click事件又关了,我们先看一下这个方法:performClick();

    performClick()

    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
    }
    

    不过要想执行到 performClick() 这个方法要想执行到,首先得能进入到上面 onTouchEvent() 方法中的 if() 条件判断

    下面我们在MainActivity的onCreate方法中给ImageViewButton设置两个click监听事件,把onTouch事件还原到默认的false返回值

        mImageView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
                return false;
            }
        });
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
                return false;
            }
        });
    
    
        mImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG,"----mImageView--------onClick---->");
            }
        });
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG,"----mButton--------onClick---->");
            }
        });
    

    先点击ImageView打印log:

    10-30 14:17:01.255 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
    10-30 14:17:01.272 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
    10-30 14:17:01.305 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
    10-30 14:17:01.309 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onClick---->
    

    再点击Button打印log:

    10-30 14:18:00.624 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
    10-30 14:18:00.641 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
    10-30 14:18:00.671 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
    10-30 14:18:00.674 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onClick---->
    

    咦?竟然一样?你可能要问了,对于Button打印的结果我们可以理解,但是我们只给ImageViewButton添加了Click监听事件,其他都没变,两个onTouch()方法返回的竟然也都一样,按照我们最初只添加两个setOnTouchListener方法,ImageView应该只打印一次才对呀,怎么添加了一个setOnClickListener,就打印三次了呢?而切如果按照我们最开始的推论,既然ImageView是默认不可点击的,那么在onTouchEvent方法中的if()判断条件不可能进入呀,更不会执行到performClick()方法,怎么会也打印出除了最后一行onClicklog呢?

    猜想:上面log既然显示执行了performClick(),也就是说onTouchEvent()中的if()判断条件进入了,而我们只设置了setOnClickListener方法,莫非这个方法做了什么手脚?是不是把ImageViewCLICKABLE的值改变了呢?
    我们进入到setOnClickListener中一探究竟:

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    

    天呐,setClickable(true);看到了吧?我们的猜想是对的,一旦我们设置了setOnClickListener当前ViewCLICKABLE的值就会被置为true,这就不难理解为什么ImageView也能进入到if()判断中,而且执行了performClick()方法。

    通过上面例子我们也的出来一个结论:ViewonTouch()方法是先于onClick()方法执行的,如果onTouch()方法返回true即被消费了,就不会执行onTouchEvent()方法,也就执行不到onClick()方法,
    这点我们也可以验证一下,就是把上面两个setOnTouchListener中的onTouch方法都置为true然后分别点击ImageViewButton然后看log
    点击ImageViewhe

    10-30 14:43:54.142 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
    10-30 14:43:54.150 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
    10-30 14:43:54.202 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
    

    点击Button

    10-30 14:44:13.478 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
    10-30 14:44:13.494 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
    10-30 14:44:13.641 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
    

    看到了吧?即使我们给ImageViewButton也都设置了setOnClickListener,但是在onTouch()方法中返回了true,所以onclick都没有执行

    总结

    • Android中分为两种类型的View,即默认可点击的和默认不可点击的(重点)

    • 当一个Touch事件发出的时候,View中最先执行的是dispatchTouchEvent()

    • 在dispatchTouchEvent()方法内部先判断onTouch()的返回值,根据返回值确定是否执行onTouchEvent()方法

    • 一个View的onTouch事件是优先于onClick执行的

    • onTouch()和onTouchEvent()的共同点是,都在dispatchTouchEvent中执行判断;区别是,onTouch()优先于onTouchEvent()执行,而且其返回值决定了onTouchEvent()能不能够得到执行

    好了以上就是我对View的事件分发机制的全部理解,不足之处或者又不理解的地方请在评论区留下您的评论,大家共同进步。
    最后附上ViewDemo源码:github下载
    下一篇:Android事件分发机制( ViewGroup篇)

    相关文章

      网友评论

          本文标题:Android事件分发机制(View篇)

          本文链接:https://www.haomeiwen.com/subject/yqkmpxtx.html