最近一段时间,一直都在忙于找工作。虽然花费了三个月的时间,但是结果并不是很美满。想去大厂、想去好公司、想遇见更厉害的人的愿望还是没有实现。或许是自己不够强大,或许自己不够努力,或许需要一定运气。生活总是需要经历一些波折。没有谁总是能一帆风顺。接下来一段时间内,会继续更新文章。希望大家能继续关注。Thanks~
前言
在Lollipop(Android 5.0)时,谷歌推出了NestedScrolling机制,也就是嵌套滑动。本文将带领大家一起去了解谷歌对该机制的设计。通过阅读该文,你能了解如下知识点:
- 传统事件分发机制中嵌套滑动的实现与局限性。
- 谷歌NestedScrolling机制的原理实现。
- NestedScrollingChild与NestedScrollingParent接口的调用关系。
- NestedScrollingChild2与NestedScrollingParent2接口出现的意义。
该博客中涉及到的示例,在NestedScrollingDemo项目中都有实现,大家可以按需自取。
传统事件机制处理嵌套滑动的局限性
在传统的事件分发机制中,当一个事件产生后,它的传递过程遵循如下顺序:父控件->子控件,事件总是先传递给父控件,当父控件不对事件拦截的时候,那么当前事件又会传递给它的子控件。一旦父控件需要拦截事件,那么子控件是没有机会接受该事件的。
因此当在传统事件分发机制中,如果有嵌套滑动场景,我们需要手动解决事件冲突。具体嵌套滑动例子如下图所示:
上述效果实现,请参看NestedTraditionLayout.java
想要实现上图效果,在传统滑动机制中,我们需要以下几个步骤:
- 我们需要调用父控件中onInterceptTouchEvent方法来拦截向上滑动。
- 当父控件拦截事件后,需要控制自身的onTouchEvent处理滑动事件,使其滑动至HeaderView隐藏。
- 当HeaderView滑动至隐藏后,父控件就不拦截事件了,而是交给内部的子控件(RecyclerView或ListView)处理滑动事件。
使用传统的事件拦截机制来处理嵌套滑动,我们会发现一个问题,就是整个嵌套滑动是不连贯的。也就是当父控件滑动至HeaderView隐藏的时候,这个时候如果想要内部的(RecyclerView或ListView)处理滑动事件。只有抬起手指,重新向上滑动。
熟悉事件分发机制的朋友应该知道,之所以产生不连贯的原因,是因为父控件拦截了事件,所以同一事件序列的事件,仍然会传递给父控件,也就会调用其onTouchEvent方法。而不是调用子控件的onTouchEvent方法。
NestedScrolling机制简介
为了实现连贯的嵌套滑动,谷歌在Lollipop(Android 5.0)
时,推出了NestedScrolling机制。该机制并没有脱离传统的事件分发机制,而是在原有的事件分发机制之上,为系统的自带的ViewGroup和View都增加了手势滑动
与处理fling
的方法。同时为了兼容低版本(5.0以下,View与ViewGroup是没有对应的API),谷歌也在support v4
包中也提供了如下类与接口进行支撑:
父控件需要实现的接口与使用到的类:
- NestedScrollingParent(接口)
- NestedScrollingParent2(也是接口并继承NestedScrollingParent)
- NestedScrollingParentHelper(类)
子控件需要实现的接口与使用到的类:
- NestedScrollingChild(接口)
- NestedScrollingChild2(也是接口并继承NestedScrollingChild)
- NestedScrollingChildHelper(类)
需要注意的是,如果你的Android平台在5.0以上,那么你可以直接使用系统ViewGoup与View自带的方法。但是为了向下兼容,建议还是使用support v4包提供的相应接口来实现嵌套滑动。下文也会着重讲解这些接口的的使用方式与方法说明。
NestedScrollingParent与NestedScrollingChild接口介绍
在了解嵌套滑动具体的使用方式之前,我们需要了解父控件与子控件对应接口中方法的说明。这里大家可以先忽略掉NestedScrollingParent2与NestedScrollingChild2接口,因为这两个接口是为了解决之前对嵌套滑动处理fling效果的Bug。所以对于目前阶段的我们只需要了解基础的嵌套滑动规则就够了。关于NestedScrollingParent2与NestedScrollingChild2接口相关的知识点,会在下文具体描述。那现在我们就先看看基础的接口的方法介绍吧。
NestedScrollingParent
如果采用接口的方式实现嵌套滑动,我们需要父控件要实现NestedScrollingParent接口。接口具体方法如下:
1 | /** |
NestedScrollingChild接口介绍
如果采用接口的方式实现嵌套滑动,子控件需要实现NestedScrollingChild接口。接口具体方法如下:
1 | /** |
谷歌嵌套滑动的方法调用设计
通过上文,我相信大家大概基本了解了NestedScrollingParent与NestedScrollingChild两个接口方法的作用,但是我们并不知道这些方法之间对应的关系与调用的时机。那么现在我们一起来分析谷歌对整个嵌套滑动过程的实现与设计。为了处理嵌套滑动,谷歌将整个过程分为了以下几个步骤:
- 1.当父控件不拦截事件,子控件收到滑动事件后,会先询问父控件是否支持嵌套滑动。
- 2.如果父控件支持嵌套滑动,那么父控件进行预先滑动。然后将处理剩余的距离交由给子控件处理。
- 3.子控件收到父控件剩余的滑动距离并滑动结束后,如果滑动距离还有剩余,又会再问一下父控件是否需要再继续消耗剩下的距离。
- 4.如果子控件产生了fling,会先询问父控件是否
预先拦截
fling。如果父控件预先拦截。则交由给父控件处理。子控件则不处理fling
。 - 5.如果父控件不预先拦截fling, 那么会将fling传给父控件处理。同时子控件也会处理fling。
- 6.当整个嵌套滑动结束时,子控件通知父控件嵌套滑动结束。
对fling效果不熟悉的小伙伴可以查看该篇文章—RecyclerView之Scroll和Fling
再结合之前我们对NestedScrollingParent与NestedScrollingChild中的方法。我们可以得到相应方法之间的调用关系。具体如下图所示:
子控件方法调用时机
当我们了解了接口的调用关系后,我们需要知道子控件对相应嵌套滑动方法的调用时机。因为在低版本下,子控件向父控件传递事件需要配合NestedScrollingChildHelper类与NestedScrollingChild接口一起使用。由于篇幅的限制。这里就不向大家介绍如何构造一个支持嵌套滑动的子控件了。在接下来的知识点中都会在NestedScrollingChildView
的基础上进行讲解。希望大家可以结合代码与博客一起理解。
在接下来的章节中,会先讲解谷歌在NestedScrollingParent与NestedScrollingChild接口下嵌套滑动的API设计。关于NestedScrollingParent2与NestedScrollingChild2接口会单独进行解释。
子控件startNestedScroll方法调用时机
根据嵌套滑动的机制设定,子控件如果想要将事件传递给父控件,那么父控件是不能拦截事件的
。当子控件想要将事件交给父控件进行预处理,那么必然会在其onTouchEvent方法,将事件传递给父控件。需要注意的是当子控件调用startNestedScroll方法时,只是判断是否有支持嵌套滑动的父控件,并通知父控件嵌套滑动开始。这个时候并没有真正的传递相应的事件。故该方法只能在子控件的onTouchEvent方法中事件为MotionEvent.ACTION_DOWN时调用。伪代码如下所示:
1 | public boolean onTouchEvent(MotionEvent event) { |
那子view仅仅通过startNestedScroll方法是如何找到父控件并通知父控件嵌套滑动开始的呢?我们来看看startNestedScroll方法的具体实现,startNestedScroll方法内部会调用NestedScrollingChildHelper的startNestedScroll方法。具体代码如下所示:
1 | public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { |
从代码中我们可以看出,当子控件支持嵌套滑动时,子控件会获取当前父控件,并调用ViewParentCompat.onStartNestedScroll
方法。我们继续查看该方法:
1 | public static boolean onStartNestedScroll(ViewParent parent, View child, View target, |
观察代码,我们可以发现,当父控件实现NestedScrollingParent接口后,会走IMPL.onStartNestedScroll方法,我们继续跟下去:
1 | public boolean onStartNestedScroll(ViewParent parent, View child, View target, |
最后会调用ViewParetCompat中的onStartNestedScroll方法,该方法最终会调用父控件的onStartNestedScroll方法。绕了一大圈,也就调用了父控件的onStartNestedScroll来判断是否支持嵌套滑动。
那现在我们再回到子控件的startNestedScroll方法中。我们可以得知,如果当前父控件不支持嵌套滑动,那么会一直向上寻找,直到找到为止。如果仍然没有找到,那么接下来的子父控件的嵌套滑动方法都不会调用。如果子控件找到了支持嵌套滑动的父控件,那么接下来会调用父控件的onNestedScrollAccepted方法,表示父控件接受嵌套滑动。
子控件dispatchNestedPreScroll方法调用时机
当父控件接受嵌套滑动后,那么子控件需要将手势滑动传递给父控件,因为这里已经产生了滑动,故会在onTouchEvent中筛选MotionEvent.ACTION_MOVE中的事件,然后调用dispatchNestedPreScroll方法这些将滑动事件传递给父控件。伪代码如下所示:
1 | private final int[] mScrollConsumed = new int[2];//记录父控件消耗的距离 |
在上述代码中,dy与dx分别为子控件竖直与水平方向上的距离,int[] mScrollConsumed
竖直用于记录父控件消耗的距离。那么当我们调用dispatchNestedPreScroll的方法,将事件传递给父控件进行消耗时,那么子控件实际能处理的距离为:
- 水平方向: dx -= mScrollConsumed[0];
- 竖直方向: dy -= mScrollConsumed[1];
接下来,我们继续查看dispatchNestedPreScroll的方法。
在dispatchNestedPreScroll方法内部会调用NestedScrollingChildHelper的dispatchNestedPreScroll方法具体代码如下:
1 | public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, |
在该方法中,会先判断获取当前嵌套滑动的父控件。如果父控件不为null且支持嵌套滑动,那么接下来会调用ViewParentCompat.onNestedPreScroll()方法。代码如下所示:
1 | public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, |
观察代码最终会调用父控件的onNestedPreScroll方法。需要注意的是,父控件可能会将子控件传递的滑动事件全部消耗。那么子控件就没有继续可处理的事件了。
onNestedPreScroll方法在嵌套滑动时判断父控件的滑动距离时尤为重要。
子控件dispatchNestedScroll方法调用时机
当父控件预先处理滑动事件后,也就是调用onNestedPreScroll方法并把消耗的距离传递给子控件后,子控件会获取剩下的事件并消耗。如果子控件仍然没有消耗完,那么会调用dispatchNestedScroll将剩下的事件传递给父控件。如果父控件不处理。那么又会传递给子控件进行处理。伪代码如下所示:
1 | private final int[] mScrollConsumed = new int[2];//记录父控件消耗的距离 |
在上述代码中,因为子控件消耗多少距离,是由子控件进行决定的,所以将这些方法抽象了出来了。在子控件的dispatchNestedScroll方法内部会调用NestedScrollingChildHelper的dispatchNestedScroll方法,具体代码如下所示:
1 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, |
该方法内部会调用ViewParentCompat.onNestedScroll方法。继续跟踪最终会调用ViewParentCompat中非静态的的onNestedScroll方法,代码如下所示:
1 | public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, |
该方法中,最终会调用父控件的onNestedScroll方法来处理子控件剩余的距离。
子控件stopNestedScroll方法调用时机
当整个事件序列结束的时候(当手指抬起或取消滑动的时候),需要通知父控件嵌套滑动已经结束。故我们需要在OnTouchEvent中筛选MotionEvent.ACTION_UP、MotionEvent.ACTION_CANCEL中的事件,并通过stopNestedScroll()方法通知父控件。伪代码如下所示:
1 | public boolean onTouchEvent(MotionEvent event) { |
在stopNestedScroll()方法中,最终会调用父控件的onStopNestedScroll()方法,这里就不做更多的分析了。
子控件fling分发时机
现在就剩下最后一个嵌套滑动的方法了!!!对!就是fling。在了解子控件对fling的处理过程之前,我们先要知道fling代表什么样的效果。在Android系统下,手指在屏幕上滑动然后松手,控件中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做fling。也就是我们需要在onTouchEvent方法中筛选MotionEvent.ACTION_UP的事件并获取需要的滑动速度。伪代码如下:
fling的中文意思为抛、扔、掷。
1 | public boolean onTouchEvent(MotionEvent event) { |
这里就不在对fling效果是怎么分发到父控件进行解释啦~~。一定要结合NestedScrollingChildView类进行理解。那么假设大家都看了源码,那么我们可以得到如下几点:
- 子控件dispatchNestedPreFling最终会调用父控件的onNestedPreFling方法。
- 子控件的dispatchNestedFling最终会调用onNestedFling方法。
- 如果父控件的拦截fling(也就是onNestedPreFling方法返回为
true
)。那么子控件是没有机会处理fling的。 - 如果父控件
不
拦截fling(也就是onNestedPreFling方法返回为false
),则父控件会调用onNestedFling方法与子控件同时处理fling。 - 当父控件与子控件同时处理fling时,子控件会立即调用stopNestedScroll方法通知父控件嵌套滑动结束。
NestedScrollingChild2与NestedScrollingParent2简介
最后一个知识点了,大家加油啊!!!!!!
在本文章前半部,我们都是围绕NestedScrollingChild与NestedScrollingParent进行讲解。并没有提及NestedScrollingChild2与NestedScrollingParent2接口。那这两个接口是处理什么的呢?这又要回到上文我们提到的NestedScrollingChild处理fling时的流程了,在谷歌之前的NestedScrollingParent与NestedScrollingChild的API设计中。并没有考虑如下问题:
- 父控件根本不可能知道子控件是否fling结束。子控件只是在
ACTION_UP
中调用了stopNestedScroll方法。虽然通知了父控件结束嵌套滑动,但是子控件仍然可能处于fling中。 - 子控件没有办法将部分fling传递给父控件。父控件必须处理整个fling。
而使用NestedScrollingChild2与NestedScrollingParent2
这两个接口,子控件就能将fling传递给父控件,并且父控件处理了部分fling后,又可以将剩余的fling再传递给子控件。当子控件停止fling时,通知父控件fling结束了。这和我们之前分析的嵌套滑动是不是很像呢?直接讲知识点,大家不是很好理解,看下面这个例子:
上述效果实现,请参看NestedScrollingParentLayout.java
在上面例子中是实现了NestedScrollingChild(NestedScrollView或RecyclerView等)与NestedScrollingParent接口的嵌套滑动,我们可以明显的看出,当我们手指快速向下滑动并抬起的时,子控件将fling分发给父控件,因为处理的距离不同,这个时候父控件已经处理滑动并fling结束,而内部的子控件(RecyclerView或NestedScrollView还在滚动,这种给我们的感觉就非常不连贯,好像每个控件在独自滑动。
在同样的滑动条件下,实现了NestedScrollingChild2(NestedScrollView或RecyclerView等)与NestedScrollingParent2接口的嵌套滑动.看下面的例子:
上述效果实现,请参看NestedScrollingParent2Layout.java
观察上图,我们能发现父控件与子控件(RecyclerView或NestedScrollView)的滑动更为顺畅与合理。那接下来我们看看谷歌对其的设计。
NestedScrollingChild2与NestedScrollingParent2分别继承了NestedScrollingChild与NestedScrollingParent,在继承的接口部分方法上增加了type参数。其中type的取值为TYPE_TOUCH(0)
、TYPE_NON_TOUCH(1)
。用于区分手势滑动与fling。具体差异如下图所示:
图片较大,可能阅读不清晰,建议放大观看。
谷歌在fling的处理上也与之前的NestedScrollingChild与NestedScrollingParent
有所差异,在onTouchEvent方法中的逻辑进行了修改,伪代码如下所示:
1 |
|
当子控件手指抬起的时候,我们发现是调用stopNestedScroll(ViewCompat.TYPE_TOUCH
)的方式来通知父控件当前手势滑动
已经结束,继续查看fling方法。伪代码如下所示:
1 | private boolean fling(int velocityX, int velocityY) { |
从代码中,我们可以看见,在新接口的处理逻辑中,还是会调用dispatchNestedPreFling与dispatchNestedFling方法。也就是之前的处理fling方式是没有被替代的,但是这并不说明没有变化。我们发现子控件调用了startNestedScroll方法,并设置了当前类型为TYPE_NON_TOUCH(fling),那么也就是说,在实现了NestedScrollingParent2
的父控件中,我们可以在onStartNestedScroll方法中知道当前的滑动类型到底是fling,还是手势滑动。我们继续查看doFling方法。伪代码如下:
1 | /** |
doFling方法其实很简单,就是调用OverScroller的fing方法,并调用postInvalidate方法(为了帮助大家理解,这里并没有采用 postOnAnimation()的方式
)。其中OverScroller的fing方法主要是根据当前传入的速度,计算出在匀减速情况下,实际运动的距离。这里也就解释了为什么,在只有速度的情况下,子控件可以将fling传递给父控件,因为速度最后变成了实际的运动距离。
这里就不对Scroller的fling方法中如何将速度转换成距离的算法进行讲解了。不熟悉的小伙伴可以自行谷歌或百度。
熟悉Scroller的小伙伴一定知道,为了获取到fling所产生的距离,我们需要调用postInvalidate()方法或Invalidate()方法。同时在子控件的computeScroll()方法中获取实际的运动距离。那么也就说最终的子控件的fing的分发实际是在computeScroll()方法中。继续查看该方法的伪代码:
1 | public void computeScroll() { |
观察代码,我们可以发现,子控件中分发fling的方式在与之前分发手势滚动的逻辑非常一致。
- 产生fing时,调用带
type(TYPE_NON_TOUCH)
参数的dispatchNestedPreScroll方法,判断父控件是否处理fling事件。 - 如果父控件处理,那么父控件消耗后,子控件再消耗剩余的距离
- 子控件消耗后,如果还有剩余的距离,则调用带
type(TYPE_NON_TOUCH)
参数的dispatchNestedScroll方法,将剩下的距离传递给父控件。 - 当子控件fling结束时,则调用stopNestedScroll(TYPE_NON_TOUCH)方法,通知父控件fling已经结束。
那么也就是说,NestedScrollingChild2与NestedScrollingParent2接口,只是在原有的方法中增加了TYPE_NON_TOUCH
参数来让父控件区分到底是手势滑动还是fling。不得不佩服谷歌大佬的设计。不仅兼容还解决了实际的问题。
总结
通过上文的分析,我们能得到如下结论:
- NestedScrolling(嵌套滑动)机制是建立在原有的事件机制之上,要实现嵌套滑动,父控件是不能拦截事件。
- NestedScrolling(嵌套滑动)机制中接口要成对使用。如NestedScrollingChild2与NestedScrollingParent2成对。NestedScrollingChild与NestedScrollingParent成对。
- 当我们需要子控件分发fling给父控件时,我们需要使用NestedScrollingChild2与NestedScrollingParent2。并在相应的方法中通过type(
TYPE_TOUCH(0)
、TYPE_NON_TOUCH(1)
),来判断是手势滑动还是fling。
最后
到现在整个NestedScrolling(嵌套滑动)机制就讲解完毕了,在接下来的文章中,会讲解相应嵌套滑动例子、CoordinatorLayout与Behavior、自定义Behavior等相关知识点,如果大家有兴趣的话,可以持续关注~。谢谢大家花时间阅读文章啦。Thanks