Android中的View分发机制详解

又是打瞌睡的一天

挥舞着指尖,谱写指尖的艺术

由于自己的能力功底有限,就把我所理解的,所知道的记录下来;

阅读完本章你将收获:

  1. View中的Click事件是在何时触发调用
  2. 为什么Button和TextView所执行的View流程有异同
  3. 如何完美的去解决滑动冲突
  4. view传递事件时是怎样的传递流程

进入正题

1.图解

在这里插入图片描述
这是对事件分发的一个大致的流程图,不过还要细分。

2.概述事件分发机制

1.Toch事件被封装成了MotionEvent 对象,包括Tonch的位置,时间,历史记录,多点触控;
2.事件类型分为:ACTION_DOWN(按下),ACTION_UP(抬起),ACTION_MOVE(移动)等,这三个较为常用;
3.dispatchTouchEvent(),表示对事件进行分发传递;
4.InterceptTouchEvent(),表示对某个事件进行拦截,就不在往下传递;
5.onTouchEvent(),表示消费事件;

首先事件从当前Activity.dispatchTouchEvent()开始传递,只要当前事件没有被拦截,就会沿着最上层的 ViewGroup 传递,交给ViewGroup来处理,ViewGroup默认不拦截事件,就会向下(子view)传递事件,父类的ViewGroup可以通过InterceptTouchEvent()返回true进行拦截事件,则不能继续往下传递事件,消耗了当前事件,如果没有被拦截,则继续往下传递,到达某个子view时,可通过onTonchEvent()返回true处理消费事件,如果当前view的Touch监听不为null,当前view可用,当前view的onTouch方法返回true,则默认消费事件,不在调用onTonchEvent方法。如果事件从上至下未被拦截,并且子view也没有消费事件,事件就会向上传递,父容器ViewGroup就可以消耗处理掉这个时间,如果ViewGroup还不处理事件,则继续往上传递给Activty的onTonchEvent()进行处理。onTonchEvent()中的MOVE,UP,DOWN里,但凡有一个返回的是false,则不会继续往下执行,例如down中返回了false,那么up,move不会得道执行。

3.以Button为例,进行实际的分析

btn.setOnTouchListener(new View.OnTouchListener() {
    
    
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
    
    
                return true;
            }
        });

可以看到这里的返回值为true,当前btn是可用状态,也设置了监听事件,onTouch也返回了true,那么他会向上传递由ViewGroup来决定是否拦截,不拦截再往下传递,传递到当前view时,机会自己消耗事件,不再往下传递。
来看看部分源码#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
    
    
    // ...
            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;
    }

看到第一个if语句,就跟刚才说的一模一样了,

li.mOnTouchListener.onTouch(this, event)

1.由于刚刚btn事件里的Touch返回了true,这里是true,三个条件成立,则result = true;第二个if语句(false && onTouchEvent)直接不会执行该语句,待会再分析onTouchEvent方法;所以这里返回了true。那么就会调用它本身的move,up,click的事件,不在往下传递。
2.如果touch返回的是false,那么就会执行到第二个if语句中,那么就要判断onTouchEvent()是否返回true,来决定result的值。

3.1 dispatchTouchEvent#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();  
     //  省略...
     //判断当前view是否有click事件,单击/长按,进入Touch事件
        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)) {
    
    
                                //处理点击事件
                                    perfoTrmClick();
                                }
                            }
                        }

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

  1. 首先它是判断该view是否有点击事件;
  2. 如果有点击事件的话,执行getAction(),判断是哪些手势,最终都会返回一个true;
  3. 如果该view没有点击事件则返回了false;

如果进入了if语句,要执行move等手势时,如果这些手势有一个实现了false,则进行拦截,后序手势不在触发,只有全为true时,后序手势会触发。
当我们实现onTouch监听时,手势都为true的返回,点击View滑动时,会触发

ACTION_DOWN->ACTION_MOVE->ACTION_UP

可以看到ACTION_UP中实现了perfoTrmClick()方法,这里面就是处理了view的点击事件。
所以得出结论:点击事件是在up中执行,up是最后触发的,所以点击事件为touch监听中最后触发。

3.2dispatchTouchEvent&&onTouchEvent的图解

dispatchTouchEvent执行流程
在这里插入图片描述
onTouchEvent()处理流程
在这里插入图片描述
本文中没提到View代理,想深入了解的可移步其他文章。

3.3ViewGroup的拦截

上面提到的都是关于view事件是否消耗的过程,现在来看看拦截的过程

public class DiyViewGroup extends ViewGroup implements View.OnTouchListener{
    
    
        private Context mContext;
        //拦截事件
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
    
            return false;
        }
 }

我这里自定义了ViweGroup,重写了onInterceptTouchEvent方法,返回了false,则默认不拦截,继续向下传递事件,当返回true时,ViweGroup下的子view的各种手势事件将失效,因为收不到事件的传递。这时会调用ViewGroup自身的MOVE,UP,DOWN;

回归自然

回到开头的问题上

  1. Click点击事件在onTouchEvent中的up手势里的 perfoTrmClick()执行了;
  2. onTouchEvent中就会判断view是否有点击事件,而button自带点击事件,TextView没实现点击事件,所以TextView会返回false;
  3. 滑动冲突不是本节重要知识点,后面博客会讲到,解决滑动冲突就需要重写ViewGrou的拦截事件,拿到getAction判断是什么手势,根据逻辑判断用户移动的坐标,是否超过滑动的区域,来返回true或者false。

最后鸣谢各大平台上的博客提供参考

猜你喜欢

转载自blog.csdn.net/q1210249579/article/details/110817624