View的事件分发机制


事件分发是安卓中一个很重要的机制,是实现用户交互的必要存在。那么啥是事件呢,事件就是用户与应用UI交互的动作。通俗点说,就是你对手机触摸屏幕摸摸点点的各种猥琐操作。一个完整的触摸事件分为按下,移动,抬起三个过程。也可能没有移动的过程。那么今天咱们就通过一个简单的点击事件的流程来了解一下View的事件分发是如何实现的。

首先,我们得知道一个必要的知识点,那就是事件分发是从View的dispatchTouchEvent()方法开始的。那么咱们就来看看View里面的dispatchTouchEvent()方法是怎么写的。研究一下当初设计这个的作者的实现思想。这次咱们的代码版本是比较新的API24 android N 版本的。代码贴上:


<span style="font-family:Microsoft YaHei;font-size:18px;">    /**
     * 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;
    }  </span>

API24的dispatchOnTouchEvent()方法相比旧一些的API来说,多了一些代码,比如校验事件的完整性等等。在上面的代码看来,代码还不是很多,虽然有些看不懂的,但咱们挑懂的看也足以让我们了解清楚它的机制。

首先,判断一下InputEventConsistencyVerifier的实例对象mInputEventConsistencyVerifier是否不等于null,如果不等于null,那就去执行它里面的onTouchEvent方法。我也不知道这个方法到底干了什么,不过InputEventConsistencyVerifier作用就是对输入事件的完整性做一个检查,检查事件的ACTION_DOWN 和 ACTION_UP 是否一一配对。很多同学可能在Android Logcat 里看到过以下一些类似的打印:"ACTION_UP but key was not down." 就出自此处。(备注,这段InputEventConsistencyVerifier作用是我Copy来的,看我多么诚实)所以我们就知道了,这里估计是用来做一个事件完成性的检查的。所以,咱们继续往下看。

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

 

通过注释可以知道,这是筛选应用安全策略的触摸事件,嗯。好像知道了也没啥卵用。至少听上去跟我们的事件分发好像没什么关系。那咱们就不管它了。继续往下走!

接下来终于到关键部分的代码了:


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

这么第一行代码咱们就忽略掉,应该不是什么重要的信息。好吧,其实是我实在翻译不出来。咱们往下看,2行,用一个引用接收ListenerInfo类型的成员变量。然后首先去判断它是否不等于null,继而判断他的OnTouchListener对象是否也不等于null,最后如果都成立即不等于空,那就去判断接下来一个,mViewFlags & ENABLED_MASK 的结果是否等于 ENABLED,这里就涉及一个很巧妙的构思了。用&来判断是否存在这个状态,这里说到底就是判断当前这个view是否enable的。简单解释一下这个巧妙的构思,因为不是咱们文章内容重点。平时我们区分状态的时候,比如打开状态OPEN,或者关闭状态CLOSE。那么一般的写法就是用两个常量分别代表这两种状态,然后判断的时候就去用多个if else一个个判断。这里呢,也是用两个常量去分别代表。可是它们常量的值却不是普通的1,0;而是1<<0,1<<1;这样。那么1<<0也就是0..0000001,0..0000010,这里因为int类型长度是32位,所以就是有32个位置可以存,每个位置表示一种状态,假如咱们的view的状态值是0..0000011的话,与上0..0000001结果就是0..0000001。结果是一样的,就可以断定它是存在这么一种状态,咱们用的时候就不需要用多个if else来判断了,而是用一个if去判断即可,节省的大量的代码。而且也是一种非常高逼格的写法。好。话不多说,说了简单说明就简单说明。咱们继续看。

如果以上3种条件都满足,那么就会去执行li.mOnTouchListener.onTouch(this,event)这句话,根据它的返回值来决定这个if条件是否成立,如果成立,那么就直接返回了true;这么来,第一个流程就走完了。这里的事件处理就交由OnTouchListener 里的onTouch方法去处理了。其实,如果你的mOnTouchListener对象不为空,那么就肯定是通过

<span style="font-family:Microsoft YaHei;font-size:18px;">public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
}</span>

去设置了它的值,所以它不会为空,onTouch也是处理触摸事件的,既然你有了一个处理事件的入口,那么接下来的onTouchEvent也就没有执行的必要啦。一口饭完整的饭只能给一个人吃。对吧。所以就直接return了,下面的

<span style="font-family:Microsoft YaHei;font-size:18px;">if (onTouchEvent(event)) {
                return true;
}</span>

就自然没法执行到了。

然后呢,ouTouch方法里面的代码,也是你通过setOnTouchListener传入的匿名内部类OnTouchListener 里面去实现了。至于如何实现的,那就看你代码怎么写了。

接下来,我们看看如果不符合第一个if的判断条件,代码往下走会怎么样,也就是

<span style="font-family:Microsoft YaHei;font-size:18px;">if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)</span>

这句话有任何一个结果是false,那么就不会进去这里面,然后往下走到了

<span style="font-family:Microsoft YaHei;font-size:18px;">if (onTouchEvent(event)) {
                return true;
}</span>

这里来。那么咱们就又点击进去这个onTouchEvent方法里面去看看。

<span style="font-family:Microsoft YaHei;font-size:18px;">    /**
     * 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;
}</span>

这里的代码不是一般的多了,咱们来好好研读研读。这种代码怎么看呢,首先看看注释,注释是一个表明作者写这个方法的目的或者一些他的思想,所以很重要的。首先第一句说明了,实现这个方法去处理屏幕触摸事件。那这么一来,我就突然很好奇onTouch方法的注释是怎么写的了

        /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onTouch(View v, MotionEvent event);

这里写着,这个onTouch是在触摸事件分发到view的时候被调用,这允许监听者在触摸事件发送到这个目标view之前做出一些响应。好吧,这也间接说明了onTouch方法是比onTouchEvent方法早一步执行的。

咱们回到上面的代码,矮油,好长啊。没事,咱们坚持住,继续往下看,下面还有个小提示,如果这个方法用来检测点击操作,就是单击事件咯。那么这个作者建议我们去实现和调用这个performClick()方法。这样确保系统行为的一致性。

接下来就走进这个方法去看看。看源码得先挑看得懂的看,然后在扩散去理解。咱们看看有哪些是我们看得懂的。咱们可以看到37行的地方,有个判断viewFlags,也就是view的状态标志。判断它是否存在点击状态可点击,或者长点击。如果当前View是可以点击的,那么就会走39行的switch判断里面去,然后又经过了一堆堆的判断,走到了咱们有些熟悉的71行,performClick()方法里面去了,咱们点进去看看。

<span style="font-size:18px;">    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener 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;
    }</span>

又经过一些判断,比如如果mListenerInfo不为null,最后条件满足就会调用它的onClick方法,这么一来,点击事件就被执行到了。mListenerInfo又怎么来的呢?

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

一番寻找,就会发现,它是通过setOnClickListener给设置上的,这么说来,onClick方法的具体操作也是由我们来决定的。所以,当你给一个控件通过setOnClickListener 设置了点击事件监听的时候,屏幕被点击时,事件一层层的传递,最后到了你的这个view的dispatchOnTouchEvent方法里面去,然后又经过我们上面的那么一大堆操作,最后调用到你设置好的OnClickListener的onClick方法。这么一来,我们的View的整个事件分发的流程也就清楚了。dispatchOnTouchEvent到onTouchEvent再到具体的onClick等各种事件处理方法。

最后还有个重要的点要说明。如果你给一个控件注册了onTouch事件,每次点击这个控件的时候就会触发一系列的DOWN,MOVE,UP的action,如果你在DOWN的action里面返回了false,那么接下来的MOVE,UP等所有在DOWN之后的action都不会被执行到了,只有上一个action返回了true才会执行下一个action。这里咱们来两个例子,分别给button和imageview注册一个onTouch事件

首先来个imageView的

<span style="font-size:18px;">imageView.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("Sakura", "actionId" + event.getAction());  
        return false;  
    }  
});  </span>

结果:

 

在我疯狂的点击4次,加很多次的拖动下,它都只能是DOWN被执行到。DOWN的action对应值就是0。咱们接下来看button的。

<span style="font-size:18px;">button.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("Sakura", "actionId" + event.getAction());  
        return false;  
    }  
});</span>

结果:

 

哎呦,卧槽。见鬼了,这怎么都可以执行,这不打我脸么。其实一切都在我的掌控当中,button有些不太一样。我们回到onTouchEvent方法里面去看看。因为你在onTouch里面返回了false之后,在dispatchOnTouchEvent方法里面,就肯定会走onTouchEvent方法。那么在onTouchEvent方法37行的位置,判断如果这个view是可点击的,那就会走到里面去。然后咱们使劲往下拖,在139行的位置,就会看到一个return true;这么一来,就可以知道了,如果当前控件是可点击的,那么它就会默认给我们return true。从而使下面的action可以被执行到。那么imageView呢,默认是不可点击的,所以就不会走到里面去,该false还是false。符合我们所说的。

这么一来,View的事件分发的所有事,咱们都讲完啦。想必大家也比之前知道了不少东西。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值