当前位置:   article > 正文

android事件分发机制源码分析_android 事件分发源码解析

android 事件分发源码解析

没什么用的前言

事件分发机制是面试中一道必问的题目,而我的应对方式则是,在网络上找一些博客看看,然后做一些笔记,最后在面试时将我自己记住的内容说出来。这种方式本身没有太大的问题,因为在看博客的过程中也学到了知识。但每次看完博客,我都没办法很好地理解整个流程,所以决定自己看一下源码,看完之后,决定通过博客这种形式将自己的笔记输出出来。

责任链设计模式

提到事件分发机制,永远都绕不开责任链设计模式,只有理解了责任链涉及模式,才能更好地理解整个分发流程。
责任链设计模式的定义(来自百度百科):责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
这里的重点有:

  • 每个对象只持有下一个对象的引用,而不会持有更下级别的引用
  • 发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求

假设有对象A、B、C,A持有B的引用,B持有C的引用。当A获取到请求后,会判断自己是否需要处理该请求,如果不需要处理,就将请求交给B处理,B也会做一样的判断。
假设请求最终是由C处理,则可以看到,A并没有持有C的引用,但请求最终却可以在由C处理。这就是责任链设计模式的优势。

代码举例

// AbstractTask
public abstract class AbstractTask {
    private AbstractTask next;

    public AbstractTask(){
    }

    public AbstractTask(AbstractTask next){
        this.next = next;
    }

    public void handleRequest(Request request){
        // 如果当前的level可以处理,就交给自己处理
        if(getTaskLevel() == request.getTaskLevel()) {
            handle(request);
        }else {
            // 如果自己没办法处理,并且存在next,就交给next处理
            if(next != null) {
                next.handleRequest(request);
            }else {
                System.out.println("没有找到处理对象");
            }
        }
    }

    public void setNext(AbstractTask next){
        this.next = next;
    }

    protected abstract int getTaskLevel();

    protected abstract void handle(Request request);
}

// ATask
// 还有B和C task,不过代码都差不多,我就不贴出来了
// B的taskLevel为2,C的taskLevel为3
public class ATask extends AbstractTask{
    @Override
    protected int getTaskLevel() {
        return 1;
    }

    @Override
    protected void handle(Request request) {
        System.out.println("这里是A task, request: " + request.toString());
    }
}

// Request
public class Request {
    private final int taskLevel;
    private Object content;

    public Request(int taskLevel){
        this.taskLevel = taskLevel;
    }

    public void setContent(Object content){
        this.content = content;
    }

    public int getTaskLevel(){
        return taskLevel;
    }

    public Object getContent(){
        return content;
    }

    @Override
    public String toString() {
        return "Request{" +
                "taskLevel=" + taskLevel +
                ", content=" + content +
                '}';
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

测试代码

public class Test {
    public static void main(String[] args) {
        ATask aTask = new ATask();
        BTask bTask = new BTask();
        CTask cTask = new CTask();
        aTask.setNext(bTask);
        bTask.setNext(cTask);
        Request request = new Request(3);
        request.setContent("new Request(3)");
        aTask.handleRequest(request);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

最后打印出来的是:这里是C task, request: Request{taskLevel=3, content=new Request(3)}。
可以看到,A没有持有C的引用,但执行了C的handle方法。
有一个必须说清楚的是,持有下一级的引用不只有链表这种方式,还有其他方式。比如用一个数组来存储,这就是ViewGroup的做法。

再看看ViewGroup几行寻找targetView的代码

// 成员变量
private View[] mChildren;

// ViewGroup.dispatchTouchEvent
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 (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        ....
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以看到,ViewGroup是从children里面寻找的,而children是一个数组。所以没必要将子对象局限于链表这种方式,如果有更好的方式,可以使用更好的方式来实现。
而这里的dispatchTransformedTouchEvent方法最终会调用View的dispatchTouchEvent方法,如果调用的View是一个ViewGroup,就会执行相同的代码,直到调用的是一个View。这就是责任链设计模式在事件分发机制上的实现。可能有人会问,如果该ViewGroup没有子View呢?这个问题留到下面的源码分析,这里只是讲解责任链设计模式在事件分发上的应用。
从这里也可以想象得到,A ViewGroup持有B ViewGroup的引用,B ViewGroup持有C View的引用,而A ViewGroup没有直接持有C View的引用,和上面的例子有点像。

流程图

在这里插入图片描述
图里面,省略了DecorView等其他界面。

如果给View2设置onClick并且点击View2,那将发生。

  1. 传递一个ACTION_DOWN的event给Activity
  2. Activity调用dispatchTouchEvent最后将事件传递给ViewGroup
  3. ViewGroup调用dispatchTouchEvent
  4. 调用ViewGroup的onInterceptTouchEvent判断是否拦截该事件,如果否,则执行第5步
  5. 从上面的源码可以看到,ViewGroup是从最后一个View向第0个View的顺序调用。所以会判断View3的范围内是否包含MotionEvent的x值和y值,发现不包含,调用View2判断。
  6. 发现View2是目标View,则调用该View的onTouchEvent方法,并且将后续的事件都给该View
  7. 上面的第4步,如果确定拦截,则会调用该ViewGroup的onTouchEvent, No touch targets so treat this as an ordinary view。在源码搜索这句注释就能看到相应的代码,可以发现,最后调用的就是super的dispatchTouchEvent方法。

总结一下流程:Activity dispatchTouchEvent -> ViewGroup dispatchTouchEvent ->ViewGroup onInterceptTouchEvent->View dispatchTouchEvent ->View onTouchEvent

这个的就不画出来了,一看就知道怎么调用,下面写完源码分析之后,将画一个较为完整的图

以上就是点击一个View后的一个大概流程,接下来开始源码分析,从源码的角度去还原一个完整的调用流程。

源码分析

从上面的流程可以看到

  • ViewGroup的调用顺序是:dispatchTouchEvent -> onInterceptTouchEvent -> onTouchEvent
  • View的调用顺序是:dispatchTouchEvent ->onTouchEvent

所以先看ViewGroup的dispatchTouchEvent方法,再根据流程分析其他方法的代码。

// ViewGroup dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    // 这个变量也是重点,不过先看完其他代码,再回头分析这个变量
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        // 这里的masked可以当action使用
        // 执行一次mask运算之后,就可以保证该action变量不存在action以外的数值
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        
        // 如果是ACTION_DOWN,则清除和重置一些变量
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        
        // Check for interception.
        final boolean intercepted;
        // 该if是用于判断是否执行onInterceptTouchEvent
        // 当ACTION_DOWN找到目标之后,mFirstTouchTarget就不会为空
        // 所以进入if有两种可能,1:action为ACTION_DOWN 2:找到消费该事件的View
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
            // ViewGroup里面有一个方法,为requestDisallowInterceptTouchEvent,调用这个方法就可以修改mGroupFlags的值,具体看下面提供的源码
            // 如果为true,则将包含FLAG_DISALLOW_INTERCEPT这个flag,此时执行这里的&运算将为true,所以将不执行onInterceptTouchEvent方法
            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;
        }
        ...
        // Check for cancelation.
        // 判断是否为cancel
        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
        // 这里的mouseEvent和split不是分析的重点,所以就不解释了,只将代码贴出来
        // Update list of touch targets for pointer down, if needed.
        final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 && !isMouseEvent;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 上面之所以会将cancel贴出来,是因为这里需要用到
        // 如果没有cancel也没有intercepted
        if (!canceled && !intercepted) {
            ...
            // 如果是ACTION_DOWN或者其他情况,这里的其他情况不包含ACTION_MOVE、ACTION_UP这些
            // 至于ACTION_MOVE、ACTION_UP这些,则会传递给消费了ACTION_DOWN的View
            // 而下面的代码,有一个重要的任务,就是寻找目标View
            // 也就是,只有ACTION_DOWN才会寻找目标View,而其他action则不会寻找
            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
                // down时getPointerId将返回0,并且这里的split为true,所以最后的结果为1
                // 该变量主要是用于解决多点触控的问题,这里不作讨论,只要知道这里的值是1即可
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                removePointersFromTouchTargets(idBitsToAssign);
                final int childrenCount = mChildrenCount;
                // 如果有子View
                if (newTouchTarget == null && childrenCount != 0) {
                    // 获取触摸的x值和y值
                    final float x =isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                    final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                    // 代码已经贴再下面了,想看的话可以看看
                    // 这里解释一下,从接收的类型上可以看到,该方法放回的是一个child List
                    // 如果ViewGroup里面的任何一个child的getZ()不为0,就会返回一个非空的数组, 否则就会返回空数组
                    // 该方法的作用是,按child的z值进行排序,z值最大的child放到list的最后
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    // child drawing代码已经贴再下面
                    // 该方法默认是false,可以调用方法设置为true,但设置的方法访问修饰符为protected
                    // 从方法名称可以知道,该方法是用于判断是否支持按照定义的顺序
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    // private View[] mChildren;
                    final View[] children = mChildren;
                    // 这里需要注意,这里的遍历是倒序遍历,即从最后一个往第一个遍历
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 上面的注释也提到,customOrder默认为false,false将返回传入的index,true的情况就不讨论了
                        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                        // 如果preorderedList不为null,就从preorderedList取数据,否则就从children取数据
                        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                        // 两个方法的代码都贴在下面
                        // 第一个方法,判断View是否可见或虽不可见但在执行动画,只要有一个为true,就不会执行continue。而如果返回true,则会执行第二个方法
                        // 第二个方法,判断x值和y值是否在child里面
                        // 如果child不可见并且正在执行动画或者View可见但没有动画 && xy值在child里面,才不会执行continue
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }
                        
                        // getTouchTarget()的作用是查找child是否存在于mFirstTouchTarget的单链表中。  
                        // 如果为true,则返回相应的TouchTarget对象,否则返回null
                        newTouchTarget = getTouchTarget(child);
                        
                        resetCancelNextUpFlag(child);
                        // 重点!!!该方法会调用View的dispatchTouchEvent
                        // 如果是一个ViewGroup,那就执行相同的代码,最后执行到这里
                        // 如果是一个View,则会根据具体情况,判断是否执行onTouchEvent和其他代码
                        // 代码我已经放到了下面,建议把该方法的代码看一看,这个方法真的很重要。在看该方法之前,请记住,action是ACTION_DOWN,cancel是false,child不为空
                        // 如果不想看,那就必须知道,该方法会调用View的dispatchTouchEvent方法,尝试将event分发给View
                        // 如果返回true,则说明child消费该down事件。返回true的情况有多种,可以是child的dispatchTouchEvent返回true
                        // 也可以onTouchListener返回true,或者是onTouchEvent返回true
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ...
                            // 找到child就将child添加到TouchTarget的链条里面,并返回新生成的target对象
                            // 这里要记住,此时,newTouchTarget已经是一个非空对象,并且在调用该方法之后,会给mFirstTouchTarget变量赋值,所以此时的mFirstTouchTarget也不为空
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 该变量也要记住,这里是在该if里面唯一一行设置为true的代码
                            alreadyDispatchedToNewTouchTarget = true;
                            // 既然找到touchTarget,那就跳出循环把,没必要继续循环下去
                            break;
                        }
                        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;
                }
            }
        }
        // 如果上面的ACTION_DOWN没有找到消费该event的view,这里的mFirstTouchTarget则为null
        if (mFirstTouchTarget == null) {
            // 此时,可以继续带着参数看该方法,这里的canceled是false,child是null
            // 不过我还是解释一下吧,由于child是null,最终调用的是自己的super.dispatchT...方法
            // 也就是说ViewGroup将调用View的dispatchTou...方法
            // 如果返回true,则表明该事件被该ViewGroup消费,以后的事件都会执行下面的else代码
            // 为了防止有人懂不懂什么会执行下面的代码,我说清楚一点,一个ViewGroup也会被其他ViewGroup调用
            // 所以我的意思是,其他ViewGroup会在上面的for循环找到target,然后执行下面的else代码
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
        } else {
            // 如果mFirstTouchTarget不为空,则会执行这里的代码
            // ACTION_DOWN不为空时,表示找到了消费事件的View
            // 其他action则是,在ACTION_DOWN找到消费的View
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 只有ACTION_DOWN时,并且找到消费的View,该变量才会为true,其他情况都是false
                // 由于ACTION_DOWN已经执行了dispatchTou...,所以这里没有执行是正确的
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 看了一下代码,只有调用performButtonActionOnTouchDown,reset方法才有可能返回true,而intercepted则不用说,只要ViewGroup不拦截该event,就wieldfal
                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                    // cancelChild上面提到,child则是消费了ACTION_DOWN的View
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 下面的if和cancel有关,不做讨论
                    // 总结一下上面的if和else,ACTION_DOWN时,如果有View消费了该事件,上面的if就为true,就会将handled置为true
                    // 并且在寻找目标View时,会调用View的dispatchTou..,所以上面的if不会调用dispatchTou...
                    // 在此之上,其他action则会执行else的代码,从intercept可以看到,即便找到目标View,也可以拦截其他事件
                    // 而不管是否拦截,都会执行View的dsipatchTou...方法,只不过如果拦截了,View拿到的action是ACTION_CANCEL,而不是event原本的action
                    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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198

其他代码可不看 ,还是建议看看dispatchTransformedTouchEvent方法的代码。里面关键代码还是很多,不看的话,肯定没办法理解整体的执行流程。
方法列表

既然return handled的代码在末尾,那就在这里总结一下,哪些地方可能为true。首先,默认值是false,所以必须手动置为true。
第一次,这段代码不能一定会置为true,只能说可能会,但还是有必要拿出来

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
  • 1
  • 2

当ACTION_DOWN找不到target时,就会执行这里的代码,最终会调用super.dispatchTou…方法。此时,如果该ViewGroup要消费该事件,就可以返回true。
第二次,当ACTION_DOWN找到target view时,直接置为true

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
  • 1
  • 2

第三次,这个if的else

} else {
    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
    if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
        handled = true;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

再探讨一下,为什么找到target view时,ViewGroup需要返回true。
我的理解是:既然是该ViewGroup的child消费事件,那也可以说是该ViewGroup消费了该事件。只不过上一级并不知道具体消费的对象。但这不重要,只要知道该VIewGroup需要就行,不需要理会具体是哪个对象。
可以思考一下,如果返回false会怎么样。如果返回false,那ViewGroup在调用dispatchTra…方法的时候,就没办法找到target view。所以ViewGroup必须返回true,然后再由该ViewGroup去调用自己的child,将事件传递给target view。

上面的dispatchTransformedTouchEvent方法一定要看,里面有很多很重要的代码。
简单梳理一下执行流程

  1. Activity接收到触摸事件之后,会调用ViewGroup的dispacthTouchEvent方法,让ViewGroup自己将触摸事件分发出去
  2. 如果是ACTION_DOWN,就清空touch相关的数据。并从最后一个到第一个寻找需要消费的子View。寻找的方式为,判断触摸的x值和y值是否在子View里面,并且子View的dispacthTouchEvent方法是否返回true。如果返回true,则会用该View的信息构建一个TouchTarget并将TouchTarget的值赋给mFirstTocuhTarget
  3. ACTION_DOWN的代码执行完成之后,将判断mFirstTouchTarget的值是否为空。如果不为空,就说明找到需要消费的View,ViewGroup的dispatchTouchEvent方法将返回true,表示该事件被该ViewGroup消费了。如果为空,则会调用super.dispatchTouchEvent方法,尝试将该事件分配给自己。
  4. ACTION_DOWN以外的事件将不寻找消费的子View,而是判断mmFristTouchTarget是否为空。如果不为空,继续调用child的dispatchTouchEvent方法并返回true。如果为空,则会调用当前ViewGroup的super.dipatchTouchEvent方法。
  5. 有一点需要注意,如果不是ACTION_DOWN并且没有找到消费的子View或者是调用了disallowIntercept方法,则不会调用onInterceptTouchEvent。其他情况都会调用onInterceptTocuhEvent方法,所以即使找到了消费的子View,也会尝试拦截触摸事件。
  6. 关于View的dispatchTouchEvent,在调用onTouchEvent之前,会判断onTouchListener是否为空。如果不为空,并且onTouch方法返回true,则会返回true,表明消费了该事件。否则会执行onTouchEvent方法。
  7. 在onTouchEvent方法里面,如果没有设置click或longClick或tooltip,就会返回false。longClick和tooltip的触发逻辑是一样的,都是在down的时候,向RunQueue放入一个延迟任务,如果延迟结束就会执行longClick。如果延迟还没结束就松开手指,即执行了up事件,则会将该任务移除。如果up时延迟还没有结束,或者没有设置longClick,并且设置了click,就会触发onClick。如果同时设置了longClick和click,并且长按超过400毫秒,就会执行longClick,此时松开手指也不会执行click。也就是,longClick的优先级是高于click。

最后根据上面分析的执行流程,重新画一个简单流程图。先说清楚,这个流程图只是用于理解,所以像第7步的longClick、click不会包含在这里面。
在这里插入图片描述

方法列表

requestDisallowInterceptTouchEvent
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

回到源码分析

buildTouchDispatchChildList
/**
 * Provide custom ordering of views in which the touch will be dispatched.
 *
 * This is called within a tight loop, so you are not allowed to allocate objects, including
 * the return array. Instead, you should return a pre-allocated list that will be cleared
 * after the dispatch is finished.
 * @hide
 */
public ArrayList<View> buildTouchDispatchChildList() {
    return buildOrderedChildList();
}

/**
 * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children,
 * sorted first by Z, then by child drawing order (if applicable). This list must be cleared
 * after use to avoid leaking child Views.
 *
 * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated
 * children.
 */
ArrayList<View> buildOrderedChildList() {
    final int childrenCount = mChildrenCount;
    if (childrenCount <= 1 || !hasChildWithZ()) return null;
    if (mPreSortedChildren == null) {
        mPreSortedChildren = new ArrayList<>(childrenCount);
    } else {
        // callers should clear, so clear shouldn't be necessary, but for safety...
        mPreSortedChildren.clear();
        mPreSortedChildren.ensureCapacity(childrenCount);
    }
    
    final boolean customOrder = isChildrenDrawingOrderEnabled();
    for (int i = 0; i < childrenCount; i++) {
        // add next child (in child order) to end of list
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View nextChild = mChildren[childIndex];
        final float currentZ = nextChild.getZ();
        // insert ahead of any Views with greater Z
        int insertIndex = i;
        while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
            insertIndex--;
        }
        mPreSortedChildren.add(insertIndex, nextChild);
    }
    return mPreSortedChildren;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

回到源码分析

isChildrenDrawingOrderEnabled
protected boolean isChildrenDrawingOrderEnabled() {
    return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER;
}

protected void setChildrenDrawingOrderEnabled(boolean enabled) {
    setBooleanFlag(FLAG_USE_CHILD_DRAWING_ORDER, enabled);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

回到源码分析

canReceivePointerEvents和isTransformedTouchPointInView
protected boolean canReceivePointerEvents() {
    return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}

// 这里的outLocalPoint应该是想将x值和y值设置到该变量里面,所以传入了一个引用
protected boolean isTransformedTouchPointInView(float x, float y, View child,
        PointF outLocalPoint) {
    final float[] point = getTempLocationF();
    point[0] = x;
    point[1] = y;
    transformPointToViewLocal(point, child);
    final boolean isInView = child.pointInView(point[0], point[1]);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0], point[1]);
    }
    return isInView;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

回到源码分析

dispatchTransformedTouchEvent
// 在dispatchTouchEvent里面,由三个地方调用该方法,其中两个是child调用的,一个是当前的VieweGroup调用的
// child调用时:child不为空
// 当前ViewGroup调用时:child为null
// cancel就都当作false吧
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    // 虽说当作canel为false,但还是说一下代码的执行逻辑
    // 除了action是cancel之外,如果再ACTION_DOWN找到target view,并且当前的action不是ACTION_DOWN,并且ViewGroup的onInterceptTouchEvent返回true
    // cancel的值也是true的,此时,也会进入到该if,所以在进入之后,需要手动设置为cancel,因为进入之前的action不是cancel
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        // 可以看到,如果child为空,就会调用ViewGroup自己的super.dispatchTou...方法。而如果不为空,则会调用child的该方法。
        // 而如果是调用super.dispatchTou...方法,则方法逻辑就是View的该方法,下面会将View的该方法的代码贴出来,分析代码的执行流程
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }
    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    // 和多点触控有关,一般为true,上面的注释也写得很清楚。并且大部分情况下,会在该if里面执行
    if (newPointerIdBits == oldPointerIdBits) {
        // 不明白hasIdentityMatrix的作用,而且代码是native,看不了。总之,只要知道,该方法一般返回true
        // 所以如果child不为空,是可以进入到if的
        if (child == null || child.hasIdentityMatrix()) {
            // 下面的代码没什么好说的, 接下来看看View的dispatchTou...方法
            // 代码贴在这个方法的下面,滑下去就行了
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                handled = child.dispatchTouchEvent(event);
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    // Done.
    transformedEvent.recycle();
    return handled;
}

// View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    // 这里是accessibility相关的代码
    ...
    // result可以当作ViewGroup的handled,都是默认为false,再根据情况改为true
    // 而如果为true,则表明该View要消费该事件,所以下面那些将resule设置为true的代码,都是表明要消费该事件
    // 不过有一点需要记住,一个View如果想要消费move、up这些事件,就需要在down时表明需要该事件
    // 不过也有其他解决方案,比如从ViewGroup入手,毕竟不是所有情况都需要down事件
    boolean result = false;
    
    ...

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        // 如果enable为true,并且正在拖拽,就直接将resule设置为true
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // noinspection SimplifiableIfStatement
        // li就是保存各种listener的类,比如onClick、onLongClick、onTouchListener等
        // 如果onTouchListener不为空,并且onTouch返回true,就将result设置为true
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 如果上面的if没有执行,则会调用onTouchEvent代码
        // 此时,来回答一道在面试时会被问的题目。onTouch和onTouchEvent哪个会先被调用?
        // 从代码上看,答案已经出来了。不过如果没有看源码,可能就只知道这回事,但不知道为什么
        // 滑下去看onTouchEvent的代码
        // 这里还是提醒一下,如果onTouchEvent返回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;
}

// View的dispatchTouchEvent
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    
    // 代码有点长,不看也可以。该变量如果为true,那就可以click或者longClick
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    // 如果View为disable,即enable为false
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        // 上面的注释已经说明了一切。说一下我的理解,觉得就算一个View disabled,只要设置了click或者longClick,那就必须消费该事件。
        // 只是,是否响应看的是View是否disable
        return clickable;
    }
    // 如果设置了delegate,则执行delegate的代码,并且当delegate返回true时,直接return。如返回false,那就继续执行下面的代码。
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    // 只有设置了click或者tooltip才能进入if。记住,只要进入if,就一定会返回true,即消费该事件。没有进入则返回false
    // tooltip是android 26新出的一个功能,给一个View设置tooltip(String)之后,长按该View,就会出现一个提示
    // 所以tooltip就是一个longClick,这不是我瞎说的。实际上,tooltip和longClick就是调用同一段代码,具体可以看下面的源码分析
    // tooltip的设置方式:调用setTooltipText。如果参数是一个空字符串,则会清空flag,否则会设置tooltip的flag
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            // 只分析ACTION_DOWN和ACTION_UP,可以先看ACTION_DOWN的代码,再回头看ACTION_UP的代码
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                // 如果不能点击,则表明是因为设置了tooltip
                // 而如果看了checkForLongClick的代码,就会知道,显示tooltips不是在ACTION_UP执行的,而是由Runnable自己完成的
                // 所以到了这里直接移除callback是没有问题的。假设在up之前就显示tooltip,那移除肯定没问题。而up之后才需要执行,那更要移除,因为都已经松开手指了
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                // 在ACTION_DWON,有一句代码调用了setPressed(true, x, y),并且执行了mPrivateFlags |= PFLAG_PRESSED,所以这里为true
                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);
                    }
                    // 在checkForLongClick我有提到,如果有handle longClick,该变量就会变为true,所以如果没有handle,并且不忽略up event,就可以进入event
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        // 如果是clickable,上面的break则不会执行,所以需要在这里执行
                        removeLongPressCallback();
                        // Only perform take click actions if we were in the pressed state
                        // 如果没有focus,就执行click
                        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();
                            }
                            // Returns true if the Runnable was successfully placed in to the message queue.  
                            // Returns false on failure, usually because the looper processing the message queue is exiting.
                            // 上面的注释是post方法的注释,不是原本写在这里的注释
                            // 意思是,如果没办法成功向消息队列放入Runnable,则会执行下面的performClick方法
                            // 而PerformClick从类名也看得出,就是用来执行click,所以效果是一样的
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    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:
                ...
                // ACTION_UP时,该变量用于判断是否调用onClick,只有false时,才可以调用onClick
                mHasPerformedLongPress = false;
                // 如果不能点击, 那就只有tooltip这种可能性了,直接break就行。记住,我上面提到了,只要break,就会返回true
                // 为什么现在就可以return,可以滑下去看看该方法的代码。顺便一提,该方法和longClick调用的是同一个方法,只要看一遍就可以了
                if (!clickable) {
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    break;
                }
                // Performs button-related actions during a touch down event. True if the down was consumed.
                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.
                // 如果正在滚动,则将延迟100ms执行
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    // 点击查看ViewConfiguration.getTapTimeout()的代码,可以发现,获取到的值是100mms
                    // 最终,mPendingCheckForTap也会执行else里面的checkFor...代码,所以逻辑是一样的
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                break;
                
            case MotionEvent.ACTION_CANCEL:
                ...
                break;
                
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        
        return true;
    }
    return false;
}

// View的checkForLongClick
private void checkForLongClick(long delay, float x, float y, int classification) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        mPendingCheckForLongPress.setClassification(classification);
        // mPendingCheckForLongPress是一个Runnable,看run方法就行
        // 再结合下面的run方法可以知道,如果设置了tooltip或longClick,按下去之后,不管是否有move,只要时间到了,就会触发设置的事件
        postDelayed(mPendingCheckForLongPress, delay);
    }
}

// CheckForLongPress的run
public void run() {
    if ((mOriginalPressedState == isPressed()) && (mParent != null)
            && mOriginalWindowAttachCount == mWindowAttachCount) {
        recordGestureClassification(mClassification);
        // 调用longClick,具体代码我就不贴出来了,只要知道有这件事就行了
        // 不过调用了,不代表一定就会执行longClick,具体要看是否设置了longClickListener或者tooltip
        // 当然了,有人可能会思考。在longClick响应之前就松开手指,会怎么办?这个就需要看ACTION_UP的代码,ACTION_UP有一行代码会将callback移除掉,以保证不会触发longClick
        if (performLongClick(mX, mY)) {
            // 可以看到longClick有handle,就会将mHasPerformedLongPress置为true,这很重要
            mHasPerformedLongPress = true;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332

回到源码分析

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/673343
推荐阅读
相关标签
  

闽ICP备14008679号