View 事件分发机制

View 中的事件消息传递,是android的一个重点和难点,我们只有掌握了它,才能更好的理解view,写出自己比较满意的自定义控件,解决控件嵌套时产生的滑动冲突和点击事件失效问题。
我们知道 View 是所有控件的基类,是祖师爷级的存在,我们从它入手,看看它里面的有关事件的方法 dispatchTouchEvent(MotionEvent event) 、 onTouchEvent(MotionEvent event)
、setOnTouchListener(OnTouchListener l) 、 setOnClickListener(OnClickListener l) 、 setOnLongClickListener(OnLongClickListener l) 等,我们先写个简单的demo,看一下

public class TouTestView extends View {

    private final static String TAG = "TouTestView";

    public TouTestView(Context context) {
        this(context, null);
    }

    public TouTestView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouTestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initLister();
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent:  " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent:  " + event.getAction() );
        return super.onTouchEvent(event);
    }

    private void initLister() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouchListener:  " + event.getAction() + "  " + isClickable() +"   " + isEnabled());
                return false;
            }
        });

        Log.e(TAG, "setOnClickListener"  + "  " + isClickable() +"   " + isEnabled());
    }

    
}

把该控件放入xml布局中,我们进入该页面,发现打印了log,
    E/TouTestView: setOnClickListener   isClickable()   false     isEnabled()  true
这个是 initLister() 方法中的日志,显示默认view中 默认是无点击, enabled 属性时 true,先打印这个log,放在这里,往下面分析。我们知道,View 的事件分发入口是dispatchTouchEvent(MotionEvent event) 方法,消费事件的有 onTouchEvent(MotionEvent event) 、OnTouchListener 回调,甚至还有 点击事件和长点击事件,这里先分析前面三个方法,当view接收到一个事件,调用 dispatchTouchEvent(MotionEvent event) 方法,我们看一下简化的源码

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
        if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }
        ...
        return result;
    }

    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

onFilterTouchEventForSecurity(MotionEvent event) 方法默认是true, 除非设置view的布局属性 filterTouchesWhenObscured 改变 mViewFlags 的值,同时event返回的Flags 值与FLAG_WINDOW_IS_OBSCURED 运算后不为0,正常情况下, onFilterTouchEventForSecurity() 返回值为 true。 我们注意看一下 if 里面的代码,先对 mListenerInfo做一个非空判断,
mListenerInfo是什么呢? ListenerInfo 是个静态内部类,存储各种点击和滑动事件的回调,比如 setOnClickListener 点击事件等

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }
    
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
        
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

我们的事件回调,都在它里面存储着,这时候会先把 mOnTouchListener 取出来,看看是否设置了 setOnTouchListener(OnTouchListener l) 方法,注意下一步,做了个值校验(mViewFlags & ENABLED_MASK) == ENABLED, 只有这一步通过了,才会执行 li.mOnTouchListener.onTouch(this, event) 设置的触摸事件,onTouch(this, event) 如果返回了true,result = true;如果 onTouch(this, event) 返回值为false,没有消费,则 result 值不变,依然为 result = false。 下一步,只有result = false时才会执行onTouchEvent(event)方法,如果 onTouchEvent(event) 返回值为 true,则 dispatchTouchEvent(MotionEvent event) 接收到的值为 true,说明焦点触摸事件被消费了,如果onTouchEvent(event) 返回值为
false,则 dispatchTouchEvent(MotionEvent event) 接收到的值为 false,事件未消费。分析到这,我们知道了,一个view,如果设置了setOnTouchListener(OnTouchListener l) ,也重写了 onTouchEvent(event) 方法,它会先执行 OnTouchListener, 如果OnTouchListener回调返回值为false,才会执行 onTouchEvent(event),这里要注意一点小细节,就是
(mViewFlags & ENABLED_MASK) == ENABLED 这个校验,我们在开始的时候打印了个log日志,打印 isClickable() 和 isEnabled(),看isEnabled()源码
    public boolean isEnabled() {
        return (mViewFlags & ENABLED_MASK) == ENABLED;
    }
发现 (mViewFlags & ENABLED_MASK) == ENABLED 这个校验和 isEnabled() 方法代码一样,这也就是说,只有 isEnabled() 为true,才会执行 OnTouchListener 的回调,否则会直接执行 onTouchEvent(event) 方法,这是个小细节,注意一下。

按照我们上面控件里的代码,我们点击了一下控件,打印日志如下
    E/TouTestView: dispatchTouchEvent:  0
    E/TouTestView: onTouchListener:  0  false   true
    E/TouTestView: onTouchEvent:  0

先执行 dispatchTouchEvent(MotionEvent event) ,然后 onTouch(View v, MotionEvent event) ,最后 onTouchEvent(MotionEvent event),我们看到打印的action值为0,
先来看几个主要的值,
public static final int ACTION_DOWN             = 0;  初次接触到屏幕 时触发。
public static final int ACTION_UP               = 1;  离开屏幕 时触发。
public static final int ACTION_MOVE             = 2;  在屏幕上滑动 时触发,会多次触发。
public static final int ACTION_CANCEL           = 3;  被上层拦截 时触发。
public static final int ACTION_OUTSIDE          = 4;  不在控件区域 时触发。

这是我们再次点击,打印的还是这几个值,我们在控件上滑动,打印的日志还是这几个,这是怎么回事呢?如果我们把 onTouch(View v, MotionEvent event) 返回值改为 true,试试?

    private void initLister() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouchListener:  " + event.getAction() );
                return true;
            }
        });
    }
点击,打印为
  E/TouTestView: dispatchTouchEvent:  0
  E/TouTestView: onTouchListener:  0   
  E/TouTestView: dispatchTouchEvent:  1
  E/TouTestView: onTouchListener:  1   
如果滑动呢,再试一下
E/TouTestView: dispatchTouchEvent:  0
   E/TouTestView: onTouchListener:  0
   E/TouTestView: dispatchTouchEvent:  2
   E/TouTestView: onTouchListener:  2
   E/TouTestView: dispatchTouchEvent:  2
   E/TouTestView: onTouchListener:  2
   E/TouTestView: dispatchTouchEvent:  2
   E/TouTestView: onTouchListener:  2
   E/TouTestView: dispatchTouchEvent:  1
   E/TouTestView: onTouchListener:  1
我们知道,0代表 按下,1代表抬起,2代表滑动,这次是正常的,那为什么之前不行呢?把代码还原到之前的样式,onTouch(View v, MotionEvent event) 返回为false,修改onTouchEvent(MotionEvent event) 里面的打印代码,
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean is = super.onTouchEvent(event);
        Log.e(TAG, "onTouchEvent:  " + event.getAction() +"    " + is);
        return is;
    }
按下,滑动,打印日志为
   E/TouTestView: dispatchTouchEvent:  0
   E/TouTestView: onTouchListener:  0
   E/TouTestView: onTouchEvent:  0    false
看来down的时候 onTouchEvent(MotionEvent event)  里面返回的是 false,所以导致了没有后面的move和up事件了,莫非 View 的 onTouchEvent(MotionEvent event) 返回值为false吗?我们看一下它的简略源码

    public boolean onTouchEvent(MotionEvent event) {
        ...
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
                        ...
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                    ...
                    break;

                case MotionEvent.ACTION_DOWN:
                    ...
                    setPressed(true, x, y);
                    checkForLongClick(0);
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        setPressed(false);
                    }
                    break;
            }

            return true;
        }

        return false;
    }

这个是简化过后的代码,一些细节被删除,我们主要看剩余的这一部分。 我们看到if代码里,这里如果没进去,则直接返回了return false,我们看看之前的触发事件只有down事件很大可能是因为没走进if的判断语句。我们看看if判断语句是什么:
                (viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE
仔细看,会发现一个问题,这不就是变相的 
    public boolean isClickable() {
        return (mViewFlags & CLICKABLE) == CLICKABLE;
    }
    public boolean isLongClickable() {
        return (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
    }
    public boolean isContextClickable() {
        return (mViewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    }
上面的if语句中的判断条件,其实就是这三个方法的判断,文章的开头,我们打印了 isClickable() 为 false,此时如果打印后面两个方法,也都是false,怎么才能改变它的值呢?我们重新看 setOnClickListener(@Nullable OnClickListener l) 、 setOnLongClickListener(@Nullable OnLongClickListener l) 事件,看看方法中,会与运算,判断有没有这个值,如果没有,就赋值,以点击事件为例

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setClickable(boolean clickable) {
        setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
    }
也就是说,如果我们给view设置一个点击事件的回调,那么 isClickable() 为 true,同理,也适用于 isLongClickable() 和 isContextClickable()。isLongClickable() 是长按点击事件,这个好理解,isContextClickable() 应该是插入外部设备,比如鼠标,是否设置它的回调点击事件。在这,重点关注  isClickable() 和 isLongClickable()。
    给view添加点击事件和长按点击事件

    private void initLister() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouchListener:  " + event.getAction() );
                return false;
            }
        });
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "onClick: ");
            }
        });

        setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                Log.e(TAG, "onLongClick: ");
                return false;
            }
        });
    }
这时候 onTouchEvent(MotionEvent event) 就可以走到if的判断语句里面了,我们整体看一下,发现只要走到if里面,不管里面怎么判断,switch 判断后,返回的结果必然是returntrue; 也就是说,我们按下后,onTouchEvent(MotionEvent event) 必然是返回 true,我们先不分析代码,看一下打印的日志,点击一下控件,打印结果为
   E/TouTestView: dispatchTouchEvent:  0
   E/TouTestView: onTouchListener:  0
   E/TouTestView: onTouchEvent:  0
   E/TouTestView: dispatchTouchEvent:  1
   E/TouTestView: onTouchListener:  1
   E/TouTestView: onTouchEvent:  1
   E/TouTestView: onClick: 
我们发现有 down 事件,也有 up 事件,也有点击事件;如果我们在控件上滑动一下,肯定也有 move 事件,这个大家可以自己试一下。我们这时候继续分析代码,首先是Down事件MotionEvent.ACTION_DOWN 中,我们设置了长按点击事件 checkForLongClick(0); CheckForLongPress 是个 Runnable,把长按点击事件的回调包了一层,然后延迟500毫秒执行,可以理解为Handler的延迟执行runnable操作即可,这就是down中做的事情; MotionEvent.ACTION_MOVE 中,如果滑动达到一定的标准,并且在500毫秒内,runnable 还没被执行,这时候就会把 runnable 的长按点击事件回到给取消掉,就不会触发长按事件; MotionEvent.ACTION_CANCEL 就简单了,只要是在时间内,取消长按的runnable,同上;重点看看 MotionEvent.ACTION_UP ,手指抬起时,会有个if判断,if判断里面是view点击事件的回调,PerformClick 也是个 Runnable,最终也会执行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;
    }
原来我们的点击事件是在UP的时候触发的,但仔细看,触发之前有个if (!mHasPerformedLongPress && !mIgnoreNextUpEvent)判断, mIgnoreNextUpEvent 默认为false,这个可以先不管,看看 mHasPerformedLongPress,意思是是否已经消费了长按点击事件,默认是false,我们看看它是哪里赋值的。从前面的action_down中可以看到,如果500毫秒后执行了长按点击事件,会执行 CheckForLongPress 类里的 run() 方法,
    public void run() {
        if (isPressed() && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            if (performLongClick()) {
                mHasPerformedLongPress = true;
            }
        }
    }

    public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

performLongClick() 在正常情况下可以直接简化为 onLongClick(View v) 方法的返回值,也就是说 mHasPerformedLongPress 对应的是 onLongClick(View v)返回值,如果返回值为false,则 mHasPerformedLongPress = false,同理,如果onLongClick(View v)返回为true,则 mHasPerformedLongPress = true。我们再次看up时里面的代码,就明白 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) 的意思了,mHasPerformedLongPress 会在  MotionEvent.ACTION_DOWN 和 MotionEvent.ACTION_CANCEL 或 checkForLongClick(int delayOffset) 时候重新置为false,mHasPerformedLongPress 的作用域更接近一个焦点事件,down 、move 、up 范围内。如果长按点击事件 onLongClick(View v) 返回为false,onClick(View v) 在长按松手时会被执行;反之,则不会。如果按下了又move了,关键是看 onLongClick(View v)有没有执行,才能决定 onClick(View v)  是否执行。

总结,view中先执行 dispatchTouchEvent(MotionEvent event),接着是设置的触摸回调事件 setOnTouchListener(),如果它的 onTouch(View v, MotionEvent event) 方法返回false,则执行 view 本身的 onTouchEvent(MotionEvent event) 方法,setOnLongClickListener() 长按点击事件时Down时就开始准备了,setOnClickListener() 点击事件是在Up的时候执行的。如果 ACTION_DOWN 的时候,onTouch(View v, MotionEvent event) 或 onTouchEvent(MotionEvent event) 方法返回了false,导致 dispatchTouchEvent(MotionEvent event) 返回的值也是false,那么抱歉,不会有后续的  ACTION_MOVE 和 ACTION_UP 事件了,只有 MotionEvent.ACTION_DOWN 为true时,才会有后面的一系列事件传递。

关于 (viewFlags & CLICKABLE) == CLICKABLE ||
       (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
       (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE
的值,View 、 TextView 、 ImageView 他们的默认值都是 false, 而 Button 、 ImageButton 的默认值为 true,所以如果想让焦点能走个全程,则可以使用 Button 、 ImageButton代替TextView 、 ImageView,或者直接给 TextView 、 ImageView 设置个setOnClickListener()点击事件就可以了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值