Android进阶知识树——View、ViewGroup事件分发机制详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Alexwll/article/details/85057663

对于Android开发者来说,自定义View是必须攻克的一关,也是从初级工程师迈向高级的进阶关卡,要想通过此阶段,除了必须掌握View的测量、绘制、滑动等基础知识外,更要掌握View的核心知识点:View的事件分发,本篇就一起从源码的角度分析View和ViewGroup的事件分发机制;

1、View的事件分发

在我们平时的使用或写自定义View时,都会直接或间接的使用View的事件分发,View的事件分发主要与View源码中的3个方法有关:

  1. dispatchTouchEvent()
  2. onTouch()
  3. onTouchEvent()

下面我们针对这三个方法从源码学习和分析事件的分发,一起从本质上掌握View是如何在层层传递和消耗事件;

  • dispatchTouchEvent(MotionEvent event)
public boolean dispatchTouchEvent(MotionEvent event) {
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;
}
}

上面代码是dispatchTouchEvent()中的部分代码,也是与我们使用最接近的核心代码,首先会判断View是否设置触摸监听mOnTouchListener,如果设置则会调用OnTouchListener.onTouch()方法,如果此方法返回true,则dispatchTouchEvent()返回true即拦截事件,若onTouch()返回false,则调用onTouchEvent(),如果onTouchEvent()返回true则事件被消耗,否则事件继续传递;从上面的方法和叙述我们可以得出以下结论:

  1. 若View设置OnTouchListener,则先调用onTouch(),所以OnTouchListener的优先级高于onTouchEvent()
  2. 若onTouch()返回true,表示onTouch消耗事件,此时onTouchEvent()不会调用
  3. 若onTouch()返回false,此时onTouchEvent()被调用,若onTouchEvent返回true,事件被消耗

1.1、onTouchEvent()源码分析

  • ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
    mPrivateFlags3 |= PFLAG3_FINGER_DOWN; // 设置mPrivateFlags3为FINGER_DOWN标记
}
mHasPerformedLongPress = false;  //设置false表示此事还未出发长按事件

boolean isInScrollingContainer = isInScrollingContainer();  // 调用父容器的shouldDelayChildPressedState(),默认true

if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED; // 状态设置为中间状态PFLAG_PREPRESSED
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); // 延时发送执行CheckForTap中的run(),ViewConfiguration.getTapTimeout() = 100ms
} else {
    setPressed(true, x, y);
    checkForLongClick(0, x, y); // 直接检测长按事件
}

//CheckForTap中调用检测长按事件
@Override
public void run() {
    mPrivateFlags &= ~PFLAG_PREPRESSED;
    setPressed(true, x, y);
    checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);//调用长按检测方法
}

// checkForLongClick中延时发送CheckForLongPress实例
postDelayed(mPendingCheckForLongPress,
        ViewConfiguration.getLongPressTimeout() - delayOffset);  // getLongPressTimeout()为 500ms(系统默认的长按时间)

@Override
public void run() {
    if ((mOriginalPressedState == isPressed()) && (mParent != null)
            && mOriginalWindowAttachCount == mWindowAttachCount) {
        if (performLongClick(mX, mY)) {  // 
            mHasPerformedLongPress = true;  // 设置标志表示触发长按;此标志是否为true取决于li.mOnLongClickListener.onLongClick的返回值
        }
    }
}

//在performLongClick()中代码会最终调用performLongClickInternal()
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
    handled = li.mOnLongClickListener.onLongClick(View.this);  //调用长按监听中的onLongClick();返回值影响mHasPerformedLongPress
}

以上代码是View的onTouchEvent()的ACTION_DIOWN执行逻辑,只粘贴了部分关键代码,所执行逻辑如上面注释,下面我们逐步分析以下:

  1. 首先将mPrivateFlags3设置为FINGER_DOWN标记
  2. 将mHasPerformedLongPress设置为false,表示点击还未触发长按事件
  3. 创建CheckForTap()实例,并延时发送执行CheckForTap中的run()
  4. 在checkForLongClick中延时发送CheckForLongPress实例,检测长按事件
  5. 在performLongClick()中代码会最终调用performLongClickInternal(),performLongClickInternal回调设置的mOnLongClickListener.onLongClick()
  6. 若onLongClick()返回true,则会将mHasPerformedLongPress设置为true表示触发长按事件,否则不触发长按事件
  • ACTION_MOVE
if (!pointInView(x, y, mTouchSlop)) { //判断手指是否划出View范围
    removeTapCallback();  // 移除CheckForTap事件
    removeLongPressCallback();   // 移除长按检测事件
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

在Action_Move事件中,主要根据手指滑动的坐标判断是否移除View的范围,若移除则取消和移除CheckForTap事件

  • ACTION_UP
if (!clickable) {   // 如果步可点击移除所有的事件检测
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { //如果已经出发长按事件,且mHasPerformedLongPress设置为true则不去执行单击
if (mPerformClick == null) {
    mPerformClick = new PerformClick();  //创建PerformClick检测单击事件,最终调用 performClick();
}
if (!post(mPerformClick)) { //发送失败直接调用performClick()
    performClick();
}
}

public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);  // 调用onClick方法
    result = true;
} else {
    result = false;
}
}

在手指抬起时View执行以下操作:

  1. 如果View的clickable为false,则移除所有的检测事件
  2. 根据mHasPerformedLongPress的值,设置事件点击检测,若mHasPerformedLongPress为true,表明触发了长按事件则不用检测点击事件
  3. 若mHasPerformedLongPress为false,创建PerformClick()实例,并发送PerformClick实例,若发送失败则直接调用performClickInternal()方法
  4. 在PerformClick()实例中的run()直接调用performClickInternal()方法,最终调用performClick() 
  • performClick() 
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);  // 调用onClick方法
    result = true;
} else {
    result = false;
}
}

这个方法看起来是不是很面熟,和上面判断onTouch()的基本一致,首先判断View是否设置了OnClickListener事件监听,若设置则调用onClick()方法,此时result返回true表示消耗事件,所以我们设置的onClick的监听等级较低,按照事件分发逻辑看,处理我们触摸事件的方法按优先级以此为:onTouch() -> onTouchEvent() -> onClick();

View的事件传递到此就结束了,下面看看比他更复杂的、它的父类ViewGroup的事件分发;

2、ViewGroup事件分发

前面分析了View的事件分发,但在实际开发过程中真正要使用View事件分发时,基本都是因为ViewGroup的嵌套导致的内外滑动问题,所以对ViewGroup的事件分发更需要深入了解,和View的事件分发一样,ViewGroup事件分发一样与几个方法有关:

  1. dispatchTouchEvent()
  2. onInterceptTouchEvent()
  3. onTouchEvent()

使用一段伪代码来表述上面三个方法在ViewGroup事件分发中的作用,代码如下:

public boolean dispatchTouchEvent(MotionEvent event){
  boolean consume = false;
  if(onInterceptTouchEvent(event)){
  consume = onTouchEvent(event);
}else{
  consume = child.dispatchTouchEvent(event);
}
 return consume;
}

从上面代码中看出,事件传递到ViewGroup时首先传递到dispatchTouchEvent(MotionEvent event)中,然后执行以下逻辑,首先在ViewGroup.dispatchTouchEvent() 中调用onInterceptTouchEvent()  方法:

  1.  返回true,表示拦截事件 ->  onTouchEvent() -> 返回true 表示消耗
  2. 返回false,表示不拦截事件 -> child.dispatchTouchEvent(event) 事件向下传递,如此反复传递分发

在onInterceptTouchEvent()  返回false时,表明当前ViewGroup不消耗事件,此事件会向下传递给子View,此子View可能是View也可能是ViewGroup,如果是View则按照上面的事件分发消耗事件;

  • ViewGroup.dispatchTouchEvent()

事件的传递首先是从手指触摸屏幕开始,所以我们先查看dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩余复杂的逻辑,方法有一段主要的代码:

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  // 返回true表示子View设置了父容器不拦截事件
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 
    } else {
        intercepted = false;
    }
} else {
    intercepted = true;
}

上述代码虽然简单但ViewGroup的事件分发多半与此处的逻辑有关,里面的每个细节都会影响到最终的事件消耗,总结上面代码执行如下:

  1. 在dispatchTouchEvent()中只有在MotionEvent.ACTION_DOWN 或 mFirstTouchTarget != null,才会调用onInterceptedTouchEvent()询问是否拦截
  2. mFirstTouchTarget:指向处理触摸事件的子View;当ViewGroup子View成功拦截后,mFirstTouchTarget指向子View,此时在整个事件过程中会不断询问ViewGroup的拦截状况;
  3. 如果ViewGroup确定拦截事件,mFirstTouchTarget为null,所以整个触摸事件不会询问ViewGroup的onInterceptedTouchEvent();

在上述代码中除了MotionEvent.ACTION_DOWN和mFirstTouchTarget != null条件之外,还有一个会影响到onInterceptedTouchEvent()的调用,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0,这里主要是用于在子View中设置父容器的拦截条件(多用于滑动冲突),先看以下FLAG_DISALLOW_INTERCEPT这个标识为:

  1. FLAG_DISALLOW_INTERCEPT:控制事件拦截标记位,在子View中requestDisallowInterceptTouchEvent()中可以设置标记位

看一下requestDisallowInterceptTouchEvent()方法源码:

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {    // 状态相等时无需设定
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;     // mGroupFlags = FLAG_DISALLOW_INTERCEPT
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // mGroupFlags = 0;
    }
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

上面代码中mGroupFlags初始值为0,FLAG_DISALLOW_INTERCEPT初始值为0x80000,在方法中根据参数boolean设置mGroupFlags的值:

  1. 当传入disallowIntercept为true时,mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT = 0x80000;此时在dispatchTouchEvent()中 满足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = true,所以intercepted 直接返回false,不拦截事件

  2. 当传入disallowIntercept为false时,mGroupFlags = mGroupFlags & ~FLAG_DISALLOW_INTERCEPT = 0;此时在dispatchTouchEvent()中 不满足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = false,所以回调onInterceptTouchEvent(),父布局有机会拦截事件

总结一句话就是在requestDisallowInterceptTouchEvent()中设置true,表示不允许父容器拦截事件,设置为false,表示允许父容器拦截事件;

既然上面所有的条件都在判断是否需要调用onInterceptTouchEvent(),说明事件最后的拦截取决于onInterceptTouchEvent()方法的返回值,那么我们先看一下此方法;

  • onInterceptTouchEvent()默认返回false,表示父容器默认不拦截事件
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;  //默认返回false,即父容器不拦截任何事件
}
  • dispatchTouchEvent()向子View的传递
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {  //循环检测每个子View
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
          …...
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) { //检测当前坐标是否超出View的范围,若超出跳过此view
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //调用dispatchTransformedTouchEvent方法
    …...
    newTouchTarget = addTouchTarget(child, idBitsToAssign); // addTouchTarget中赋值mFirstTouchTarget指向child
    alreadyDispatchedToNewTouchTarget = true;
    break;
 }
}

//dispatchTransformedTouchEvent
if (child == null) {
    handled = super.dispatchTouchEvent(event); // 如果child == null,直接调用super.dispatchTouchEvent,ViewGroup自己处理
} else {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    event.offsetLocation(offsetX, offsetY);

    handled = child.dispatchTouchEvent(event); // 如果存在child,调用child.dispatchTouchEvent(event)

    event.offsetLocation(-offsetX, -offsetY);
}
}

上面代码为ViewGroup的dispatchTouchEvent()中的部分代码,也是控制ViewGroup的事件传向子View的传递,一起来看一下执行逻辑:

  1. 首先判断事件是否被取消或被ViewGroup拦截即intercepted是否为false,若被拦截事件已经消耗不需要传递
  2. 检测当前坐标是否超出View的范围,若超出跳过此view
  3. 调用dispatchTransformedTouchEvent()方法,

在dispatchTransformedTouchEvent()中根据子View判断执行,如果child == null则直接调用super.dispatchTouchEvent,ViewGroup自己处理,如果存在child,调用child.dispatchTouchEvent(event),则事件传递到View,接着刚才的代码向下看,当dispatchTransformedTouchEvent()返回true时,代码会执行到addTouchTarget(child, idBitsToAssign)方法:

 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

在addTouchTarget()方法中将mFirstTouchTarget指向子View,所以上面的判断mFirstTouchTarget != null在子View拦截事件时成立;

  • onTouchEvent()返回false

到View的onTouchEvent()返回true即表示事件被View消耗,事件的分发也到此结束了,可有没有考虑过最上层的子View的onTouchEvent()如果不拦截事件呢?最终的事件会去哪呢?答案是要被Activity的onTouchEvent()消耗,我们知道当一个事件产生时最先获取的是Activity,然后按照Activity -》Window -》ViewGroup -》View这样的顺序传递下去,而在ViewGroup中子View的返回值是在dispatchTransformedTouchEvent()中获取的,查看代码:

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;

在dispatchTransformedTouchEvent()中若返回false,程序会执行到以下逻辑:

if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 

通过上面的学习我们知道mFirstTouchTarget是指向消耗事件的子View,但当子View不消耗时此时mFirstTouchTarget == null成立,代码会再次调用dispatchTransformedTouchEvent()方法,此时传递的child为null,通过上面的代码我们知道child = null时代码执行super.dispatchTouchEvent(event),即调用父类的dispatchTouchEvent(event),因为ViewGroup本质上也是继承View,只不过是包含子View的View,所以事件的传递又到了上层View中,在View的dispatchTouchEvent()会询问onTouch()和onTouchEvent()方法,所以事件又被向上传递了;

但如果所有的ViewGroup和子View都不消耗事件,事件会逐层向上传递知道事件的开始,也就是Activity层,这时我们点开Activity的dispatchTouchEvent()方法,

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从代码中可以看出系统调用getWindow().superDispatchTouchEvent(ev)进行事件分发,其实就是向Window和ViewGroup进行事件的传递,若有消耗事件的这里返回true方法结束,若没有View消耗事件即getWindow().superDispatchTouchEvent(ev)返回false,系统会调用Activity的onTouchEvent()处理事件,所以事件一定会被消耗掉,到此针对View不消耗事件的分析就结束了,我们也可以得出以下结论:

  1. 当事件传递到View时,如果View的onTouchEvent()返回false,则父类的onTouchEvent()会被调用,依次向上传递
  2. 若所有的View都不消耗事件时,Activity的onTouchEvent()会被调用

关于ViewGroup的事件分发的基本知识和源码分析到这里就介绍完了,可能直接理解会比较抽象,下面我们具体的看一下是如何控制和拦截事件的;

3、结论分析

根据上面的View和ViewGroup的事件分发学习,这里给出几个View事件传递的结论(以下结论针对系统自动分发),并根据学习内容进行逐条分析

  • 正常情况下一个事件序列只能被一个View拦截或消耗;
  • 对于View一旦决定拦截事件即onTouchEvent()返回true,那后续的整个事件序列都会交给它消耗;
  • 如果View不消耗ACTION_DOWN事件,则后续的事件序列都不会再给他处理
  1. 如果View在ACTION_DOWN时返回false,那系统的mFirstTouchTarget为null,在后续的MOVE、UP事件中onInterceptTouchEvent()不会再被调用,直接拦截事件

4、ViewGroup、View的事件拦截

事件拦截最经典的使用示例和场景就是滑动冲突,按照View的冲突场景分,滑动冲突可以分为3类:

  1. 外部滑动和内部滑动方向不一致
  2. 外部滑动和内部滑动方向一致
  3. 以上两种情况嵌套

一般处理滑动冲突有两种拦截方法:外拦截和内拦截

  • 外部拦截

外拦截顾名思义是在View的外部拦截事件,对View来说外部就是其父类容器,即在父容器中拦截事件,通过上面的代码我们知道,ViewGroup的事件拦截取决与onInterceptTouchEvent()的返回值,所以我们在ViewGroup中重写onInterceptTouchEvent()方法,在父类需要的时候返回true拦截事件,具体需要的场景要按照自己的业务逻辑判断:

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var intercept = false
        when(ev!!.action){
            MotionEvent.ACTION_DOWN ->{intercept = false}
            MotionEvent.ACTION_MOVE->{
                intercept = if (isNeed()){
                    true
                }else{
                    false
                }
            }
            MotionEvent.ACTION_UP->{intercept = false}
        }
        return intercept
    }

从上面代码中看出:在onInterceptTouchEvent()的ACTION_DOWN中必须返回false,即不拦截ACTION_DOWN事件,因为如果ACTION_DOWN一但拦截,事件后面的事件都会默认给ViewGroup处理,也不会再调用onInterceptTouchEvent()询问拦截,那子View将没有获取事件的机会;在ACTION_DOWN中,根据自己需要的时候返回true,那此时事件就会被父ViewGroup消耗

  • 内部拦截

内拦截是在View的内部控制父容器是否拦截事件,你可能已经想到了就是使用上面介绍的requestDisallowInterceptTouchEvent(),答案没错就是利用这个方法,关于使用这个方法去控制mGroupFlags的值上面已经介绍了,下面我们分析下为何设置此数据来控制ViewGroup的事件拦截: 

因为事件的拦截是在onInterceptTouchEvent()中确定的,我们不可能在子View中控制父容器的方法,但从上面的代码中看出,ViewGroup访问onInterceptTouchEvent()之前必须通过一段关卡,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 要成立,而如果此条件不成立,那dispatchTouchEvent()会直接返回false,所以我们在子View中只要控制这个值就可以了;

到此虽然可以控制访问权限,但如何确保只要在允许访问的时候就会自动拦截呢?那就是onInterceptTouchEvent()要在特定状态下一直返回true,即默认想拦截事件 ,综上所述我们在子View中要想控制父容器必须满足以下条件:

  1. 事件要可以传递到子View,即父容器不能拦截ACTION_DOWN事件
  2. 子View中要通过requestDisallowInterceptTouchEvent()设置控制onInterceptTouchEvent()访问的开关
  3. ViewGroup中onInterceptTouchEvent()中要拦截除了ACTION_DOWN事件意外的事件

上面的事件分发,其实和公司安排任务一样,当一项任务来临时,公司会开会进行任务安排,你可能做好了承担一切任务的准备,但大领导不询问你,整个事件就会按照领导的意见进行安排,突然在某个任务时大领导问了你愿不愿意接,这时你提出了肯定的答复,然后事情就归你了 ,当然干好干不好就是你的问题了,拦截的情况和这个例子一样,下面看下拦截的代码:

//在子View中重写dispatchTouchEvent()方法控制父类的拦截
@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                y = event.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float currentY = event.getY();
                int minTouchSlop = 150;
                if (Math.abs(currentY - y) >= minTouchSlop) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }


//在ViewGroup中拦截除ACTION_DOWN以外的事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var intercept = false
        when(ev!!.action){
            MotionEvent.ACTION_DOWN ->{intercept = false}
            else -> {intercept = true}
        }
        return intercept
    }

到此View和ViewGroup的事件分发和事件滑动冲突的处理到此介绍完毕了,虽然很早之前就学习过这部分的内容,但并没有很好的整理这部分内容,自己写一遍会对整个只是点更加详细的理解,相信在开发过程中很多人都被滑动冲突困扰过,尤其对初级开发者,那段痛苦是必须经过的,所以只有熟悉View和ViewGroup的事件分发逻辑,才能从根本上解决实际开发中的问题

猜你喜欢

转载自blog.csdn.net/Alexwll/article/details/85057663