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

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

一、View的事件分发

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

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

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

  • 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.一、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执行逻辑,只粘贴了部分关键代码,所执行逻辑如上面注释,下面咱们逐步分析如下:post

  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执行如下操做:ui

  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();this

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

二、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的事件分发的基本知识和源码分析到这里就介绍完了,可能直接理解会比较抽象,下面咱们具体的看一下是如何控制和拦截事件的;

三、结论分析

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

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

四、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的事件分发逻辑,才能从根本上解决实际开发中的问题

相关文章
相关标签/搜索