美文网首页Android开发Android技术知识Android开发经验谈
初探Android事件分发机制源码下之ViewGroup,Vi

初探Android事件分发机制源码下之ViewGroup,Vi

作者: 晨心w | 来源:发表于2017-08-17 10:46 被阅读89次

    在上一篇中我们一起分析了事件从手机硬件传递到DecroView的过程,接着本文我们一起来分析一下ViewGroup和View是怎么传递,处理触摸事件的。
    View的事件分发机制重要性不言而喻,面试,平时做都是经常接触。平时都是照着代码写,但是其实并不知道很多原理。比如为什么onTouch比OnClick先执行?为什么onTouch返回true后OnClick就不再执行?onTouch和onTouchEvent有什么区别?这些问题可以说是一直困扰着很多人。今天,我们就通过写demo,边看效果边跟源码来一个一个问题的分析。本文源码均来自API24。文末会附加分析触摸事件从硬件开始到最后View处理的整个流程。

    秉承着前人栽树后人乘凉的想法,首先给出自己所读总结的精华和大神的总结图:
    在Android中,View的事件分发主要有3个方法:dispatchTouchEvent()、 onInterceptTouchEvent() 以及 onTouchEvent()。当我们点击某个子View控件时,首先调用的是这个view的父ViewGroup的dispatchTouchEvent(),dispatchTouchEvent()内部调用onInterceptTouchEvent()来决定是否拦截该事件,如果拦截,则调用ViewGroup的onTouchEvent()进行处理,并且屏蔽掉该事件,不再往下传递,所以有可能会产生你点击某个按钮,按钮的处理动作没有发生,父控件的处理事件触发了的情况。如果父ViewGroup不拦截事件,否则调用子View的dispatchTouchEvent(),可以参考如下图:


    出自http://blog.csdn.net/huachao1001
    -------------------------------一条酷炫的分割线-----------------------------------------
    总结说完了,说句实话也就只有那些以前研究过回头来看的大神们想起来了是个什么回事,没了解过的萌新们看了半天还是一句哈玩意儿?那么接下来我们就写一个demo,边看效果边跟源代码来分析一下。
    ------------------------------又是一条酷炫的分割线------------------------------------

    ViewGroup

    首先我们来定义一个布局,只有一个Button按钮,外围是一个我们自定义的Layout,我们取名为TestLayout。暂时只继承于LineLayout,不加任何代码。

    xml布局

    TestLayout代码:

    public class TestLayout extends LinearLayout {
    
        public TestLayout(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public TestLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    }
    

    Activity代码:

    public class Main6Activity extends AppCompatActivity {
    
        Button mButton;
        TestLayout mTestLayout;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main6);
            mButton = (Button) findViewById(R.id.test_bt);
            mTestLayout = (TestLayout) findViewById(R.id.test_layout);
    
            //TestLayout注册onTouchListener
            mTestLayout.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.d("touchEvent", "mTestLayout的触摸事件执行了!执行Action为:" + event.getAction());
                    return false;
                }
            });
    
            //mButton注册OnClickListener        
            mButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.d("touchEvent", "button的点击事件执行了!");
                }
            });
            //mButton注册onTouchListener        
            mButton.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.d("touchEvent", "button的触摸事件执行了!执行Action为:" + event.getAction());
                    return false;
                }
            });
        }
    }
    
    

    点击按钮运行一下看一下日志:


    点击按钮日志

    首先我们看到是button的onTouch方法首先执行(有手指按下和抬起两个事件),随后执行button的onClick方法。而父布局testLayout的touch事件没有被触发。
    emmm.......
    哈玩意儿?不是说好的事件先给viewGroup么?咋这都没反应呢??那给父布局注册touch事件干嘛啊。别忙,既然这样,我们就去看看ViewGroup的dispatchTouchEvent()方法的源代码(读者如果亲自跟进去就会发现这个方法的源码很长很复杂,这里为了分析,分解为各个部分讲解):

     @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
                    ...//其他代码
    
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial down.
    代码1处---------------------------
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    // Throw away all previous state when starting a new touch gesture.
                    // The framework may have dropped the up or cancel event for the previous gesture
                    // due to an app switch, ANR, or some other state change.
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
    

    我们将关键代码贴出来,我们来开始分析一下,在上面代码1处判断如果是按下事件,会调用cancelAndClearTouchTargets(ev)方法,这个方法里面会把mFirstTouchTarget置空。这个变量非常重要,在后面会继续讲解。接着往下看:

                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {
    代码2处---------------------------
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action); // restore action in case it was changed
                    } else {
                        intercepted = false;
                    }
                } else {
                    intercepted = true;
                }
              ...//其他代码
    

    接下来继续分析,当如果是按下事件或者 mFirstTouchTarget 不为空的时候会调用代码2处的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;
        }
    

    在这里我们可以看做这个方法正常情况下是默认返回false。因此这里也说明了ViewGroup是默认不会拦截触摸事件的。所以在之前的demo效果中自然会出现父布局没有做任何处理直接将触摸事件往下传递给Button的情况。如果我们重写父布局的onInterceptTouchEvent()方法返回true。那么表明ViewGroup父布局拦截触摸事件,事件就不会再继续往下传递给子View。我们来继续改demo看看效果:
    在TestLayout中重写onInterceptTouchEvent()方法返回false:

    public class TestLayout extends LinearLayout {
    
       ...//其他代码
    
      //重写该方法,返回true
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return true;
        }
    }
    

    我们再点击按钮看看效果:

    点击按钮日志

    如上图,我们可以看到,当TestLayout拦截了触摸事件时,Button的触摸和点击事件都没有被触发。因此我们可以得出结论,事件分发是从父布局向子控件传递的。
    回到ViewGroup的代码我们接着往下看:

    代码3处---------------------------
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
                if (!canceled && !intercepted) {
                            ...//其他代码
    
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                          ...//其他代码
    代码4处---------------------------
                        final int childrenCount = mChildrenCount;
                        if (newTouchTarget == null && childrenCount != 0) {
                            final float x = ev.getX(actionIndex);
                            final float y = ev.getY(actionIndex);
                            // Find a child that can receive the event.
                            // Scan children from front to back.
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                               ...//其他代码
                            }
    

    在代码3处,会定义一个 newTouchTarget变量。接着会判断事件是否取消并且是否打断,如果都不。那么执行if条件里面的代码。
    在代码4处,会定义一个int类型的childrenCount来表示该ViewGroup有多少个子View。接下来再判断childrenCount个数是否为0(即ViewGroup是否有子View)。如果有,那么就执行代码去寻找事件应该传递到哪个集体的子View。

    继续往下看源码:

    代码5处---------------------------
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    // Child wants to receive touch within its bounds.
                                    mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {
                                        // childIndex points into presorted list, find original index
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) {
                                                mLastTouchDownIndex = j;
                                                break;
                                            }
                                        }
                                    } else {
                                        mLastTouchDownIndex = childIndex;
                                    }
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
    代码6处---------------------------
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                                ev.setTargetAccessibilityFocus(false);
                            }
                            if (preorderedList != null) preorderedList.clear();
                        }
    
                        if (newTouchTarget == null && mFirstTouchTarget != null) {
                            newTouchTarget = mFirstTouchTarget;
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }
                }
    
    

    我们可以看到在代码5处会调用dispatchTransformedTouchEvent()方法,这个是什么方法呢?我们再跟进去看看:

       private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    ...//其他代码
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
    ....//其他代码
    

    我们在这里就可以看到,当child为空时,表示没有找到触摸事件在某个具体子View的范围内。此时调用super.dispatchTouchEvent()方法,而ViewGroup的父类是View对象。表明此时事件已经交给ViewGroup自己处理。而如果child不为空,则调用子View的dispatchTouchEvent()方法来处理。当触摸事件处理完毕,就会返回一个布尔值handled,该值表示子View是否消耗了事件。怎样判断一个子View是否消耗了事件呢?如果说子View的onTouchEvent()返回true,那么就是表明消耗了事件。

    接下来在代码6处,会调用addTouchTarget()方法来赋值给newTouchTarget变量。我们再跟进去看一看:

      private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    我们可以看到,首先是找到具体的child的TouchTarget对象,然后mFirstTouchTarget指向这个child.然后返回target。

    继续回到ViewGroup的dispatchTouchEvent()方法往下看:

    代码7处---------------------------
                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    while (target != null) {
                        final TouchTarget next = target.next;
    代码8处---------------------------
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                        }
                                 ...//其他代码
                    }
                }
    
              ...//其他代码
            return handled;
        }
    

    在代码7处会判断mFirstTouchTarget是否为空,很明显,在刚才的分析里可以看到,如果我们的触摸事件没有被拦截,就会在addTouchTarget()方法中为mFirstTouchTarget赋值,因此这段代码就不会被执行。只有在ViewGroup类型不拦截或者点击了ViewGroup的空白处(没有点击任何子控件,事件没有发生在任何子View的范围内)。这两种情况下mFirstTouchTarget不会被改变依然为null才会执行if条件里面的代码。换句话说反过来也就是如果mFirstTouchTarget == null条件成立执行的这段代码会处理ViewGroup拦截了事件或者所有子View均不消耗事件这两种情况。那么在这里调用dispatchTransformedTouchEvent()方法交由ViewGroup处理事件。如果mFirstTouchTarget不为空,那么表明有child已经处理了ACTION_DOWN触摸事件,那么执行else代码块的代码。

    到代码8处,在上面的分析中可以看到,如果子View消耗了ACTION_DOWN触摸事件,那么alreadyDispatchedToNewTouchTarget会修改为true并且target == newTouchTarget也是成立的。因此表示这是个ACTION_DOWN事件,如果有一个不成立,表明是ACTION_DOWN之外的其他事件,那么在这里继续把其他事件分发给子View处理。
    至此我们就已经把ViewGroup的dispatchTouchEvent()方法分析完毕了。总结一下:

    • 当事件传递到我们的ViewGroup时,会调用dispatchTouchEvent()方法,在里面首先会调用onInterceptTouchEvent()方法(默认为false不拦截)来决定ViewGroup是否拦截该事件。结果为两种:1. 如果方法返回true,即要拦截,那么事件传递到此为止,不再传递给子View。2.如果放回false,不拦截事件,那么将事件传递给子View,由子View去进行处理。

    View

    上面分析完了ViewGroup的dispatchTouchEvent()源码,接下来我们来分析分析View的dispatchTouchEvent()方法。

    我们先来把demo改一下代码,首先把之前的Button去掉,重写一个自定义View继承自Button重写onTouchEvent()方法(记得把我们之前的TestLayout的拦截事件方法改为返回false哦,否则事件都被拦截了):

    public class TestButton extends Button {
    
        public TestButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public void init() {
            setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.d("touchEvent", "button的点击事件执行了!");
                }
            });
    
            setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.d("touchEvent", "button的onTouch方法执行了!执行Action为:" + event.getAction());
                    return false;
                }
            });
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.d("touchEvent", "button的onTouchEvent方法执行了!执行Action为:" + event.getAction());
            return super.onTouchEvent(event);
        }
    }
    

    布局如下很简单:

    <com.demo.dltlayoutlayoutparams.TestLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/test_layout"
        tools:context="com.demo.dltlayoutlayoutparams.Main6Activity">
    
        <com.demo.dltlayoutlayoutparams.TestButton
            android:id="@+id/test_bt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
    </com.demo.dltlayoutlayoutparams.TestLayout>
    

    Activity的代码如下:

    public class Main6Activity extends AppCompatActivity {
    
        TestLayout mTestLayout;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main6);
            mTestLayout = (TestLayout) findViewById(R.id.test_layout);
    
            mTestLayout.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.d("touchEvent", "mTestLayout的触摸事件执行了!执行Action为:" + event.getAction());
                    return false;
                }
            });
        }
    }
    

    我们运行demo点击一下按钮,看一下日志结果:


    点击按钮日志

    如上图结果,我们可以看到显然是onTouch方法先执行,然后OntouchEvent()再执行,然后又执行了click()方法,因为我们手指按下后又抬起。所以有两个事件先后发生处理。

    那么为什么onTouch()方法会比onTouchEvent()方法先执行呢?onTouch()方法返回类型为布尔值,有什么用呢?(不可能因为onTouch字母数比onTouchEvent()字母少所以先执行把?)带着这些疑惑,我们来一起分析分析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)) {
                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)) {
                    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;
        }
    

    emmm...首先可以看到源码其实包含了很多注释了很多其他代码来做一些判断和处理工作,在这里我抽出核心过程代码来看看:

      //ListenerInfo是一个包装类,里面封装了View的各种监听器Listener。
      //我们设置onTouchListener也是设置给ListenerInfo。
       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;
                }
    

    可以看出,首先如果view的OnTouchListener不为空并且我们的View是可以编辑的(处于Enable状态)并且重写的onTouch()方法返回true,那么result设置为true。那么接下来就不会再执行onTouchEvent()方法。因此onTouch()方法比onTouchEvent()方法优先执行的原因在这里。我们来设置onTouch()方法返回true来看看效果:

    setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.d("touchEvent", "button的onTouch方法执行了!执行Action为:" + event.getAction());
                    //修改前
                    return false;
                    //修改后
                    return true;
                }
            });
        }
    

    运行点击一下按钮:

    按钮点击日志

    可以看到我们onTouch方法返回true,那么就不会再执行onTouchEvent()方法。

    诶?突然发现,怎么onClick()方法也没了呢??我没有改点击事件啊?那么为什么我们的onTouch()方法返回true后,onClick()方法没有执行呢?我们跟进去onTouchEvent()方法一探究竟。

    先放上onTouchEvent()方法的源代码:

     public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == 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)
                        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
            }
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                    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 && !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)) {
                                        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();
                        }
                        mIgnoreNextUpEvent = false;
                        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, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        setPressed(false);
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        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;
    
        }
    

    emmm.....好像有点长??看了真是让人头大。那么长的代码。大概的我们也不可能一一去分析,那么我们就过滤掉一些无关代码,来提取一下精华:

     public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
         
            .......//不重要代码
    
            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                                    .......//不重要代码
    
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
            .......//不重要代码
            return false;
    
        }
    

    经过这么一提取,关键代码就出现了。我们可以看到在上面的case MotionEvent.ACTION_UP情况下,我们获取执行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;
        }
    

    至此就很清晰了,就是在这里执行了我们在代码中设置的onClickListener的onClick()方法。因此,如果我们的onTouch()方法返回true,那么onTouchEvent()方法也不会执行,那么onTouchEvent()方法里的onClick()方法更不能得到执行!

    那么问题又又又来了....onTouch()返回值影响了onTouchEvent()是否执行,那么onTouchEvent()的返回值又是拿来干嘛的?好像这个返回值又不影响点击事件的执行?我们来改一改试成false和true试试:

    @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.d("touchEvent", "button的onTouchEvent方法执行了!执行Action为:" + event.getAction());
            //修改前
            return super.onTouchEvent(event);
            //第一次修改后
            return false;
            //第二次修改后
            return true;
        }
    

    我们来看一下效果:


    返回false效果
    返回true效果

    我们看到,首先修改为true或者false后,点击事件都消失了,这个其实很好理解。因为onClick()方法之前分析过是在View的onTouchEvent()方法调用的。而我们现在重写onTouchEvent()方法却没有调用super.onTouchEvent()方法,因此点击事件肯定不会被调用。

    其次,我们发现,在返回false的时候发现onTouch和onTouchEvent()都只是处理了按下的动作,并没有继续处理后面的抬起动作。而且TestLayout的onTouch()抬起动作执行了,但是也没处理抬起动作。

    其实这是因为onTouchEvent()当我们返回false,表明我们子View不处理事件,于是就会将事件全部回传给父布局。当down事件不消耗后,后续的move,up等等事件也不再执行onTouch()方法。因此在这里直接将事件交给父布局,而在这里我们的父布局TestLayout在Activity中设置的onTouch()方法也返回false。因此也会将事件继续上传,这里有兴趣的同志可以把TestLayout的onTouch()方法返回true来看一下效果(文章篇幅实在太长了。。。)。
    而当我们返回true就表示我们要处理事件,因此事件就不会回传到父布局了。

    ------------------------------一条假装很华丽的分割线---------------------------------
    额外分析: 具体原因是在文初的ViewGroup源码的代码5处调用onTouch()方法,而这个代码5处是包含在代码4处上面的if条件里面的,条件是事件为手指按下事件或者其他事件才会执行。然后onTouch方法中调用的onTouchEvent()方法,如果onTouchEvent()返回false,那么onTouch()方法也返回false(可看上面View的dispatchTouchEvent()源码),那么代码5处的条件判断不成立,那么mFirstTouchTarget变量不会被修改仍然为null,因此在走到ViewGroup源代码7处交给父布局处理了。而其他比如手指移动,手指抬起事件不会再走代码4,5处。因此直接跳到代码7处,mFirstTouchTarget仍然为null。所以说当子View不处理down事件,所有事件就不会再交给子View来处理了。换言之所以只有onTouchEvent()中第一个down事件处理返回true,才会使得mFirstTouchTarget赋值不为null。那么在代码7处才会把后续事件交给子View处理。
    ------------------------------一条假装很华丽的结束线---------------------------------


    呼...至此总算就分析完了ViewGroup和View的事件分发的传递和回传过程,也算是完结撒花啦!
    最后总结一下View:

    • View的执行顺序为onTouch()->onTouchEvent()->onClick()。如果onTouch()返回true表示消耗了事件,就不再传给onTouchEvent()执行。(好比小明和小刚一起去买冰淇淋,小明先吃一口,他可以选择把冰淇淋给小刚吃还是把冰淇淋留下来)

    • 如果View只消耗down事件,而不消耗其他事件,那么其他事件不会回传给ViewGroup,而是默默的消逝掉。我们知道,一旦消耗down事件,接下来的该系列所有的事件都会交给这个View,因此,如果不处理down以外的事件,这些事件就会被“遗弃”。

    • 某个View,在onTouchEvent中,如果针对最开始的down事件都返回false,那么接下来的事件系列都不会交给这个View。

    • View没有onInterceptTouchEvent方法。因为它不是一个父控件,不需要决定是否拦截。


    再ps:本文也参考借鉴了郭霖大神和各位大神的文章:
    Android事件分发机制完全解析,带你从源码的角度彻底理解
    彻底理解View事件体系!

    相关文章

      网友评论

        本文标题: 初探Android事件分发机制源码下之ViewGroup,Vi

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