您的位置:首页 > 移动开发 > Android开发

Android 的事件分发机制(一)---view的事件传递

2015-12-02 16:05 666 查看
最近一直在学习有关android事件分发机制的问题,翻阅了各个大牛的书籍以及网上各位码神的博客,自己又写了demo测试并仔细看过源码之后终于算是搞的差不多了,为了防止遗忘,并分享给各位跟我一样懵懂的小伙伴们作为参考,决定写下来作为笔记,第一次写博客,文笔不好请见谅,如有不对之处欢迎指正。

言归正传,我们大家可能都做过需要判断手指在控件上按下移动离开这样类似的功能,我们一般是加setOnTouchListener,即触摸监听,或者有时候我们也会重写onTouchEvent方法,但是这两种监听有什么区别呢?如果都有的话,谁先谁后呢?这两种方法都是带有布尔类型的返回值的,那么true和false对结果有什么影响呢?带着这些问题我们来简单写个demo测试下:

自定义一个View,并重写dispatchTouchEvent和onTouchEvent方法,我直接重写了一个Button

public class MyButton extends Button {
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.v("EventTest", "dispatchTouchEvent ");
        return super.dispatchTouchEvent(event);
    }
	
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.v("EventTest", "onTouchEvent (MyButton)");
        return super.onTouchEvent(event);
    }
}


主activity的部分代码,我们给按钮加了touch事件监听,并加了click监听

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.v("EventTest", "onClick");
    }
});
button.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.v("EventTest","onTouch (MyButton)"+event.getAction());
        return true;
    }
});


运行程序,打印日志如下:



首先我们要明白分析事件分发机制其实就是分析MotionEvent对象的传递过程,即点击事件,当MotionEvent产生后,系统会把这个事件在View之间进行传递,这个过程就是分发,典型的事件类型有三种:

ACTION_DOWN--------------手指刚接触屏幕的一瞬间

ACTION_MOVE--------------手指在屏幕上移动的时候

ACTION_UP--------------手指在屏幕上松开的瞬间

一般情况下,一个完整的事件序列会从ACTION_DOWN开始,中间经历个数不等的ACTION_MOVE,最后是ACTION_UP事件,通过以上的日志,我们可以看到点击事件先是触发了dispatchTouchEvent方法,然后是onTouch方法(日志中0代表的是down,2代表的是move,1代表的是up事件),但是却没有执行我们重写的onTouchEvent方法和设置的onclickListener方法,为什么呢,这是因为我们的onTouch方法是带有返回值的,返回值代表是否消耗当前事件,上面的代码返回了true,所以事件被onTouch消耗了,后续的方法就得不到执行。我们继续测试,改一下上面的返回值为false,重新运行程序,日志如下:



这时候我们可以看到onTouchEvent执行了,并且在最后还执行了onClick事件。

这里我们可以总结一下,View的事件执行过程:dispatchTouchEvent->onTouch(如果有的话)-> onTouchEvent->onclick(如果有)

(这是单纯的一个View的事件传递,不包含VewGroup,ViewGroup的后面我们会再做分析)

下面我们从源码上面来分析一下,首先一个View中如果有事件传递,那么先执行的肯定是dispatchTouchEvent方法,所以我们先看这个方法

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}
代码也不多,我们直接看第13行,if条件里面执行了onFilterTouchEventForSecurity(event)这么一个方法,我们进去看一下

/**
 * Filter the touch event to apply security policies.
 *
 * @param event The motion event to be filtered.
 * @return True if the event should be dispatched, false if the event should be dropped.
 *
 * @see #getFilterTouchesWhenObscured
 */
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;
}


简单来说,就是通过判断点击的时候的View是否被遮住来决定是否舍弃掉当前事件

我们继续分析if代码块里面,第16行第一个条件,判断li是否为空,这是个啥呢,通过源码我们可以看出来这是一个内部类,里面封装了各种事件监听,具体代码就不贴了,这个大家可以通过源码看到,是不是空我们先放下看下一个,第二个条件就是li.mOnTouchListener,通过名字就能看出来是我们设置的OnTouchListener,我们来看一下setOnTouchListener这个方法做了什么:

/**
 * Register a callback to be invoked when a touch event is sent to this view.
 * @param l the touch listener to attach to this view
 */
public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}


里面代码很简单,我们继续看:

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}


这下明白了,就是将传入的OnTouchListener赋值给内部类ListenerInfo的其中一个mOnTouchListener属性上面,这样第一个和第二个条件都已经成立了,第三个条件的意思是当前view是否是enable,这个默认都是enable的,所以成立,看第四个,重点来了,这不就是我们设置的onTouchListener里面的方法吗,如果返回了true,那么代码到这里就会直接return true,不会继续执行了,就相当于消耗了事件,如果返回了false,那么执行下面的代码,下面就是onTouchEvent方法了,我们看看onTouchEvent方法的源码:

/**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == 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));
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            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);
                   }

                    if (!mHasPerformedLongPress) {
                        // 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();
                }
                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();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();

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


代码有点长,不过我们主要看流程,不必纠结每一句代码,onTouchEvent里面肯定有对down、move和up事件的处理,就看这一部分就可以了,源码中37行可以看到,我们需要首先判断控件是否是可点击的或者是否可以长按,view的长按默认都是false的,所以一般判断只要可以点击那么就可以进入代码块中,onclick事件是在手指点击控件离开屏幕的时候触发的,可以判断应该在UP事件处理中,通过源码71行我们可以看到一个performClick方法,我们进去看看:

/**
 * Call this view's OnClickListener, if it isdefined.  Performs all normal
 * actions associated with clicking: reportingaccessibility event, playing
 * a sound, etc.
 *
 * @return True there was an assignedOnClickListener that was called, false
 *        otherwise is returned.
 */
public boolean performClick() {
   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
 
    ListenerInfo li = mListenerInfo;
    if (li != null &&li.mOnClickListener != null) {
       playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        return true;
    }
 
    return false;
}


这里15行我们可以看到执行了我们熟悉的onClick方法,更是确定了onClick方法是最后才执行的
回到onTouchEvent方法中,if代码块执行完之后,直接就return true返回了,这说明我们的View的OnTouchEvent方法是默认消耗点击事件的,那么如果onTouchEvent返回了false会是什么情况呢,我们来试一下,我们直接修改我们重写的onTouchEvent,使之返回了false,这里我重写了运行结果如下:



从日志我们可以看到,如果onTouchEvent返回了false,那么后续事件就不再交给这个View执行了,通俗点说就是,一旦某个View处理事件,如果它不消耗ACTION_DOWN事件(就是onTouchEvent返回了false),那么后续的ACTION_MOVE和ACTION_UP事件都不会继续给这个View执行了,那么事件消失了吗,当然没有,事件其实是交给了当前View的父元素去处理,即父元素的onTouchEvent会被调用,这个就是涉及到了ViewGroup的事件分发了,这个由于篇幅原因下次再分析。
我们可以继续测试下,如果down事件返回了true,其他的返回了false,会是什么样,修改一下代码

@Override
    public boolean onTouchEvent(MotionEventevent) {
 
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.v("EventTest","onTouchEvent---down---(MyButton) ");
                return true;
            case MotionEvent.ACTION_MOVE:
                Log.v("EventTest","onTouchEvent---move---(MyButton) ");
                break;
            case MotionEvent.ACTION_UP:
                Log.v("EventTest","onTouchEvent---up---(MyButton) ");
                break;
        }
        return false;
    }


结果如下:



可以看到如果down事件返回了true,其他的返回了false,那么不影响当前view收到后续的事件,但是这里要注意,这个点击事件最终到了哪里呢,这里我可以先说一下,消失了的点击事件会传递给activity处理,我重写一下activity的dispatchTouchEvent方法和onTouchEvent方法,运行结果如下:



可以看到,当Button的onTouchEvent在MOVE和UP事件的时候返回了false,Activity的onTouchEvent就会执行。

现在我们来总结下:
1、同一个事件序列指的是从手指接触屏幕一直到手指离开屏幕的整个过程中产生的所有事件,一般以一个down事件开始,中间有若干个move事件,最后以up事件结束。
2、view的onTouchEvent方法是默认消耗事件的(返回true),除非这个view是不可点击的(clickable和longclickable都为false),view的longclickable是默认为false的。
3、View的enable属性是不影响onTouchEvent默认返回值的,onTouchEvent默认返回值是只要clickable和longclickable有一个为true,那么就默认返回true。
4、onclick要想发生需要收到down事件以及up事件,当然view是可点击的。
5、当View开始处理事件,如果不消耗事件(直接返回了false),那么后续的事件就不会交给此View处理,这时候当前View的父元素的onTouchEvent就会被调用。
6、当View开始处理事件,如果不消耗除了ACTION_DOWN以外的事件(down返回了true,其他返回了false),那么View仍然可以接收到后续事件,最终那些返回false 的事件就会传递给activity来处理。
走过路过的朋友,欢迎留言,一起探讨,共同进步。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: