欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android事件分发与责任链模式

程序员文章站 2022-06-22 10:58:01
一、责任链模式 责任链模式是一种行为模式,为请求创建一个接收者的对象链.这样就避免,一个请求链接多个接收者的情况.进行外部解耦.类似于单向链表结构。 优点: 1. 降低耦合度。它将请求的发送者和接收者解耦。 2. 简化了对象。使得对象不需要知道链的结构。 3. 增强给对象指派职责的灵活性。通过改变链 ......

一、责任链模式

责任链模式是一种行为模式,为请求创建一个接收者的对象链.这样就避免,一个请求链接多个接收者的情况.进行外部解耦.类似于单向链表结构。

优点:

1. 降低耦合度。它将请求的发送者和接收者解耦。

2. 简化了对象。使得对象不需要知道链的结构。

3. 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次 序,允许动态地新增或者删除责任。

4. 增加新的请求处理类很方便。

缺点:

1. 不能保证请求一定被接收。

2. 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。

3. 可能不容易观察运行时的特征,有碍于除错。

责任链的模式在事件分发场景方面的原理:

一般我们理解的事件分发的模式如下(传统模式):

Android事件分发与责任链模式

使用责任链模式直接将message丢到链中,让他们自己匹配.

Android事件分发与责任链模式

二、android 事件分发传递机制

1. view事件传递分发层级结构

 a). 事件收集之后最先传递给 activity, 然后依次向下传递,大致如下:

activity -> phonewindow -> decorview -> viewgroup -> ... -> view

这样的事件分发机制逻辑非常清晰,可是,你是否注意到一个问题?如果最后分发到view,如果这个view也没有处理事件怎么办,就这样让事件浪费掉?当然不会啦。

 b). 如果没有任何view消费掉事件,那么这个事件会按照反方向回传,最终传回给activity,如果最后 activity 也没有处理,本次事件才会被抛弃:

activity <- phonewindow <- decorview <- viewgroup <- ... <- view

可以看到,这是一个非常经典的责任链模式,如果我能处理就拦截下来自己干,如果自己不能处理或者不确定就交给责任链中下一个对象。 这种设计是非常精巧的,上层view既可以直接拦截该事件,自己处理,也可以先询问(分发给)子view,如果子view需要就交给子view处理,如果子view不需要还能继续交给上层view处理。既保证了事件的有序性,又非常的灵活。

view点击事件分发有三个关键流程方法:

1.dispatchtouchevent:事件下发 --- view和viewgroup都有的方法

2.onintercepttouchevent:拦截下发的事件,并交给自己ontouchevent处理处理 ---viewgroup才有的方法

3.ontouchevent:事件上报 --- view和viewgroup都有的方法

以下是不同层级对事件的分发、拦截和消费的功能表:

Android事件分发与责任链模式

可以看到 activity 和 view 都是没有事件拦截的:

a). activity 作为原始的事件分发者,如果 activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。

b). view最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。

下图是点击view,事件传递但是都没有被处理,生成的一个完整的事件分发流程图:

Android事件分发与责任链模式

如果事件被view处理了,那么事件分发流程图应该如下:

Android事件分发与责任链模式

如果事件被viewgroup拦截处理了, 那么事件分发流程图应该如下:

Android事件分发与责任链模式

从上面的流程,我们可以概括android的事件分发机制为:责任链模式,事件层层传递,直到被消费。

三、q&a

上面我们讲解了一下android的事件分发机制,可能很多人会有疑惑,下面我们针对部分疑惑进行分析和说明:

1. 为什么 view 会有 dispatchtouchevent ?

答:我们知道 view 可以注册很多事件监听器,例如:单击事件(onclick)、长按事件(onlongclick)、触摸事件(ontouch),并且view自身也有 ontouchevent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchtouchevent,所以 view 也会有事件分发。

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 the event should be handled by accessibility focus first.
        if (event.istargetaccessibilityfocus()) {
            // we don't have focus or no virtual descendant has it, do not handle the event.
            if (!isaccessibilityfocusedvieworhost()) {
                return false;
            }
            // we have focus and got the event, then use normal event dispatch.
            event.settargetaccessibilityfocus(false);
        }

        boolean result = false;

        if (minputeventconsistencyverifier != null) {
            minputeventconsistencyverifier.ontouchevent(event, 0);
        }

        final int actionmasked = event.getactionmasked();
        if (actionmasked == motionevent.action_down) {
            // defensive cleanup for new gesture
            stopnestedscroll();
        }

        if (onfiltertoucheventforsecurity(event)) {
            if ((mviewflags & enabled_mask) == enabled && handlescrollbardragging(event)) {
                result = true;
            }
            //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;
            }
        }

        if (!result && minputeventconsistencyverifier != null) {
            minputeventconsistencyverifier.onunhandledevent(event, 0);
        }

        // clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an action_down but we didn't want the rest
        // of the gesture.
        if (actionmasked == motionevent.action_up ||
                actionmasked == motionevent.action_cancel ||
                (actionmasked == motionevent.action_down && !result)) {
            stopnestedscroll();
        }

        return result;
    }

2. view事件分发时各个方法调用顺序是怎样的?

a). 单击事件(onclicklistener) 需要两个两个事件(action_down 和 action_up )才能触发,如果先分配给onclick判断,等它判断完再交由其他相应时间显然是不合理的,会造成 view 无法响应其他事件,应该最后调用。(所以此调用顺序最后)

b). 长按事件(onlongclicklistener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要action_up,应该排在 onclick 前面。(onlongclicklistener > onclicklistener)

c). 触摸事件(ontouchlistener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)

d). view自身处理(ontouchevent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onclicklistener 后面。(ontouchlistener > onclicklistener)

所以事件的调度顺序应该是 ontouchlistener > ontouchevent > onlongclicklistener > onclicklistener

3. viewgroup 的事件分发流程又是如何的呢?

在默认的情况下 viewgroup 事件分发流程是这样的。

a). 判断自身是否需要(询问 onintercepttouchevent 是否拦截),如果需要,调用自己的 ontouchevent。

b). 自身不需要或者不确定,则询问 childview ,一般来说是调用手指触摸位置的 childview。

c). 如果子 childview 不需要则调用自身的 ontouchevent。

viewgroup的dispatchtouchevent源码:

@override
    public boolean dispatchtouchevent(motionevent ev) {
        if (minputeventconsistencyverifier != null) {
            minputeventconsistencyverifier.ontouchevent(ev, 1);
        }

        // if the event targets the accessibility focused view and this is it, start
        // normal event dispatch. maybe a descendant is what will handle the click.
        if (ev.istargetaccessibilityfocus() && isaccessibilityfocusedvieworhost()) {
            ev.settargetaccessibilityfocus(false);
        }

        boolean handled = false;
        if (onfiltertoucheventforsecurity(ev)) {
            final int action = ev.getaction();
            final int actionmasked = action & motionevent.action_mask;

            // handle an initial down.
            if (actionmasked == motionevent.action_down) {
                // throw away all previous state when starting a new touch gesture.
                // the framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, anr, or some other state change.
                cancelandcleartouchtargets(ev);
                resettouchstate();
            }

            // check for interception.
            final boolean intercepted;
            if (actionmasked == motionevent.action_down
                    || mfirsttouchtarget != null) {
                final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0;
                if (!disallowintercept) {
                    intercepted = onintercepttouchevent(ev);
                    ev.setaction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // there are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // if intercepted, start normal event dispatch. also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mfirsttouchtarget != null) {
                ev.settargetaccessibilityfocus(false);
            }

            // check for cancelation.
            final boolean canceled = resetcancelnextupflag(this)
                    || actionmasked == motionevent.action_cancel;

            // update list of touch targets for pointer down, if needed.
            final boolean split = (mgroupflags & flag_split_motion_events) != 0;
            touchtarget newtouchtarget = null;
            boolean alreadydispatchedtonewtouchtarget = false;
            if (!canceled && !intercepted) {

                // if the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // we are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                view childwithaccessibilityfocus = ev.istargetaccessibilityfocus()
                        ? findchildwithaccessibilityfocus() : null;

                if (actionmasked == motionevent.action_down
                        || (split && actionmasked == motionevent.action_pointer_down)
                        || actionmasked == motionevent.action_hover_move) {
                    final int actionindex = ev.getactionindex(); // always 0 for down
                    final int idbitstoassign = split ? 1 << ev.getpointerid(actionindex)
                            : touchtarget.all_pointer_ids;

                    // clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removepointersfromtouchtargets(idbitstoassign);

                    final int childrencount = mchildrencount;
                    if (newtouchtarget == null && childrencount != 0) {
                        final float x = ev.getx(actionindex);
                        final float y = ev.gety(actionindex);
                        // find a child that can receive the event.
                        // scan children from front to back.
                        final arraylist<view> preorderedlist = buildtouchdispatchchildlist();
                        final boolean customorder = preorderedlist == null
                                && ischildrendrawingorderenabled();
                        final view[] children = mchildren;
                        for (int i = childrencount - 1; i >= 0; i--) {
                            final int childindex = getandverifypreorderedindex(
                                    childrencount, i, customorder);
                            final view child = getandverifypreorderedview(
                                    preorderedlist, children, childindex);

                            // if there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. we may do a double iteration but this is
                            // safer given the timeframe.
                            if (childwithaccessibilityfocus != null) {
                                if (childwithaccessibilityfocus != child) {
                                    continue;
                                }
                                childwithaccessibilityfocus = null;
                                i = childrencount - 1;
                            }

                            if (!canviewreceivepointerevents(child)
                                    || !istransformedtouchpointinview(x, y, child, null)) {
                                ev.settargetaccessibilityfocus(false);
                                continue;
                            }

                            newtouchtarget = gettouchtarget(child);
                            if (newtouchtarget != null) {
                                // child is already receiving touch within its bounds.
                                // give it the new pointer in addition to the ones it is handling.
                                newtouchtarget.pointeridbits |= idbitstoassign;
                                break;
                            }

                            resetcancelnextupflag(child);
                            if (dispatchtransformedtouchevent(ev, false, child, idbitstoassign)) {
                                // child wants to receive touch within its bounds.
                                mlasttouchdowntime = ev.getdowntime();
                                if (preorderedlist != null) {
                                    // childindex points into presorted list, find original index
                                    for (int j = 0; j < childrencount; j++) {
                                        if (children[childindex] == mchildren[j]) {
                                            mlasttouchdownindex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mlasttouchdownindex = childindex;
                                }
                                mlasttouchdownx = ev.getx();
                                mlasttouchdowny = ev.gety();
                                newtouchtarget = addtouchtarget(child, idbitstoassign);
                                alreadydispatchedtonewtouchtarget = true;
                                break;
                            }

                            // the accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.settargetaccessibilityfocus(false);
                        }
                        if (preorderedlist != null) preorderedlist.clear();
                    }

                    if (newtouchtarget == null && mfirsttouchtarget != null) {
                        // did not find a child to receive the event.
                        // assign the pointer to the least recently added target.
                        newtouchtarget = mfirsttouchtarget;
                        while (newtouchtarget.next != null) {
                            newtouchtarget = newtouchtarget.next;
                        }
                        newtouchtarget.pointeridbits |= idbitstoassign;
                    }
                }
            }

            // dispatch to touch targets.
            if (mfirsttouchtarget == null) {
                // no touch targets so treat this as an ordinary view.
                handled = dispatchtransformedtouchevent(ev, canceled, null,
                        touchtarget.all_pointer_ids);
            } else {
                // dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  cancel touch targets if necessary.
                touchtarget predecessor = null;
                touchtarget target = mfirsttouchtarget;
                while (target != null) {
                    final touchtarget next = target.next;
                    if (alreadydispatchedtonewtouchtarget && target == newtouchtarget) {
                        handled = true;
                    } else {
                        final boolean cancelchild = resetcancelnextupflag(target.child)
                                || intercepted;
                        if (dispatchtransformedtouchevent(ev, cancelchild,
                                target.child, target.pointeridbits)) {
                            handled = true;
                        }
                        if (cancelchild) {
                            if (predecessor == null) {
                                mfirsttouchtarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionmasked == motionevent.action_up
                    || actionmasked == motionevent.action_hover_move) {
                resettouchstate();
            } else if (split && actionmasked == motionevent.action_pointer_up) {
                final int actionindex = ev.getactionindex();
                final int idbitstoremove = 1 << ev.getpointerid(actionindex);
                removepointersfromtouchtargets(idbitstoremove);
            }
        }

        if (!handled && minputeventconsistencyverifier != null) {
            minputeventconsistencyverifier.onunhandledevent(ev, 1);
        }
        return handled;
    }

4. viewgroup将事件分发给childview的机制

viewgroup分发事件时会遍历 childview,如果手指触摸的点在 childview 区域内就分发给这个view。当 childview 重叠时,一般会分配给显示在最上面的 childview。

5. viewgroup 和 childview 同时注册了事件监听器(onclick等),哪个会执行?

事件优先给 childview,会被 childview消费掉,viewgroup 不会响应。

 

附:参考资料:

1. android事件传递机制分析

2. android 事件分发机制详解

3. 安卓自定义view进阶-事件分发机制原理