Android中GUI系统的Event路由机制

前两天在论坛上看到有人发了一个帖子,询问一个Android GUI Event处理的问题:有一个LinearLayout,里面有很多的child view,他问如何监听这个LinearLayout的Click事件?他的做法是:

        setClickable(true);

        setOnClickListener(listener);

        最后他发现listener中的回调函数根本不会被调用。

        事实上,在Android的GUI系统的中,硬件触发的Event(KeyEvent、 TouchEvent、 TrackballEvent等)最开始是Window拿到了,Window将Event转发给了前台的Activity。但是Activity同样不能马上自己处理掉,而是将Event传递给了它里面的ContentView。

        如果ContentView是一个容器View(继承自ViewGroup类型),它一般都是先判断这个Event落在哪一个Child View上。然后将该Event Dispatch给这个Child View了。

        我们以Touch Event为例,看看ViewGroup类中的dispatchTouchEvent()函数:

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }
        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) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                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)) {
                            // offset the event to the view's coordinate system
                            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))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            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 have a target, see if we're allowed to and want to intercept its
        // events
        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)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }
        if (isUpOrCancel) {
            mMotionTarget = null;
        }
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        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);
    }

我们可以看到ViewGroup通过调用Child View的dispatchTouchEvent()函数,将Event按照View树状结构一级一级的Dispatch下去(Child View, GrandChild View ……)。

        最终,最里层的Child View拿到了这个Event,而它又没有Child View了。于是它就开始处理Event(也就是响应事件)。

        我们还以Touch Event为例,看看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 (!onFilterTouchEventForSecurity(event)) {
            return false;
        }
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

        首先,它是判断这个Touch Event是否安全,如果不安全,返回。

        然后,判断是否设置了Touch Event监听器,如果设置了,就调用监听器的OnTouchListener的处理函数,返回。

        如果没有Touch Event监听器,就调用自身定义onTouchEvent()方法,返回。

        在这种事件处理函数中,都要返回一个boolean类型的值。如果返回了false,它的父容器还能再次拿到了事件的。如果返回了true,也就相当于告诉它的父容器:这事我管了,你就别过问了。

        因此,Activity拥有的处理Event权限是最低级别的。

        我们同时也会注意到,除了硬件触发的Event(KeyEvent、 TouchEvent、 TrackballEvent等)外,还存在一些Event,如:Click、LongPress、DoubleClick等。这些Event通常是人为的将“硬件触发的Event”封装得来的。例如Click Event就是使用了TouchEvent 中的MotionEvent.ACTION_UP和MotionEvent.ACTION_DOWN封装而来的。默认情况下,它们使得onTouchEvent处理函数返回了true。因此,Linearlayout的孩子们一旦设置了自己的Click事件监听器,Linearlayout本身就拿不到事件了,因为它的孩子已经进行了处理。这正是论坛中问题的答案所在!

        不过,这里也存在一种解决方案。我们在看ViewGroup的dispatchTouchEvent()函数时,应该注意到ViewGroup在将Event Dispatch给Child View之前,先调用了自己的onInterceptTouchEvent()函数(Intercept就是拦截的意思)。因此,我们可以通过重写Linearlayout的onInterceptTouchEvent()方法,来拦截我们想要处理的Event,它会在事件传给孩子之前被调用的。

 

posted @ 2015-10-09 18:00  天津大学  阅读(206)  评论(0编辑  收藏  举报