• [Android]触摸、滑动与嵌套滑动(一)事情与翻滚 – ()
  • [Android]触摸、滑动与嵌套滑动(二)几种场景下的事情处理分析和调试 – ()
  • [Android]触摸、滑动与嵌套滑动(三)ScrollView与滑动冲突 – ()

怎样完成舒畅的嵌套滑动

当咱们ScrollView套着ScrollView会出现一个问题,假设手指一次性滑动了500pix,内层ScrollView耗费了300pix,即便还余下200pix也不能持续滑动了,而是必须抬起手指,再滑动一次才能够让外层的ScrollView被滑动:

而咱们希望的是,当ScrollView滑动了300pix之后,将余下的200pix交给父View,也便是外层的ScrollView进行滑动:

[Android]触摸、滑动与嵌套滑动(四)事件传递机制与嵌套滑动

这按照传统的事情传递机制似乎是不可行的,由于现行的事情传递机制规定了:一组事情(DOWN/MOVE/UP)必须由一个View消费完结,除非外层的视图自动阻拦,才会出现外层视图不承受ACTION_DOWN只承受ACTION_MOVE的状况。

而现在的问题是,ScrollView嵌套ScrollView的时分,内层的ScrollView底子无法滑动:

[Android]触摸、滑动与嵌套滑动(四)事件传递机制与嵌套滑动

所以在之前的内容中,咱们的处理办法是,在外层的ScrollView的onInterceptTouchEvent()依据必定状况回来false,旨在告知外层ScollView假如内层视图能够滑动则不阻拦事情,可是咱们难以操控什么时分能够让外层View从头取得事情。

假设,咱们希望在内层ScrollView无法再向上滑动的时分,即滑动量达到300pix的时分,将事情交给父View处理,咱们能够有两种解决方案

  1. 父View在收到子View某种信号的时分自动去阻拦子View的事情。就和之前ScrollView和Button嵌套的时分,Button收到ACTION_DOWN的时分那样。
  2. 不上交事情,只把滑动量交给父View来处理,余下的200pix经过某种办法让父View换一种办法来消费这个滑动量,而不是事情。

显然,2是更为合理的,咱们看看之前的这一种效果图,咱们能够发现在父View接收了余下的滑动量之后,咱们从头下滑,此刻立即呼应的是子View下滑,而不是父View下滑。而1中,假如咱们去阻拦子View事情后,再从头下发,就目前的滑动模型来说,是非常困难的。

[Android]触摸、滑动与嵌套滑动(四)事件传递机制与嵌套滑动

1. NestedScrollView是怎样做的

咱们能够先监听一下官方NestedScrollView嵌套时,它们的事情是怎样传递的,就能够知道在派发剩余滑动量时,消费事情的究竟是子View仍是父View。

仍是比较明显的,即便嵌套滑动发生的时分,消费事情的View仍然是内层的NestedScrollView:

Out::dispatchTouchEvent,true
// 以下为一组(由于是在super.onTouchEvent之后调用的打印,所以看着是反着的)
Inner::onTouchEvent,true
Inner::dispatchTouchEvent,true
Out::dispatchTouchEvent,true
//
D/rEd: Inner::onTouchEvent,true
D/rEd: Inner::dispatchTouchEvent,true
Out::dispatchTouchEvent,true

由于是先调用的super,再打印的日志,所以看着是反着的。可是要害是在Inner::onTouchEvent中回来的true,这就说明了,嵌套滑动子View只是将剩余的滑动量派发到父View,并没有抛弃事情

假如是子View滑究竟,然后抬起手指,再在子View上滑动呢?此刻承受事情的是谁?

此刻子View仍然是无法向上滑动的(由于究竟了),翻滚的是父View,可是承受事情的仍是子View,也便是说,父View在任何状况下都就将事情派发给子View了,而不是自己去滑动(当然点击事情得发生在父View和子View重合的区域)。

这就说明了,嵌套滑动相关的工具类:NestedScrollParent、NestedScrollChild等等,要解决的核心问题,便是:NestedScrollChild向NestedScrollParent派发剩余的滑动量,而Child需求处理:

  • 什么时分发生剩余滑动量(究竟/顶之后剩余的滑动量);
  • 怎样派发、怎样派发;

Parent需求处理:

  • 怎样承受剩余的滑动量;
  • 在事情传递机制大框架之外额外进行滑动;

2. NestedScrollChild与NestedScrollParent

咱们看看这两个接口对应的一些办法,首先是NestedScrollingChild3,假如你直接承继它,你要重写如下的办法:

override fun startNestedScroll(axes: Int, type: Int): Boolean
override fun stopNestedScroll(type: Int)
override fun hasNestedScrollingParent(type: Int): Boolean

这三个看着还算正常,也比较好理解。下面前两个显然是用来派发滑动量的,第三个dispatchNestedPreScroll,则为嵌套滑动操作中的父View供给了,在子View运用滑动操作之前运用部分或悉数翻滚操作的机会

override fun dispatchNestedScroll(
    dxConsumed: Int,
    dyConsumed: Int,
    dxUnconsumed: Int,
    dyUnconsumed: Int,
    offsetInWindow: IntArray?,
    type: Int,
    consumed: IntArray
) 
override fun dispatchNestedScroll(
    dxConsumed: Int,
    dyConsumed: Int,
    dxUnconsumed: Int,
    dyUnconsumed: Int,
    offsetInWindow: IntArray?,
    type: Int
): Boolean
override fun dispatchNestedPreScroll(
    dx: Int,
    dy: Int,
    consumed: IntArray?,
    offsetInWindow: IntArray?,
    type: Int
): Boolean

而Parent的也基本上能够从名称中看出大致的效果。

override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int)
override fun onStopNestedScroll(target: View, type: Int) 
override fun onNestedScroll(
    target: View,
    dxConsumed: Int,
    dyConsumed: Int,
    dxUnconsumed: Int,
    dyUnconsumed: Int,
    type: Int,
    consumed: IntArray
)
override fun onNestedScroll(
    target: View,
    dxConsumed: Int,
    dyConsumed: Int,
    dxUnconsumed: Int,
    dyUnconsumed: Int,
    type: Int
)
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int)

常用的组件中,NestedScrollView分别完成了NestedScrollParent和NestedScrollChildren;RecyclerView则完成了NestedScrollChild,而CoordinatorLayout则完成了NestedScrollParent,后续的阅读能够以这三个组件的对这俩个接口的运用为主;

此外,还有两个重要的类,在上述两个接口的注释中,告知咱们:

Classes implementing this interface should create a final instance of a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.

大致意思是,咱们假如要运用NestedScrollParent,咱们需求创立一个final的NestedScrollingParentHelper的实例,运用其间的一些办法来代理掉原先View、ViewGroup中的同名办法,同样地,NestedScrollChild也有这么段话,只不过实例变成了:NestedScrollingChildHelper。

3. RecyclerView是怎样完成NestedScrollChild的

RecyclerView在大多数的场景下以LinearLayoutManager的形式出现,偶尔也以表格、瀑布流的形式出现,详细的取决于LayoutManager中的自定义,它是支持嵌套滑动的,也便是说,它会将剩余的偏移量下发给NestedScrollParent。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3

NestedScrollingChild2和3一共有6个办法需求重写,可是重写的内容悉数是运用NestedScrollingChildHelper中的同签名办法替换掉了,比如:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow, int type) {
    return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
    getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
……

已然悉数托付给了NestedScrollingChildHelper,那么咱们滑动量的派发的关注点,又能够缩小一些了。

4. 一次嵌套滑动

NestedScrollingChildHelper中,主要是针对NestedScrollChild中的重写的办法,进行一些逻辑处理,以NestedcScrollChild开端嵌套滑动的startNestedScroll为例:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

完成判别一下是否已经在嵌套滑动的进程中,假如已经在了则直接回来。

否则,从View Hierarchy上的当时节点开端,去查找嵌套滑动的:NestedScrollParent,并企图让它进行消费事情。

这儿又来了一个新的类:ViewParentCompat,其间声明了很多的静态办法,用于处理一些兼容性的滑动。以ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)的调用为例:

public static boolean onStartNestedScroll(@NonNull ViewParent parent, @NonNull View child,
        @NonNull View target, int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
        // First try the NestedScrollingParent2 API
        return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
        // Else if the type is the default (touch), try the NestedScrollingParent API
        if (Build.VERSION.SDK_INT >= 21) {
            try {
                return Api21Impl.onStartNestedScroll(parent, child, target, nestedScrollAxes);
            } catch (AbstractMethodError e) {
                Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                        + "method onStartNestedScroll", e);
            }
        } else if (parent instanceof NestedScrollingParent) {
            return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes);
        }
    }
    return false;
}

其间,以parent可能为NestedScrollParent2选择直接调用2中对应的parent的onStartNestedScroll进行嵌套滑动;或者是NestedScrollParent来处理嵌套滑动。Api21Impl中,由于后续的ViewParent做了兼容性的处理,直接调用ViewParent下的同名办法即可:

@DoNotInline
static boolean onStartNestedScroll(ViewParent viewParent, View view, View view1, int i) {
    return viewParent.onStartNestedScroll(view, view1, i);
}

而其他状况下,则去调用NestedScrollingParent接口完成类下的同名办法。

回到主干部分,ViewParentCompat.onNestedScrollAccepted中的调用和上面的onStartNestedScroll大同小异。

……
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
    setNestedScrollingParentForType(type, p);
    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
    return true;
}
……

onNestedScrollAccepted办法为视图及其超类供给了为嵌套翻滚履行初始配置的机会,比如在NestedScrollView这个NestedScrollParent中:

@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
        int type) {
    mParentHelper.onNestedScrollAccepted(child, target, axes, type);
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}

注意,这儿又调用到了最开端**startNestedScroll** ,(以A->B->C为例)由于嵌套滑动的进程,并不是只在BC之间进行的,也可能是C和A之间的嵌套。

剩余滑动量的派发的进程如下:NestedScrollingChildHelper的dispatchNestedScrollInternal中:

// 有删减
private boolean dispatchNestedScrollInternal(
        int dxConsumed, 
        int dyConsumed,
        int dxUnconsumed, 
        int dyUnconsumed, 
        @Nullable int[] offsetInWindow,
        @NestedScrollType int type, 
        @Nullable int[] consumed
        ) {
        if(!滑动可用){
           return false;
        }
        final ViewParent parent = getNestedScrollingParentForType(type);
        // 记录View和Window的初始偏移量
        val startX = offsetInWindow[0]
        val startY = offsetInWindow[1];
        // 开端滑动
       ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
        // 重置滑动偏移量
        if (offsetInWindow != null) {
            mView.getLocationInWindow(offsetInWindow);
            offsetInWindow[0] -= startX;
            offsetInWindow[1] -= startY;
        }
        return true;
}

要害就在于**ViewParentCompat.onNestedScroll(parent, mView,dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);** ,假如你认真看过之前的start是怎样运作的,那么这儿面的内容你应该能猜到,咱们直接看NestedScrollParent怎样处理的:

// NestedScrollView中:
private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    if (consumed != null) {
        consumed[1] += myConsumed;
    }
    final int myUnconsumed = dyUnconsumed - myConsumed;
    mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}

无非便是依据未消费的滑动量:dyUnconsumed,运用scrollBy对当时位置进行一个相对距离的滑动。

5. 总结

上面已经简略的分析过NestedScrollXXX组件是怎样完成一次嵌套滑动的了,作为官方供给的组件,由于要兼容非常多种状况和api,所以现在读起来会显得很『厚』,这是代码迭代的必然结果,所以怎样从中提炼出一个详细的,嵌套滑动的模型才是要害的:

总结的几件事情是:

  1. 嵌套滑动的Child要处理滑动,一起需求将剩余的滑动上发给Parent;
  2. Parent默许状况下将所有的事情都下发给Child处理;
  3. Parent要接收额外的滑动状况,使用scrollBy在onTouchEvent以外进行视图的滑动;

归根到底地,嵌套滑动用一句话总结,便是:父View不阻拦ACTION_DOWN事情,子View全权消费事情,而且子View会依据必定的状况下发剩余的滑动偏移量给父View,而父View则经过子View上发的「偏移量」而不是「事情」凭借scrollBy办法对「偏移量」进行滑动。 这儿的偏移量通常是子View触底未消费完的滑动量,亦或者是其它的事务交互下的偏移量,详细是什么,能够依据详细需求的UI完成来确定。

~End