美文网首页
Android事件传递机制

Android事件传递机制

作者: keepWriteCode | 来源:发表于2019-04-30 11:10 被阅读0次
    1.View和ViewGroup

    View是Android视图的抽象,View是Android所有视图组件的基类。无论是常用的Android基础组件TextView、Button还是复杂的Android布局组件LinearLayout、RelativeLayout都是View的派生类。因此View是Android视图层的抽象基类。

    ViewGroup继承了View,ViewGroup译为视图组,即多个View组成的视图组。因此ViewGroup不仅可以包含多个View还可以包含多个ViewGroup,而View则不可以包含View和ViewGroup。上文提到的Android布局组件LinearLayout和RelativeLayout不仅是View的派生类还是ViewGroup的派生类,因此布局组件可以包含TextView和Button,而TextView和Button则不能包含TextView和Button更不能包含LinearLayout和RelativeLayout。因此ViewGroup是Android视图层视图容器的抽象基类。

    基于View和ViewGroup的特性组成了View树结构,如下图所示。

    View树.png

    当触摸事件发生时需要一套机制来确定哪个View响应事件,因此Android设计了一套事件的传递和消费机制。

    2.MotionEvent

    MotionEvent表示用户所有的触摸事件,例如:

    MotionEvent.ACTION_DOWN = 0:按下屏幕

    MotionEvent.ACTION_UP = 1:从屏幕上抬起

    MotionEvent.ACTION_MOVE = 2:在屏幕上滑动

    MotionEvent源码中大约有9种,有部分是被废弃了,以上是三个常用的触摸事件。ACTION_DOWN是触摸事件的开始,ACTION_UP是事件的结束,中间可以N个ACTION_MOVE(N可以等于0)。
    正常情况下,点击事件的事件顺序是ACTION_DOWN -->> ACTION_UP;滑动事件的事件顺序是ACTION_DOWN -->> ACTION_MOVE -->> ...(多个ACTION_MOVE) -->> ACTION_UP。

    3.Android触摸事件相关函数

    Android事件传递和消费的函数最主要的有三个,dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent。

    dispatchTouchEvent
    1.dispatchTouchEvent负责将事件一层层的传递,直到事件被消费了为止。

    当点击屏幕的时候,Activity将事件传给Window,Window再将事件传给View树的root View,root View再传给子View。事件传递顺序如图所示

    dispatchTouchEvent.png

    事件传递到root View时必然会执行dispatchTouchEvent函数。dispatchTouchEvent先调用当前View的onInterceptTouchEvent函数询问是否拦截该事件,onInterceptTouchEvent return true表示拦截事件,此时dispatchTouchEvent将调用当前View的onTouchEvent函数消费事件。如果onInterceptTouchEvent return false(默认 return false)表示不拦截事件,此时dispatchTouchEvent将调用当前View的子元素的dispatchTouchEvent向子元素分发事件。如此循环,直至事件被消费为止。

    下面是表述三者关系的伪代码,通过伪代码,我们可以清晰的了解事件分发的逻辑。

    // root View的dispatchTouchEvent函数
    public boolean dispatchTouchEvent(MotionEvent event){
        boolean result = false;
      if(onInterceptTouchEvent()){ // 首先判断是否拦截
        result = onTouchEvent(); // 拦截就消费
      }else{
        result = child.dispatchTouchEvent(); // 不拦截就向子元素分发
      }
    return result; // 返回结果取决于当前View和当前View的子元素是否消费了事件
    }
    

    值得注意的是由于View里没有onInterceptTouchEvent函数,所以上述伪代码逻辑只在ViewGroup里成立。因为View没有子元素,所以不需要在分发事件时进行拦截。从源码中也能发现View的dispatchTouchEvent逻辑要比ViewGroup里简单一些。ViewGroup在dispatchTouchEvent里分发事件时不仅要考虑到自身是否消费事件还要考虑是否向子元素分发事件,而View只需要考虑自身如何消费事件。

    2.通过查看源码我们也能理清这三个函数的关系,下面是ViewGroup的dispatchTouchEvent和View里的dispatchTouchEvent。
    // 这是ViewGroup的dispatchTouchEvent函数的关键代码
    // Check for interception.
    
    final boolean intercepted;
    
    if (actionMasked == MotionEvent.ACTION_DOWN
    
            ||mFirstTouchTarget !=null) {
    
    final boolean disallowIntercept = (mGroupFlags &FLAG_DISALLOW_INTERCEPT) !=0;
    
        if (!disallowIntercept) {
    
    intercepted = onInterceptTouchEvent(ev);//在这里调用onInterceptTouchEvent函数询问是否拦截
    
            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;
    
    }
    
    // 这是View的dispatchTouchEvent函数的全部代码,没有onInterceptTouchEvent只调用了关键的处理事件的函数onTouch和onTouchEvent
    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)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //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)) { // 在这里调用onTouchEvent
                    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;
        }
    
    onInterceptTouchEvent
    1.onInterceptTouchEvent,直译为拦截事件,即在事件分发过程中起到拦截事件的作用。上文提到当事件被onInterceptTouchEvent拦截,事件就不会向子元素分发,而是交由当前View的onTouchEvent进行消费。默认是不拦截的,除非重写该函数并返回true。

    下面是ViewGroup的onInterceptTouchEvent函数源码

    public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                    && ev.getAction() == MotionEvent.ACTION_DOWN
                    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
                return true;//表示拦截
            }
            return false;//默认表示不拦截
        }
    
    onTouchEvent
    1.onTouchEvent函数主要是处理触摸事件,并告知dispatchTouchEvent是否消费了事件。

    当用户按下,滑动,弹起的时候在onTouchEvent做相应的响应操作。比如按下一个Button的时候,onClickListener的onClick函数会在onTouchEvent的ACTION_UP条件下执行回调。通常情况下,自定义View时会在该函数里处理View的触摸事件。

    下面是View源码中onTouchEvent函数,ACTION_UP发生时执行的部分代码。

    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)) {
                                        performClickInternal();//在这个函数里调用了performClick
                                    }
                                }
                            }
    

    performClickInternal()

    private boolean performClickInternal() {
            // Must notify autofill manager before performing the click actions to avoid scenarios where
            // the app has a click listener that changes the state of views the autofill service might
            // be interested on.
            notifyAutofillManagerOnClick();
    
            return performClick();//在这个函数里调用的onClick函数
        }
    

    performClick()

    public boolean performClick() {
            // We still need to call this method to handle the cases where performClick() was called
            // externally, instead of through performClickInternal()
            notifyAutofillManagerOnClick();
    
            final boolean result;
            final ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                li.mOnClickListener.onClick(this);//onClick在这里被调用
                result = true;
            } else {
                result = false;
            }
    
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    
            notifyEnterOrExitForAutoFillIfNeeded(true);
    
            return result;
        }
    
    4.onTouch onTouchEvent onClick onLongClick函数的执行顺序以及源码分析

    在上面四个函数中相对陌生的是onTouch函数,onTouch 即触摸函数。在View的源码中这样解释这个函数“Called when a touch event is dispatched to a view. This allows listeners to get a chance to respond before the target view. ”大意是当触摸事件发生时调用,让监听者有机会在目标View之前响应触摸事件。要想在onTouch函数里处理事件,必须设置onTouchListener监听,就像给Button设置onClickListener监听一样。
    如果给一个View同时设置onTouchListener,onClickListener,onLongClickListener,那么onTouch,onClick,onLongClick以及onTouchEvent函数哪个先被调用呢?

    下面是点击Button打印的log,通过log发现调用顺序onTouch>onTouchEvent>onClick

    ClickLog.jpg

    下面是长按Button打印的log,此时调用顺序onTouch>onTouchEvent>onLongClick>onClick并且还发现onLongClick是在ACTION_DOWN的时候调用的,onClick则是在ACTION_UP时调用的。

    LongClickLog.jpg

    下面是demo的主要代码

    package com.text.demo;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity implements View.OnClickListener,View.OnLongClickListener,View.OnTouchListener{
        private MyButton bt;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            bt = findViewById(R.id.Button);
            bt.setOnClickListener(this);
            bt.setOnLongClickListener(this);
            bt.setOnTouchListener(this);
    
        }
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.i("zxq-bt-onTouch",event.getAction() + "");
            return false;
        }
    
        @Override
        public boolean onLongClick(View v) {
            Log.i("zxq-bt-onLongClick",v.toString());
            return false;
        }
    
        @Override
        public void onClick(View v) {
            Log.i("zxq-bt-onClick",v.toString());
            Toast.makeText(MainActivity.this,"button",Toast.LENGTH_LONG).show();
        }
    }
    
    package com.text.demo;
    import android.content.Context;
    import android.support.v7.widget.AppCompatTextView;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    
    public class MyButton extends AppCompatTextView{
    
        public MyButton(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public MyButton(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyButton(Context context) {
            super(context);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.i("zxq-bt-onTouchEvent", "" + event.getAction());
            return super.onTouchEvent(event);
        }
    }
    

    通过阅读源码,进一步证实了上面的结论。在View的dispatchTouchEvent函数中,确实是先调用onTouch函数,如果onTouch函数return true表示消费了触摸事件,此时就不会执行onTouchEvent函数了。

    if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {// 如果设置了onTouchListener监听则调用onTouch函数。
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {如果onTouch返回true则result=true那么!result为false,此时就不会调用onTouchEvent函数了。
                    result = true;
                }
            }
    

    而在View的onTouchEvent函数中,onLongClick函数确实是在ACTION_DOWN的时候就调用了,而onClick则是ACTION_UP中才调用的。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                       ...
                            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)) {
                                        performClickInternal();// onClick函数在这个方法里调用,上文有更详细的源码。
                                    }
                                }
                            }
                        ...
                        }
                        mIgnoreNextUpEvent = false;
                        break;
                    case MotionEvent.ACTION_DOWN:
                       ...
                        if (!clickable) {
                            checkForLongClick(0, x, y);// onLongClick在这个函数里调用
                            break;
                        }
                       ...
                        break;
                    ...
                return true;
            }
    
    5.事件分发过程中的一些结论
    如果View不消耗除ACTION-DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。
    如果View消费了ACTION-DOWN事件,那么后续的ACTION-UP也会交给这个View去处理。
    ViewGroup向其子View分发事件的顺序是从前到后,也就是最后add到ViewGroup里的元素最先遍历到,最先add到ViewGroup里的元素最后遍历到。

    相关文章

      网友评论

          本文标题:Android事件传递机制

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