Android事件分发机制研究

写个Demo,分别调用一个Button的onTouch和onClick函数:

        bt1 = (Button) findViewById(R.id.bt1);
        bt1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(TAG, "onTouch,event action:" + event.getAction());
                return false;
            }
        });

        bt1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick...");
            }
        });

在手机上做点击操作,结果如下:

01-16 06:41:54.078 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:0
01-16 06:41:54.173 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:1
01-16 06:41:54.217 7507-7507/com.acxingyun.touchevent I/TouchActivity: onClick...

0代表按下,1代表抬起:
这里写图片描述
后面还有拖动等其它定义:
这里写图片描述
试试拖动操作:

01-16 06:49:16.708 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:0
01-16 06:49:16.833 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:16.848 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:16.865 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:16.899 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:16.956 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:16.987 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:2
01-16 06:49:17.056 7507-7507/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:1
01-16 06:49:17.088 7507-7507/com.acxingyun.touchevent I/TouchActivity: onClick...

action_down后,就是一连串的action_move,最后action_up,就是点击,拖动,然后抬起。
改下代码,onTouch返回true:

bt1 = (Button) findViewById(R.id.bt1);
        bt1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(TAG, "onTouch,event action:" + event.getAction());
                return true;
            }
        });

        bt1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick...");
            }
        });

结果:

01-16 07:12:19.293 9767-9767/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:0
01-16 07:12:19.355 9767-9767/com.acxingyun.touchevent I/TouchActivity: onTouch,event action:1

为啥没有再回调onClick了?
Andorid中,任何一个控件被点击后,都会调用该控件的dispatchTouchEvent,Button没有这个方法,但依次网上查找,从父类TextView到View,终于找到了这个方法,在这个方法里摘取部分代码:

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

mOnTouchListener是在设置监听的时候就赋值了,view默认是ENABLED的,即(mViewFlags & ENABLED_MASK) == ENABLED,mOnTouchListener.onTouch(this, event)意思是调用mOnTouchListener的onTouch函数,这个函数是在设置监听的时候就override了:

bt1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(TAG, "onTouch,event action:" + event.getAction());
                return true;
            }
        });

如果override返回true,一系列条件成立,resule = true,就不会进入下面onTouchEvent了:

            if (!result && onTouchEvent(event)) {
                result = true;
            }

再看看onTouch的源码:
这里写图片描述
当Touch事件分配到了一个view上调用,如果监听消费了这个touch事件,返回true。所以一旦override了控件的onTouch,返回true,表示onTouch事件被消费了,不会再进入onTouchEvent,而onClick正是在onTouchEvent中:

public boolean onTouchEvent(MotionEvent event) {
        ..................

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    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:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }

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

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

在li.mOnClickListener.onClick(this);这里,回调了View的onClick函数。

有个疑问,一系列的操作,action_down,action_move,和action_up是如何相继触发的???

ViewGroup的事件分发机制:

ViewGroup继承View,是多个View的集合,LinearLayout、RelelaytiveLayout这些布局都是继承自ViewGroup。
之前说过,点击一个View时首先会调用它自身的dispatchTouchEvent,当我们点击一个View时,其实是先调用它的父布局,也就是ViewGroup的dispatchTouchEvent:

    public boolean dispatchTouchEvent(MotionEvent ev) {  
        final int action = ev.getAction();  
        final float xf = ev.getX();  
        final float yf = ev.getY();  
        final float scrolledXFloat = xf + mScrollX;  
        final float scrolledYFloat = yf + mScrollY;  
        final Rect frame = mTempRect;  
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
        if (action == MotionEvent.ACTION_DOWN) {  
            if (mMotionTarget != null) {  
                mMotionTarget = null;  
            }  
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
                ev.setAction(MotionEvent.ACTION_DOWN);  
                final int scrolledXInt = (int) scrolledXFloat;  
                final int scrolledYInt = (int) scrolledYFloat;  
                final View[] children = mChildren;  
                final int count = mChildrenCount;  
                for (int i = count - 1; i >= 0; i--) {  
                    final View child = children[i];  
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                            || child.getAnimation() != null) {  
                        child.getHitRect(frame);  
                        if (frame.contains(scrolledXInt, scrolledYInt)) {  
                            final float xc = scrolledXFloat - child.mLeft;  
                            final float yc = scrolledYFloat - child.mTop;  
                            ev.setLocation(xc, yc);  
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                            if (child.dispatchTouchEvent(ev))  {  
                                mMotionTarget = child;  
                                return true;  
                            }  
                        }  
                    }  
                }  
            }  
        }  
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                (action == MotionEvent.ACTION_CANCEL);  
        if (isUpOrCancel) {  
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
        }  
        final View target = mMotionTarget;  
        if (target == null) {  
            ev.setLocation(xf, yf);  
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
            return super.dispatchTouchEvent(ev);  
        }  
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
            final float xc = scrolledXFloat - (float) target.mLeft;  
            final float yc = scrolledYFloat - (float) target.mTop;  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            ev.setLocation(xc, yc);  
            if (!target.dispatchTouchEvent(ev)) {  
            }  
            mMotionTarget = null;  
            return true;  
        }  
        if (isUpOrCancel) {  
            mMotionTarget = null;  
        }  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        ev.setLocation(xc, yc);  
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            mMotionTarget = null;  
        }  
        return target.dispatchTouchEvent(ev);  
    }  

进来后,如果是action_down,先把mMotionTarget置为null,然后做个判断:

if (disallowIntercept || !onInterceptTouchEvent(ev)){
    ......
}

disallowIntercept表示是否拦截点击事件,默认false,就进入了后面:

onInterceptTouchEvent{
    return false;
}

返回false,就进入里面的逻辑:遍历ViewGroup中的childView,如果发现某个childView是当前点击的view,调用该view的dispatchTouchEvent:

                            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                            final float xc = scrolledXFloat - child.mLeft;  
                            final float yc = scrolledYFloat - child.mTop;  
                            ev.setLocation(xc, yc);  
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                            if (child.dispatchTouchEvent(ev))  {  
                                mMotionTarget = child;  
                                return true;  
                            }  

child.dispatchTouchEvent(ev)返回true,则退出这个ViewGroup的dispatchTouchEvent;而child.dispatchTouchEvent(ev)又会进入到这个view的onTouch中,只要是clickable的,child.dispatchTouchEvent都会返回true;不是clickable的,child.dispatchTouchEvent返回false,如果所有child.child.dispatchTouchEvent返回false,继续往下执行:

        final View target = mMotionTarget;  
        if (target == null) {  
            ev.setLocation(xf, yf);  
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
            return super.dispatchTouchEvent(ev);  
        }  

因为mMotionTarget为null,所以这里进入super.dispatchTouchEvent(ev),因为ViewGroup继承自View,这里就进入了View.dispatchTouchEvent,就是执行这个View自身的点击时间流程,就和普通View的点击流程一样了。
ViewGroup的事件分发流程:
这里写图片描述
View的事件分发流程:
这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值