前语

作为一名移动开发,咱们对滑动抵触能够说是层出不穷。虽然Android现已供给了比方NestedScrollView、CoordinatorLayout等支撑嵌套滑动的组件,但其实并不能掩盖一切的滑动场景,咱们终归会遇到需求自己去处理的滑动抵触。这篇文章将论述如何处理常见的滑动抵触,而滑动抵触的处理本质上便是处理工作分发,所以咱们从工作分发讲起,一步一步斩首滑动抵触。

工作分发

何为工作分发?

工作指的是屏幕触发工作——即Android中的TouchEvent/MotionEvent。每一次咱们接触屏幕,都会产生一连串的接触工作,这些一连串的接触工作合起来便是一个接触工作序列。

接触工作在Android官方API中由类MotionEvent来描述,不同的接触工作对应不同的工作类型。工作类型别离有ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL。

那什么叫分发呢?咱们都知道Android是由View树进行烘托的。假设屏幕坐标为(11,11)的区域既归于一个LinearLayout,又归于LinearLayout下的一个Button,那我这次触碰所产生的接触工作,是该给LinearLayout仍是Button呢?当然,咱们很确定这次接触工作终究会被Button所处理。那接触工作是怎样给到Button的呢?需求经过LinearLayout吗?怎样能让Button不处理呢?这就需求咱们了解接触工作(后文统称为工作)在View树上传递与消费的过程,这便是工作的分发。

工作分发机制

下面咱们就详述工作是如何在View树上进行分发的。当然,除了View树外,还有Activity、Dialog等组件也会对工作进行处理,但他们都是用View树进行终究的烘托的,所以这儿只拿Activity进行举例,结合 ViewGroup与View一同看。

三大金刚

首要咱们需求知道三个核心办法:

dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

Android便是靠这三个办法,处理了工作的分发。留意onInterceptTouchEvent()办法只要ViewGroup才会有,dispatchTouchEvent()、onTouchEvent()是Activity、ViewGroup、View都有的。它们的回来值都是布尔类型,但回来值代表的含义却不尽相同,下面咱们会讲到。

ACTION_DOWN的分发

下面咱们以ACTION_DOWN工作为例,讲解ACTION_DOWN工作的分发过程。

Demo准备

为了便利演示,做了一个Demo,终究长这个姿态:

Android斩首行动—滑动冲突
在各自的办法上加上日志:

Acitivity:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "Activity dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "Activity onTouchEvent return super")
    return super.onTouchEvent(event)
}

ViewGroup1:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 onTouchEvent return super")
    return super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 onInterceptTouchEvent return super")
    return super.onInterceptTouchEvent(ev)
}

ViewGroup2:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 onTouchEvent return super")
    return super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 onInterceptTouchEvent return super")
    return super.onInterceptTouchEvent(ev)
}

View:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "View dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "View onTouchEvent return super")
    return super.onTouchEvent(event)
}

默许回来值

这三个办法,默许都是回来的super.xxx()。这儿咱们先触碰一下View的区域,从日志上看一下工作是怎样分发的:

Android斩首行动—滑动冲突

能够看到默许状况下,ACTION_DOWN工作的分发遵从以下流程图:

Android斩首行动—滑动冲突

这个流程图能够看成是一个U型图,从Activity的dispathchTouchEvent办法开端,假如都是回来super的话,会一直到Activity的onTouchEvent结束。

改动dispatchTouchEvent回来值

现在咱们将ViewGroup2的dispatchTouchEvent回来值设置为false,看看会产生什么:

Android斩首行动—滑动冲突

能够看到,当ViewGroup2的dispatchTouchEvent回来false时,TouchEvent会传递给父View(即ViewGroup1)的onTouchEvent办法,然后再持续往上分发。用流程图表明:

Android斩首行动—滑动冲突

再将ViewGroup2的dispatchTouchEvent回来值设置为true,日志:

Android斩首行动—滑动冲突

能够看到,当ViewGroup2的dispatchTouchEvent回来true时,TouchEvent会直接在这儿消费掉,不再持续分发。流程图:

Android斩首行动—滑动冲突

改动onTouchEvent回来值

下面咱们改动ViewGroup2的onTouchEvent的回来值,别的两个办法都是回来super。先将回来值设为false,日志:

Android斩首行动—滑动冲突

能够看到,当onTouchEvent回来false时,工作分发跟默许回来super是相同的,仍然是按U型链进行分发,将工作传递给父View的onTouchEvent。流程图:

Android斩首行动—滑动冲突

当咱们将onTouchEvent回来值设置为true时,日志:

Android斩首行动—滑动冲突

能够看到,当onTouchEvent回来true时,工作就在这儿消费掉了,不再持续分发。流程图:

Android斩首行动—滑动冲突

改动onInterceptTouchEvent回来值

现在咱们设置ViewGroup2的onInterceptTouchEvent回来值为false,调查日志:

Android斩首行动—滑动冲突

能够看到,onInterceptTouchEvent回来false跟onTouchEvent回来false是相同的,都是以默许的途径持续分发工作。流程图:

Android斩首行动—滑动冲突

下面设置onInterceptTouchEvent回来值为true,调查日志:

Android斩首行动—滑动冲突

能够看到,当ViewGoup2的onInterceptTouchEvent回来true时,工作被分发到了ViewGoup2的onTouchEvent办法,但没有消费掉,而是持续分发。流程图:

Android斩首行动—滑动冲突
之前有提到,只要ViewGroup才会有onInterceptTouchEvent办法。为什么呢?由于咱们能够从上面的几个流程图中看到,Activity的dispatchTouchEvent办法在回来false时,工作会分发到Activity的onTouchEvent办法。View的dispatchTouchEvent办法在回来super时,工作也相同能分发到View的onTouchEvent办法。而ViewGroup的dispatchTouchEvent办法在回来false和super时是将工作往下分发,在回来true时是直接消费,经过改动dispatchTouchEvent办法的回来值底子不能直接分发到自己的onTouchEvent办法。所以针对ViewGroup会有一个onInterceptTouchEvent办法,来让它能够挑选将工作分发给自己的onTouchEvent办法。

小结

工作的三个办法在View中默许回来都是super,按U型链进行分发。

关于dispatchTouchEvent办法:

    • 回来true,消费工作
    • 回来false,假如不是Activity,会将工作分发到上一级View的onTouchEvent。假如是Activity,由于没有上一级View了,就会直接消费工作。

关于onTouchEvent办法:

    • 回来true,消费工作
    • 回来false,将工作分发到上一级View的onTouchEvent

关于onInterceptTouchEvent办法:

    • ViewGroup特有
    • 回来true,将工作分发到自己的onTouchEvent办法
    • 回来false,将工作分发到下一级View的dispatchTouchEvent
    • 不论回来值是什么,都不会消费工作,只起到分发作用

ACTION_UP、ACTION_MOVE的分发

下面咱们来看一下,ACTION_UP工作是怎样分发的,跟ACTION_DOWN有哪些不同。日志中,特意加上了工作的ACTION,枚举如源码所示:

Android斩首行动—滑动冲突

默许回来值

上一节咱们知道,默许状况下,ACTION_DOWN是沿着三个办法组成的U型链进行分发。那默许状况下,ACTION_UP的分发,如日志所示:

Android斩首行动—滑动冲突

能够看到,ACTION_UP是没有像ACTION_DOWN相同沿着U型链分发的,它终究只走了Activity的两个办法。用流程图能够清晰地表明出来:

Android斩首行动—滑动冲突

为什么ACTION_UP不跟ACTION_DOWN相同,沿着U型链走一圈呢?这儿直接给定论:ACTION_UP的分发途径,取决于ACTION_DOWN工作终究是在哪里被消费的。咱们能够试着改动ACTION_DOWN工作的消费方位,来验证这个定论。

改动dispatchTouchEvent回来值

首要将ViewGroup2的dispatchTouchEvent办法回来true,调查日志:

Android斩首行动—滑动冲突

红色是ACTION_DOWN的分发,绿色是ACTION_UP的分发。

流程图:

Android斩首行动—滑动冲突

能够看到ACTION_DOWN工作在ViewGroup2的dispatchTouchEvent被消费了,ACTION_UP跟ACTION_DOWN的分发途径一致,相同也是在ViewGroup2的dispatchTouchEvent被消费了。

改动onTouchEvent回来值

下面咱们将ViewGroup2的onTouchEvent办法回来true,调查日志:

Android斩首行动—滑动冲突
流程图:

Android斩首行动—滑动冲突

能够看到,由于ACTION_DOWN在ViewGroup2的onTouchEvent处被消费了,所以ACTION_UP也在ViewGroup2的onTouchEvent处被消费。但ACTION_UP的分发途径有所不同,比较ACTION_DOWN的分发途径相当于抄了一条近路。由于现已知道ACTION_DOWN“路过”View时没有被消费,所以ACTION_UP就不必再次走View了,而是直接从自己的dispatchTouchEvent分发到自己的onTouchEvent处。

假如将ViewGroup2的onInterceptTouchEvent工作也回来true,咱们能够预知ACTION_UP的途径跟上面的结果是相同的。这儿需求留意一点:这儿都没有走ViewGroup2的onInterceptTouchEvent工作,但ViewGroup1的onInterceptTouchEvent工作是有走到的。 所以相同有一个定论:当ACTION_DOWN工作在ViewGroup的onTouchEvent处被消费了,该ViewGroup的onInterceptTouchEvent是收不到ACTION_UP工作的。

DOWN工作与UP工作分开阻拦

下面讲个杂乱些的比方。已然咱们能够干与ACTION_DOWN的分发,那么相同也能干与ACTION_UP的分发。比方咱们现在在ViewGroup2的onTouchEvent办法中回来true消费ACTION_DOWN工作,然后在ViewGroup1的dispatchTouchEvent办法中回来false分发ACTION_UP工作,日志如下:

Android斩首行动—滑动冲突

能够看到,ACTION_UP在dispatchTouchEvent回来false时,体现也是跟ACTION_DOWN工作相同,分发给了上一个View的onTouchEvent办法。这样,原先的ACTION_UP分发途径也就被阻拦了,ViewGroup1将收不到ACTION_UP工作。所以咱们也能相同得出定论:ACTION_UP工作分发的干与逻辑跟ACTION_DOWN是相同的。 假如有同学调查仔细的话,能够发现在红框内,VIewGroup2额定收到了两个ACTION_CANCEL工作,这是为什么呢?下面会讲到。

至于ACTION_MOVE工作,经过上面几个ACION_UP比方中的日志,咱们能够看出来,ACTION_MOVE工作的分发逻辑,跟ACTION_UP工作的分发逻辑是保持一致的。感兴趣的同学能够自己去试一下。

小结

  • ACTION_UP的消费方位在不干与的状况下,与ACTION_DOWN的消费方位一致
  • ACTION_UP工作分发的干与逻辑跟ACTION_DOWN是相同的
  • 当ACTION_DOWN工作在ViewGroup的onTouchEvent处被消费了,该ViewGroup的onInterceptTouchEvent是收不到ACTION_UP工作的
  • ACTION_MOVE工作的分发逻辑与ACTION_UP工作的分发逻辑相同

ACTION_CANCEL的分发

上一节ACTION_UP的比方中,咱们有发现ACTION_CANCEL的身影,这儿直接用那个比方的日志看一下:

Android斩首行动—滑动冲突

这个比方中咱们阻拦了ACTION_UP工作,在ViewGroup1的dispatchTouchEvent办法中将ACTION_UP工作直接分发给了ViewGroup1的onTouchEvent工作。而ACTION_UP工作本来应该经过ViewGroup2的dispatchTouchEvent办法与onTouchEvent办法,现在不经过了,恰巧这儿(上图黄框)打印出来了ACTION_CANCEL工作。也就意味着:当ACTION_UP工作被上一层View阻拦时,未分发到ACTION_UP工作的办法会收到ACTION_CANCEL工作

咱们无妨再做个比方验证一下,将ACTION_DOWN工作在View的onTouchEvent办法消费掉,一起将ACTION_UP工作在ViewGroup2的dispatchTouchEvent回来true。日志如下:

Android斩首行动—滑动冲突
流程图:

Android斩首行动—滑动冲突

能够看到,View的dispatchTouchEvent和onTouchEvent办法,本能够收到ACTION_UP工作。但由于上层View阻拦的原因,没有收到,此刻它们就会收到ACTION_CANCEL工作。同理,ACTION_MOVE工作被上层容器阻拦,子容器也是会收到CANCEL工作的,感兴趣的同学能够自行验证一下。

别的还有几个场景相同会触发ACTION_CENCEL,由于不怎样常见,所以这儿只列出来一下,来源网上:

  • ACTION_DOWN初始化操作中
  • 在子View处理工作的过程中被父View中移除
  • 子View被设置为了PFLAG_CANCEL_NEXT_UP_EVENT标志位时

需求留意的是,手势滑出View的范围并不会触发ACTION_CANCEL,这个过程中即使滑出范围了,仍然会一直触发ACTION_MOVE工作,并最后触发ACTION_UP工作,只是不会响应点击算了。

OnTouchListener和OnClickListener

咱们开发时必定会遇到OnTouchListener和OnClickListener,那么他们俩跟三大金刚是什么联系呢?咱们无妨看下源码中是怎样完成的。定位到View的dispatchTouchEvent办法中的这段代码:

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是否设置了OnTouchListener,假如设置了OnTouchListener,就会直接阻拦工作,dispatchTouchEvent办法回来true,调用OnTouchListener的onTouch办法,而不会再触发后续的onTouchEvent办法。

再定位到View的onTouchEvent办法中的这段代码:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {          
        case MotionEvent.ACTION_UP:
            if (!post(mPerformClick)) {
                performClickInternal();
            }

能够发现是在onTouchEvent办法中,判别了View是否可点击。若可点击且设置了OnClickListener,那么就会调用OnClickListener的onClick办法。

所以咱们能够得到一个定论:按优先级排序,OnTouchListener>OnTouchEvent>OnClickListener。若设置了OnTouchListener,则不会触发后边两者。OnClickListener在ACTION_UP后触发。

滑动抵触

滑动抵触的场景

滑动抵触常产生于两个可滑动的控件产生嵌套的状况下。比方RecyclerView嵌套ListView,RecyclerView嵌套ScrollView,ViewPager嵌套RecyclerView等。ViewPager之所以没有滑动抵触是由于它自身就现已帮咱们处理掉了。但其它没帮咱们处理的状况就需求咱们自己写代码去处理。

典型的,根据两个控件的滑动方向,能够将滑动抵触分红两类:一个是不同方向的滑动抵触,如外层控件笔直滑动,内层控件水平滑动。另一个便是同方向的滑动抵触,如表里两层控件都是笔直滑动。

下面举一个不同方向滑动抵触的比方。父容器ScrollViewParent,嵌套HorizontalScrollViewChild,ScrollViewParent可笔直滑动,HorizontalScrollViewChild可水平滑动。如图所示:

Android斩首行动—滑动冲突

当咱们手指放在HorizontalScrollViewChild的区域内并竖直滑动时,咱们发现是能够翻滚外层的ScrollViewParent的。阐明ScrollView自身是处理了部分的滑动抵触的,不然HorizontalScrollViewChild假如消费了MOVE工作,ScrollViewParent就消费不了了,也就无法竖直滑动。调查日志:

Android斩首行动—滑动冲突

日志终究咱们能够看到,在绿色处,HorizontalScrollViewChild是有消费MOVE工作的,那之前不是讲错了吗?已然HorizontalScrollViewChild有消费MOVE工作,为啥ScrollViewParent还能滑动呢?由于在刚开端滑动的时候,滑动的距离还太小,因而ScrollViewParent的onInterceptTouchEvent还没有阻拦这个工作,所以HorizontalScrollViewChild能够消费到MOVE工作。但后边一旦笔直滑动了一定距离,MOVE工作就会直接被ScrollViewParent消费掉,然后完成竖直滑动。咱们能够看ScrollViewParent的源码佐证一下:

final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    mIsBeingDragged = true;
    mLastMotionY = y;
    initVelocityTrackerIfNotExists();
    mVelocityTracker.addMovement(ev);
    mNestedYOffset = 0;
    if (mScrollStrictSpan == null) {
        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
    }
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
}

能够看到第3行,只要当yDiff大于一定滑动距离即mTouchSlop时,才会被认定为在笔直方向上滑动,将mIsBeingDragged设置为true。若咱们水平滑动HorizontalScrollViewChild,能够判定,由于yDiff是没有超越mTouchSlop的,所以HorizontalScrollViewChild就能够正常滑动了。调查日志:

Android斩首行动—滑动冲突

能够看到,水平滑动HorizontalScrollViewChild时,ScrollViewParent没有在onInterceptTouchEvent阻拦MOVE工作,MOVE工作得以顺利被HorizontalScrollViewChild消费,完成水平滑动。

那看姿态,两个ScrollView并不抵触呀,他们都现已写好内部逻辑了。其实不然。假设咱们现在手指放在HorizontalScrollViewChild区域中,滑斜上方45角度向左/向右滑动,这时候咱们就会发现,有时候咱们滑动的是HorizontalScrollViewChild,有时候咱们滑动的却是ScrollViewParent,这便是典型的不同方向上的滑动抵触。

通用处理方案

一般状况下,咱们有“内部阻拦法”和“外部阻拦法”两种处理方案去处理常见的滑动抵触。

外部阻拦法

下面咱们用外部阻拦法来处理上面的滑动抵触。外部阻拦法,指的是从外部容器入手,去决议是否要去阻拦工作,若阻拦掉,子View就没法消费了。现在为了处理斜方向的滑动抵触,咱们能够简略地做一个逻辑:当在竖直方向滑动超越15像素时,咱们就认为是滑动外部容器ScrollViewParent。代码如下:

public class ScrollViewParent extends ScrollView {
    public ScrollViewParent(Context context) {
        super(context);
    }
    public ScrollViewParent(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public ScrollViewParent(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    private float downX;
    private float downY;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "ScrollViewParent dispatchTouchEvent");
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = ev.getX();
                downY = ev.getY();
        }
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "ScrollViewParent onInterceptTouchEvent. deltaY:" + (ev.getY() - downY));
        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            return Math.abs(ev.getY() - downY) > 15;
        }
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "ScrollViewParent onTouchEvent");
        return super.onTouchEvent(ev);
    }
}

代码32-33行完成了MOVE工作的阻拦逻辑。然后调查日志:

Android斩首行动—滑动冲突
红框内的日志,能够看到此刻ScrollViewParent会阻拦MOVE工作,直接分发给自己的onTouchEvent,后续的MOVE工作也会直接到onTouchEvent工作去。HorizontalScrollViewChild将收不到MOVE工作。完成作用上也符合咱们的预期,当咱们斜方向滑动时,滑动的大概率(除非dy足够小)都是外部容器ScrollViewParent了。

当然,这其实是咱们假定的一个处理抵触的逻辑,实在的产品逻辑需求根据事务状况去调整。比方当竖直滑动速度超越xx时,滑动外部容器;或许当HorizontalScrollViewChild内部某个View可见时,滑动外部容器,都有可能,但万变不离其宗,终究都是经过改动工作分发的途径去完成。

内部阻拦法

下面再讲“内部阻拦法”怎样处理滑动抵触。“内部阻拦法”跟“外部阻拦法”相反,是从内部容器出发去处理抵触。这依赖于ViewParent#requestDisallowInterceptTouchEvent(),看其源码的注释咱们很容易知道它是什么意思:

Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
Params:
disallowIntercept – True if the child does not want the parent to intercept touch events.
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

当子类不想让其父类/先人ViewGroup.onInterceptTouchEvent(MotionEvent)办法时,能够调用requestDisallowInterceptTouchEvent(),然后保证父类/先人无法经过ViewGroup.onInterceptTouchEvent(MotionEvent)办法对工作进行阻拦。且一旦设置了这个flag,那么这次工作序列中后续的一切工作,都不会经过父类/先人的onInterceptTouchEvent(MotionEvent)办法了。直到下次接触产生,才会清楚掉这个flag。

咱们能够直接看源码,看看这是怎样起作用的。requestDisallowInterceptTouchEvent完成:

@Override
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;
    }
    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

做的工作便是更改符号位,并递归拜访父节点执行该办法。在View的dispatchTouchEvent办法中,对该符号位进行了判别:

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;
}

核心代码是4-9行,当disallowIntercept结果为true时,就不会走onInterceptTouchEvent()办法了。

现在咱们仍是以上面的ScrollViewParent和HorizontalScrollViewChild举例去处理滑动抵触。“内部”要求咱们以内部容器的视角去考虑抵触,那么就假定当在水平方向滑动超越15像素时,滑动内部容器HorizontalScrollViewChild。这时候就需求这样编码:

public class HorizontalScrollViewChild extends HorizontalScrollView {
    public HorizontalScrollViewChild(Context context) {
        super(context);
    }
    public HorizontalScrollViewChild(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public HorizontalScrollViewChild(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    private float downX;
    private float downY;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "HorizontalScrollViewChild dispatchTouchEvent. deltaX:" + Math.abs(ev.getX() - downX));
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = ev.getX();
                downY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(ev.getX() - downX) > 15) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
        }
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "HorizontalScrollViewChild onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.i("touch_event_test", "HorizontalScrollViewChild onTouchEvent");
        return super.onTouchEvent(ev);
    }
}

在27行,调用了requestDisallowInterceptTouchEvent()办法。上面有提到,一旦符号位被设置为true,后续工作序列中的一切工作都不会调用了父类的onInterceptTouchEvent了。因而咱们需求视状况将约束放开,如30行的写法就将flag从头设为false。别的还要留意,由于子View是DOWN工作的消费点,那么MOVE工作是不会经过子View的onInterceptTouchEvent办法的,所以在dispatchTouchEvent或许onTouchEvent中都能够设置符号位,唯独不能放到onInterceptTouchEvent办法;

现在咱们斜方向滑动内部容器HorizontalScrollViewChild,调查日志:

Android斩首行动—滑动冲突

能够看到红框区域,当dx大于15后,后续的MOVE工作就不会经过ScrollViewParent的onInterceptTouchEvent了,咱们要的作用也就达到了。

小结

虽然上面只举了不同方向滑动抵触的比方,但不同方向抵触与同方向抵触,其终究处理的思路都是相同的,要么改动工作分发的途径,要么设置FLAG_DISALLOW_INTERCEPT符号位。它们在处理上的不同之处,就只在于“条件判别”

关于不同方向的抵触,咱们给的条件判别能够是:当DX>X时,水平滑动、当DY>Y时,竖直滑动。

关于同方向的抵触,咱们给的条件判别能够是:当滑动速度大于X时,滑动外部View,反之滑动内部View;当内部View现已滑究竟了,才滑动外部View;当内部View中的某个子View不在屏幕中时,滑动外部View。

一般这些条件判别也都是基于各自的事务出发去进行挑选,但只要咱们知道通用的处理思路,问题就都能够便利的解决了。至此,对滑动抵触的通用处理办法(“内部阻拦法”与“外部阻拦法”)就讲完了。下面进行个小结:

  • “外部阻拦法”所运用的原理是运用工作分发机制,去改动工作分发的途径,阻拦内部容器的工作。
  • “内部阻拦法”运用的是requestDisallowInterceptTouchEvent()办法设置FLAG,不让父容器/先人容器用onInterceptTouchEvent阻拦办法。
  • 运用“内部阻拦法”仍是“外部阻拦法”,首要需求去看实践事务需求咱们怎样做,是从“内部”完成比较便利,仍是从“外部”完成比较便利。
  • 相较于“外部阻拦法”,“内部阻拦法”并没有减少工作分发的层级,因而看起来可能会愈加杂乱一些。并且也需求留意requestDisallowInterceptTouchEvent办法具体在哪个办法中运用。若两个办法都能完成终究的作用,主张优先运用“外部阻拦法”。

小技巧

这儿再根据上文的工作分发机制,补充一个思路上的小技巧。调查某个View的三个办法日志时,假如有CANCEL工作,代表着上层容器必定有阻拦某个工作;假如onTouchEvent办法没有被调用,阐明必定有基层View消费掉了这个工作;假如onInterceptTouchEvent没被调用,却调用了onTouchEvent办法,阐明基层有View调用了requestDisallowInterceptTouchEvent办法。

总结

这篇文章首要介绍了Android工作分发机制,经过Demo演示了各类工作是怎样分发的,并经过修改办法回来值演示如何工作阻拦后的分发途径。在了解工作分发机制后,进而对常见的滑动抵触给出“内部阻拦法”与“外部阻拦法”两种通用处理方案。

假如你觉得这篇文章跟某篇博客或书籍中的内容很类似,不必怀疑,纯属不巧合(chao xi)。
由于我便是参阅他们的内容去做的Demo,一起我也主张还不了解的同学也能自己做个Demo去加深这块知识的理解。

文章不足之处,还望大家多多海涵,多多指点,先行谢过!

参阅文章

《Android开发艺术探究》——任玉刚

一步步探究学习Android Touch工作分发传递机制

解惑requestDisallowInterceptTouchEvent