View的事件分发简单说明

2021-08-11  本文已影响0人  取了个很好听的名字

前言

本文是阅读ViewGroup事件分发后的总结,仅供自己复习查阅使用,如果能够帮到您,那我很荣幸。PS:本文只分析单点触摸的情况

事件的类型

ACTION_DOWN:手指初次接触到屏幕时触发
ACTION_MOVE:手指在屏幕上滑动时触发,会多次触发
ACTION_UP:手指在屏幕上抬起时触发
ACTION_CANCEL:事件被上层拦截时触发

关于View的一点说明

View:例如ImageView,TextView等,不会有子View的存在,只能处理触摸事件而不能分发事件。
ViewGroup:例如LinearLayout、ConstraintLayout,可能存在子View,本身即可以分发事件也可以处理事件,在分发事件时如果所有的子View都不能处理,则需要自己进行事件处理,如果自身也无法处理的话,则通知父控件自身无法处理该触摸事件。

ViewGroup的事件分发说明

一个点击事件最先由activity进行分发,在传到ViewGroup,最后传到View。

事件分发顺序.png

首先看一下Activity的dispatchTouchEvent方法吧

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
           //设置屏保,空实现
            onUserInteraction();
        }
       //调用window的superDispatchTouchEvent方法。
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

代码很清晰,如果window的superDispatchTouchEvent返回true则,则该方法返回true,否则调用Activity的onTouchEvent方法的结果作为返回。

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

Activity的onTouchEvent中会判断点击区域是否在window的边界上,如果是返回true,否则返回false。现在在回过头来getWindow().superDispatchTouchEvent(ev),由于window是一个抽象类且只有一个实现类PhoneWindow,所以我们直接看PhoneWindow.superDispatchTouchEvent()即可。

public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

PhoneWindow直接调用了DecorView的superDispatchTouchEvent方法。

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

DecorView会调用ViewGroup的dispatchTouchEvent方法,这也是我们需要关注的重点之一。
现在总结一下Activity到ViewGroup的事件分发顺序,流程如下图


流程总结.png

现在看一下ViewGroup的dispatchTouchEvent吧

 public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

           ......

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //获取子View是否允许父View拦截事件,子控件调用requestDisallowInterceptTouchEvent来通知父控件是否可以拦截事件,默认为false
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //子View不阻拦父View
                if (!disallowIntercept) {
                    //调用onInterceptTouchEvent方法判断是否拦截此事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

     

          
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不是取消事件且父View不进行拦截
            if (!canceled && !intercepted) {
              
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
              
                    ......
                    //获取子View数量
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        .....
                        //将子View按照一定顺序排列成集合
                        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);
                             //获取指定位置的View
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }
                            .....
                            //分发指定事件到该view,如果该View能够消费事件则跳出循环,否则继续寻找下一个子View进行处理
                            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();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                          .....
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                   .......
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                //没有子View处理该事件,则由该ViewGroup尝试处理
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                 //不分析多点触摸
                 .......
            }

           .....
        }

        .....
        return handled;
    }

总结一下:首先询问父View是否拦截该事件,如果父View拦截事件的话则由父View调用dispatchTransformedTouchEvent方法进行处理,子View可以通过调用requestDisallowInterceptTouchEvent方法阻拦父View拦截该事件,如果父View不拦截则询问子View是否能够消费该事件(调用dispatchTransformedTouchEvent方法),如果没有子View能够消费的话,则仍交由父View尝试消费该事件(调用dispatchTransformedTouchEvent方法)。有代码可以看出询问子View是否消耗该事件和父View尝试消耗该事件都会调用ViewGroup.dispatchTransformedTouchEvent这个方法。再看一下ViewGroup.dispatchTransformedTouchEvent这个方法

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        //这里需要注意如果是父View尝试自己处理则child, 为null,是子View则不为null

        final int oldAction = event.getAction();
         //不是取消事件
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                //父View(ViewGroup),调用View的dispatchTouchEvent方法
                handled = super.dispatchTouchEvent(event);
            } else {
               //子View 
              //为ViewGroup 则会调用ViewGroup.dispatchTouchEvent,形成嵌套调用
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        .....
        return handled;
    }

通过这个方法我们能够看出ViewGroup的diapatchTouchEvent方法存在嵌套调用,这是因为父控件的子View可能是View也有可能是ViewGroup,如果是ViewGroup则和父View一样进行事件的分发。如此则形成了嵌套调用。至此ViewGroup的事件分发分析完毕。

View的事件处理

public boolean dispatchTouchEvent(MotionEvent event) {
.......
boolean result = false;

    .....

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
       //如果该控件是enable状态且设置了触摸回调则会调用触摸回调,如果触摸回调返回了true,则消费了该事件
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        
        //调用onTouchEvent为true表示子View消费该事件,否则不消费该事件
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    ......

    return result;
}

总结一下,如果该View设置了onTouchEventListener且该View处于Enable状态则执行回调的onTouchEvent方法,如果返回true则消费该事件,返回false则不消费该事件。如果该View不处于Enable状态,没有设置回调或者回调返回false,则调用onToucheEvent尝试进行事件的消费。最后看一下onToucheEvent方法,这也是View事件分发的一个重点需要分析的方法。

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //记录点击状态
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        //存在触摸代理,则交由触摸代理进行处理
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                //处理抬起事件,主要看PerformClick
                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) {
                                      //返回一个runnable,一会重点分析
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                    ......
                    break;
                 //处理按下事件
                case MotionEvent.ACTION_DOWN:
                   .....
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                  ......
                    break;
                //处理取消事件
                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
                
                 //处理移动事件
                case MotionEvent.ACTION_MOVE:
                   ......
                    break;
            }
 // 若该控件可点击,就一定返回true,消费该事件
            return true;
        }
// 若该控件不可点击,就一定返回false,不消费该事件
        return false;
    }

再来看一下performClick

 private final class PerformClick implements Runnable {
        @Override
        public void run() {
            recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
             //调用performClickInternal
            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();
         //调用performClick方法
        return 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;
        //如果设置了OnClickListener则执行回调,方法返回true
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

通过这段源码分析大家应该了解setOnClickListener方法的回调时机了。特别需要注意的是如果设置了setOnTouchListener并回调返回了true则不会执行setOnClickListener的回调的,View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false,如果该View不消费该事件,则由其父组件分发给其他的子View或者自己尝试处理。。View的diapatchEvent调用顺序如下:

View.dispatchTouchEvent执行顺序.png

总结

以上就是View事件分发的相关内容。掌握了相关内容才能比较容易解决事件冲突打下良好的基础

上一篇 下一篇

猜你喜欢

热点阅读