在「 [Android]接触、滑动与嵌套滑动(一)和(二)」中,咱们现已介绍了接触的根本知识,而滑动则和接触是有很大相关的,由于滑动本身便是由屏幕的屡次采样得到的,而后Android的视图系统将会将咱们的滑动事情逐层下发到方针View上,然后由对应的View进行消费。

咱们知道,事情会随着View Hierarchy从Root“冒泡”到Leaf结点,至此一切的视图都有时机「看到」一切的ACTION_DOWN事情,所以,理论上它们有相等的时机去承受ACTION_DOWN和后续的事情。

可是这种预定的机制在一些时候并不是咱们想要的。

此前的例子中,咱们用了ScrollView中嵌套一个Button来展现一个ACTION_CANCEL,首先手指按下屏幕,Button会承受到ACTION_DOWN,尔后在Button等候后续的ACTION_UP来生成一次点击事情(Click)。

可是实际状况下,咱们按下屏幕的时候,并不一定便是想按下ScrollView中的Button,触发了ACTION_DOWN之后,咱们或许这时候犹豫了,转而去上下滑动ScrollView。所以这时候ScrollView便会阻拦某一个形成滑动的ACTION_MOVE,并向下发送一个ACTION_CANCEL。CANCEL的含义就在于此,事情被取消了,换句话来说是被其他的父视图剥夺了。

这个ACTION_CANCEL,咱们在[Android]接触、滑动与嵌套滑动(二)几种场景下的事情处理剖析和调试 – ()此前提到过,是由ACTION_MOVE对应的MotionEvent经过设置类型修改后下发的,它们对应的是同一个MotionEvent对象。

明显,即便在Button接收了ACTION_DOWN之后,ScrollView还在对后续的ACTION_MOVE不断地监听,达到某一个条件之后便切断原先的事情传递链条,将终究的事情承受者修改为自己,消费接下来的事情。

可是这样就会导致原先属于Button的事情被剥夺了,ACTION_MOVE是给ScrollView,仍是Button,这就形成了一次不合,可是这不用咱们去处理,由于ScrollView会观察一切的ACTION_MOVE,并在适宜的时机去阻拦ACTION_MOVE事情,以处理这一次的不合。

可是假如咱们不期望得到上述的处理方案,咱们期望手指只要按下 + 抬起,便一定能呼应Click事情,不管手指拖动到什么方位都会由Button呼应事情,那么明显事情传递机制的处理方案便现已不契合咱们的需求了,咱们就要处理ScrollView和Button之间的一次抵触。

事情分发机制在产生了不合之后,现有机制做出了错误的挑选,与咱们预期相反,这就变成了滑动的抵触。

1.ScrollView与TouchSlop

从上面的内容中,咱们知道,抵触的来历主要便是:

父View不合时宜地阻拦了子View的某一个事情导致子View的行为没有依照预期的行为进行下去。

其实这种抵触在开发中是比较常见的。

假如你运用ScrollView嵌套ScrollView去开发一个表里嵌套的可滑动视图,导致内层视图无法滑动。这便是由于表里两个ScrollView,默认状况下具有相同的滑动条件的判别,它们都会监听一切的ACTION_MOVE。由于事情冒泡模型的存在:

[Android]触摸、滑动与嵌套滑动(三)ScrollView与滑动冲突

假如某一次的ACTION_MOVE达到了可滑动条件,一定是外层的ScrollView先拿到这次的ACTION_MOVE事情,并产生滑动偏移量,内层的ScrollVIew一定是无法滑动的。

要弄清楚其中的缘由,咱们就要先找到最底子的点:什么时候ScrollView是可滑动的

早年面的几篇文章咱们能够知道,一定是内部的ScrollView接收到ACTION_DOWN事情,然后后续的ACTION_MOVE事情被外部的ScrollView阻拦了,并下发了一个ACTION_CANCEL。所以,咱们能够从ACTION_CANCEL的下发着手,开端DEBUG,在上一篇文章中,咱们现已提过了,就不再赘述了,可是能够很轻松地定位到ACTION_CANCEL的触发代码,并往前反推,很快能够确定到OutScrollView的onInterceptTouchEvent中,它回来的是mIsBeingDragged的值,字面意思便是是否开端翻滚。咱们只需要找到置为true的状况即可。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop && ……) {
                mIsBeingDragged = true;
                mLastMotionY = y;
                // ……
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        }        
  }

在省略掉一部分的代码和条件之后,咱们能够比较清楚地看到,当yDiff > mTouchSlop的时候,就会将mIsBeingFragged置为true,这样一来在外层的ScrollView的onInterceptTouchEvent办法就会回来**true**

接下来便是回来到ViewGroup的dispatchTouchEvent办法中了,(由于是ViewGroup的dispatchTouchEvent中调用的onInterceptTouchEvent),然后就走了这段逻辑,含义便是去讲ACTION_MOVE转为ACTION_CANCEL下发给child的。

final boolean cancelChild = resetCancelNextUpFlag(target.child)
        || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
        target.child, target.pointerIdBits)) {
    handled = true;
}

对流程进行了一个大概的剖析,咱们能够提取出一个ScrollView能够滑动的根本条件,那便是

  • 某个ACTION_MOVE所对应的偏移量y的值 – 上一次ACTION_DOWN应的y的值的差值,构成的yDiff,即差值yDiff > TouchSlop,将视为一次能够滑动。

什么是TouchSlop呢?

当咱们的手指在屏幕上滑动时,实际上是屏幕进行采样,产生了一系列的ACTION_MOVE事情,彼此ACTION_MOVE事情之间会有不同的坐标点(x,y),终究这些事情会以MotionEvent的方式下发到对应方位的View上,不同的采样之间,彼此的坐标点(x,y)也会有所不同,这两个不同的点就构成了一条线,这条线便是一次滑动,这也是为什么你在开发者选项翻开滑动采样时,关于曲折的滑动轨道实际上是一段段拟合的直线:

[Android]触摸、滑动与嵌套滑动(三)ScrollView与滑动冲突

除滑动之外,点击也是一个重要的事情,只不过点击分为Click和LongClick,一个是普通点击,别的一个是长按,二者的完成根本一致,可是长按的完成比较美妙,咱们后续有时机再讲,先来看看比较简略的点击: 预想的无非便是:ACTION_DOWN + ACTION_UP就构成了一次点击事情,即按下 + 抬起。

可是实际上是ACTION_DOWN + ACTION_MOVE x n个 + ACTION_UP,即按下加手指在屏幕上逗留了0.5秒后抬起,这逗留的0.5秒,实际上屏幕现已采样了数十个ACTION_MOVE了,ACTION_MOVE就意味着滑动,可是点击的这种状况下,生成的ACTION_MOVE明显并不是咱们想要的,咱们并不想滑动,或者说开发者并不知道此时用户是想要滑动仍是点击。

所以TouchSlop应运而生,说白了便是太短的滑动不算点击。TouchSlop便是来区分这个大与小的中心值,假如两次之间的Diff > TouchSlop,就算是滑动;否则就不算。

一般来说TouchSlop的取值是8dp,详细在我的测试机上的像素值是:8 X 2.75 = 22pix,也便是说只有滑动量达到了22pix才视为一次滑动。

所以,便是在外层ScrollView上产生了大于22pix的滑动时,就会导致ACTION_CANCEL事情。ScrollView多个嵌套的状况下,永远是外层的ScrollView先拿到事情,所以永远是外层会先开端滑动。

2. 抵触的处理

那么如何处理这种抵触呢?

2.1 办法一 子View恳求父View不要阻拦事情

抵触的原因实际上咱们之前现已提过了,便是外层的ScrollView会阻拦掉形成滑动的那一次的ACTION_MOVE。 假如咱们期望处理这种抵触,无非便是外层ScrollView不去阻拦这一次的滑动,这儿咱们有办法能够直接调用,直接重写内层的ScrollView,并在接收到ACTION_DOWN的时候,调用:

parent.requestDisallowInterceptTouchEvent(true)

恳求父View不要阻拦自己接下来的事情即可。

一般来说,父View只要契合既有的时间传递机制的状况下,在子View调用了上述的办法之后,将不会再进行阻拦,这样一来,嵌套在内层的ScrollView就能够滑动了。

这便是咱们的处理办法之一:子View恳求父View不要阻拦

2.2 办法二 父View主动抛弃事情

办法一是由子View触发的,办法二便是由父View主动触发的,父View能够使用一些办法判别子View在一些场景下是否能够滑动,假如能够则不阻拦事情,不能够再去阻拦,以纵向为例:

bool canScrollVertically(direction)

同时ScrollView也是经过它来判别本身是否能滑动的,假如不能滑动它将不会去阻拦事情。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // ……
    /*
     * Don't try to intercept touch if we can't scroll anyway.
     */
    if (getScrollY() == 0 && !canScrollVertically(1)) {
        return false;
    }
    // ……

可是在ScrollView的onInterceptTouchEvent中,并没有调用child.canScrollVertically()来判别子VIew是否能够滑动,咱们能够修改一下,自定义一个OutScrollView,重写onInterceptTouchEvent()

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    val direction = 1
    val linearLayoutScrollable = children.first().canScrollVertically(direction)//false
    val innerScrollViewScrollable =
        children.first().findViewById<ScrollView>(R.id.innerScrollView)
            .canScrollVertically(direction)//true
    if (linearLayoutScrollable || innerScrollViewScrollable) {
        return false
    }
    return super.onInterceptTouchEvent(ev)
}

这仅仅举个例子,你会发现这样只有InnerScrollView能够滑动,由于每次都去检测InnerScrollView了,即便你手指在其他区域滑动也会去检测InnerScrollView,这是不合理的,你能够依据ACTION_DOWN、MOVE事情去自定义它们的行为。而且当InnerScrollView滑到底时,innerScrollViewScrollable回来了false,将不会再走return false,外层的ScrollView又开端阻拦其他事情了。

3. 总结

假如你依照2.中的办法,去自己完成了一下这种ScrollView嵌套滑动抵触的处理,你会发现尽管InnerScrollView中的内容,尽管是能够滑动了,可是你依然会觉得滑动起来有些异常,由于InnerScrollView滑动到底之后,假如手指不抬起,即便你再怎么样去向下滑动,也不会有反应。必需要手指先抬起,再重新按下、滑动,外层的OutScrollView才会滑动。

由于咱们仅仅简略地处理了这其中的滑动抵触,让功能变得“可用”。可是在其他的常见的滑动控件中,咱们的滑动或许是这样的:

[Android]触摸、滑动与嵌套滑动(三)ScrollView与滑动冲突

即内部的控件滑动的溢出值会带动外部可翻滚视图进行滑动,这便是「嵌套滑动」的领域。当然这两种UI是不同的需求,只不过嵌套滑动是属于3.中办法一的延伸,受限于篇幅的问题,咱们将在下一篇中介绍它。