以前在仿写豆瓣详情页,以及平常的一些涉及嵌套滚动的需求时,每次都须要新增自定义 View 来实现,而在 touch 事件的拦截和处理,滚动和 fling 的处理上,又有着很大的共性,为了减小以后处理相似需求的重复劳动,也为了更进一步学习 Android 提供的嵌套滚动框架,因而打造了 BehaviorScrollView
来解决嵌套滚动的共性问题。java
BehaviorScrollView
内部实现了对 touch 事件、嵌套滚动和 fling 的拦截和处理的通用逻辑,实现了 NestedScrollingParent3
和 NestedScrollingChild3
接口,支持多级嵌套(Demo 中会有一个四层嵌套的示例),支持水平和垂直方向滚动,外部能够经过实现 NestedScrollBehavior
接口来支持不一样的嵌套滚动需求。git
在讲 BehaviorScrollView
和 NestedScrollBehavior
怎么使用以前,仍是有必要介绍下 Android 是怎么处理嵌套滚动的,这部分的文章就不少了,这里只作简要介绍。github
Child
Child
处理 touch 事件,在手指滑动时产生滚动量,开始滚动自身内容Child
在处理滚动量以前,要告诉父 View 本身要开始滚动了 pre-scroll
,并一级一级的向上分发Child
此时须要计算下父级 View 还有多少滚动量没有消耗,而后开始滚动本身,并计算本身消耗了多少滚动量Child
处理完本身后,将滚动量的消耗状况向父 View 分发 after-scroll
咱们平时要处理的嵌套滚动问题也是 Grandparent
、Parent
和 Child
三种角色中的一个或多个的组合,接下来分别介绍下这三种角色分别要处理那些问题。数组
Grandparent
只须要处理子 View 的嵌套滚动事件,实现 NestedScrollingParent
(后缀的 二、3 是为了兼容更多状况进行的扩展)接口,而后根据自身需求在滚动前 pre-scroll
或 滚动后 after-scroll
,执行本身的操做。这种类型的 View 有不少,好比 NestedScrollView
、CoordinatorLayout
、SwipeRefreshLayout
等,咱们平时须要的大多数嵌套滚动需求只须要处理子 View 分发的滚动,也是这种状况。框架
Child
通常只负责处理 touch 事件,并将产生的滚动量向父 View 分发,实现 NestedScrollingChild
接口,在本身处理滚动前分发 pre-scroll
,本身处理后分发 after-scroll
。这种 View 都是些可以产生滚动的 View,好比 RecyclerView
、NestedScrollView
等。ide
Parent
相对比较复杂,即负责接收子 View 的嵌套滚动事件,还须要将其分发给本身的父 View,实现 NestedScrollingParent
和 NestedScrollingChild
接口(即当儿子有当爹),一般状况下还须要处理 touch 事件和 fling、动画等。常见的有 NestedScrollView
、SwipeRefreshLayout
等,本文介绍的 BehaviorScrollView
就属于此类。函数
同方向嵌套滚动最核心的问题是 优先级 问题,手指滑动时父 View 能够处理,子 View 也能够处理,那到底须要交给谁呢。好比常见的 SwipeRefreshLayout
嵌套 RecyclerView
。在嵌套滚动的流程中,Parent
收到 Child
的 pre-scroll
时,须要决定本身是否要处理,还要决定是先分发给 Grandparent
而后本身处理,仍是先本身处理,再分发给 Grandparent
。布局
固然,BehaviorScrollView
是不会帮你决定这些优先级的,它负责处理优先级以外的滚动量计算和分发,以及通用的 touch 事件、fling 和动画的处理,从而是咱们可以更加方便地处理优先级问题。学习
BehaviorScrollView
的使用主要是经过 setupBehavior
方法设置不一样的 NestedScrollBehavior
,从而实现不一样的优先级策略。这里就从 NestedScrollBehavior
开始,介绍它在嵌套滚动各个阶段发挥的做用。动画
interface NestedScrollBehavior {
/** * 当前的可滚动方向 */
@ViewCompat.ScrollAxis
val scrollAxis: Int
val prevView: View?
val midView: View
val nextView: View?
/** * 在 layout 以后的回调 * * @param v */
fun afterLayout(v: BehavioralScrollView) {
// do nothing
}
/** * 在 [v] dispatchTouchEvent 时是否处理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 处理,会在 dispatchTouchEvent 中直接返回 true,false -> 直接返回 false,null -> 不关心,会执行默认逻辑 */
fun handleDispatchTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/** * 在 [v] onTouchEvent 时是否处理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 处理,会直接返回 true,false -> 不处理,会直接返回 false,null -> 不关心,会执行默认逻辑 */
fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/** * 在 onNestedPreScroll 时,是否优先本身处理 * * @param v * @param scroll 滚动量 * @param type 滚动类型 * @return true -> 本身优先,false -> 本身不优先,null -> 不处理 onNestedPreScroll */
fun handleNestedPreScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
/** * 在 onNestedScroll 时,是否优先本身处理 * * @param v * @param scroll 滚动量 * @param type 滚动类型 * @return true -> 本身优先,false -> 本身不优先,null -> 不处理 onNestedPreScroll */
fun handleNestedScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
/** * 在须要 [v] 自身滚动时,是否须要处理 * * @param v * @param scroll 滚动量 * @param type 滚动类型 * @return 是否处理自身滚动,true -> 处理,false -> 不处理,null -> 不关心,会执行默认自身滚动 */
fun handleScrollSelf(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
}
复制代码
NestedScrollBehavior
提供的 scrollAxis
决定了 BehavioralScrollView
要处理的滚动方向,同时也会决定布局的方向。
BehavioralScrollView
的子 View 是由 NestedScrollBehavior
提供的 prevView
、midView
和 nextView
,会在 onLayout
时造成水平或垂直线性布局。具体来讲,BehavioralScrollView
是继承自 FrameLayout
的,在垂直布局的状况下,midView
的位置不变,prevView
会移动它的上面,nextView
移动到其下面,从而使得 BehavioralScrollView
有一个能够上下滚动的范围。布局完成后会计算滚动范围,从 preView.top
到 nextView.bottom
,而且回调 NestedScrollBehavior.afterLayout
方法。
private fun layoutVertical() {
// midView 位置不变
val t = midView?.top ?: 0
val b = midView?.bottom ?: 0
// prevView 移动到 midView 之上,bottom 和 midView 的 top 对齐
prevView?.also {
it.offsetTopAndBottom(t - it.bottom)
minScroll = it.top
}
// nextView 移动到 midView 之下,top 和 midView 的 bottom 对齐
nextView?.also {
it.offsetTopAndBottom(b - it.top)
maxScroll = it.bottom - height
}
}
复制代码
这里为何用三个 View
而不是两个或着更多呢?一方面在我涉及到的场合下,三个 View
足够用了,实在不够还能够嵌套,另外一方面,三个 View
可以比较方便地控制一些边界条件。好比在垂直滚动状况下,会在 scrollY == 0
的边界处作一些判断,调整嵌套滚动的优先级策略,判断 scrollY
是大于 0 仍是小于 0,从而判断是 nextView
滚动出来仍是 prevView
滚动出来了。若是增长到了四个以上,这种边界的判断就会变得很麻烦。
首先看下 dispatchTouchEvent
,会先回调 NestedScrollBehavior.handleDispatchTouchEvent
,返回非空的值表示 NestedScrollBehavior
已经处理了,会直接返回,空的话会在 ACTION_DOWN
时复位一些标志位,无特殊处理。
这里回调给 NestedScrollBehavior
是为了能够尽早拿到 touch 事件,这里一般会在 ACTION_UP
抬手时作一些动画或复位。
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
// behavior 优先处理,不处理走默认逻辑
behavior?.handleDispatchTouchEvent(this, e)?.also {
log("handleDispatchTouchEvent $it")
return it
}
// 在 down 时复位一些标志位,停掉 scroller 的动画
if (e.action == MotionEvent.ACTION_DOWN) {
lastScrollDir = 0
state = NestedScrollState.NONE
scroller.abortAnimation()
}
return super.dispatchTouchEvent(e)
}
复制代码
onInterceptTouchEvent
没有回调给 NestedScrollBehavior
,这里就不贴代码了,主要逻辑是只有手指在滚动方向上发生了滑动,且触点位置没有能够处理嵌套滑动的 NestedScrollingChild
才去拦截事件本身处理。
onTouchEvent
也会优先分发给 NestedScrollBehavior.handleTouchEvent
处理,默认会 ACTION_MOVE
时计算滚动量,并经过 dispatchScrollInternal
(这个方法后面再讲)进行分发,在 ACTION_UP
时进行 fling 的处理。
override fun onTouchEvent(e: MotionEvent): Boolean {
// behavior 优先处理,不处理时本身处理 touch 事件
behavior?.handleTouchEvent(this, e)?.also {
return it
}
when (e.action) {
MotionEvent.ACTION_DOWN -> {
// ...
}
MotionEvent.ACTION_MOVE -> {
// ...
dispatchScrollInternal(dx, dy, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_UP -> {
// ...
if (!dispatchNestedPreFling(vx, vy)) {
dispatchNestedFling(vx, vy, true)
fling(vx, vy)
}
}
}
// ...
}
复制代码
BehavioralScrollView
实现的 NestedScrollingParent3
和 NestedScrollingChild3
的大多数方法都不须要咱们作什么特殊处理,用 NestedScrollingParentHelper
和 NestedScrollingChildHelper
两个帮助类就能解决,能够多参考 NestedScrollView
。这里主要介绍做为 Grandparent
角色的 onNestedPreScroll
和 onNestedScroll
两个方法,顾名思义,对应上面说的 pre-scroll
和 after-scroll
两个时机。
onNestedPreScroll
会有两个重载的方法,第二个比第一个多了 NestedScrollType
参数用以区分滚动是不是 touch 事件产生的,这里统一回调到 dispatchNestedPreScrollInternal
处理。
代码逻辑比较简单,首先时回调 NestedScrollBehavior.handleNestedPreScrollFirst
判断处理的优先级,返回值有三种状况:
null
:表示不处理 pre-scroll
,这时会直接调用 dispatchNestedPreScroll
将滚动量分发给父 Viewtrue
:表示本身优先处理,这时会先调用 handleScrollSelf
(这个方法后面再讲)本身处理,而后计算未消耗的滚动量,再 dispatchNestedPreScroll
分发给父 Viewfalse
:表示父 View 优先处理,这时会先 dispatchNestedPreScroll
分发给父 View,而后计算未消耗的滚动量,再 handleScrollSelf
本身处理/** * 分发 pre scroll 的滚动量 */
private fun dispatchNestedPreScrollInternal( dx: Int, dy: Int, consumed: IntArray, type: Int = ViewCompat.TYPE_TOUCH ) {
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL ->{
val handleFirst = behavior?.handleNestedPreScrollFirst(this, dy, type)
when (handleFirst) {
true -> {
val selfConsumed = handleScrollSelf(dy, type)
dispatchNestedPreScroll(dx, dy - selfConsumed, consumed, null, type)
consumed[1] += selfConsumed
}
false -> {
dispatchNestedPreScroll(dx, dy, consumed, null, type)
val selfConsumed = handleScrollSelf(dy - consumed[1], type)
consumed[1] += selfConsumed
}
null -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
}
}
else -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
}
}
复制代码
onNestedScroll
会有三个重载方法,依次增长了 NestedScrollType
和父 View 用于记录消耗滚动量的数组 consumed
,这里会统一回调给 dispatchNestedScrollInternal
处理。
处理逻辑和 dispatchNestedPreScrollInternal
相似,先回调 NestedScrollBehavior.handleNestedScrollFirst
获得优先级,再进行分发和处理,这里再也不赘述。
/** * 分发 nested scroll 的滚动量 */
private fun dispatchNestedScrollInternal( dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int = ViewCompat.TYPE_TOUCH, consumed: IntArray = intArrayOf(0, 0) ) {
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL -> {
val handleFirst = behavior?.handleNestedScrollFirst(this, dyUnconsumed, type)
when (handleFirst) {
true -> {
val selfConsumed = handleScrollSelf(dyUnconsumed, type)
dispatchNestedScroll(dxConsumed, dyConsumed + selfConsumed, dxUnconsumed, dyUnconsumed - selfConsumed, null, type, consumed)
consumed[1] += selfConsumed
}
false -> {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
val selfConsumed = handleScrollSelf(dyUnconsumed - consumed[1], type)
consumed[1] += selfConsumed
}
null -> dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
}
}
}
}
复制代码
NestedScrollBehavior
对滚动量分发的优先级控制主要体如今 handleNestedPreScrollFirst
和 handleNestedScrollFirst
两个方法,经过 BehavioralScrollView
当前状态、滚动的距离、滚动类型和不一样策略设置不一样的优先级,从而知足不一样嵌套滚动需求。
dispatchScrollInternal
用来处理自身产生的来自 touch 事件或者 fling 的滚动量,这里实际上是处于 Child
的角色,因此在自身处理的先后都要分发嵌套滚动事件,这里复用了前面的 dispatchNestedPreScrollInternal
和 dispatchNestedScrollInternal
,在自身滚动时实现精细的优先级控制。
/** * 分发来自自身 touch 事件或 fling 的滚动量 * -> dispatchNestedPreScrollInternal * -> handleScrollSelf * -> dispatchNestedScrollInternal */
private fun dispatchScrollInternal(dx: Int, dy: Int, type: Int) {
val consumed = IntArray(2)
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL -> {
var consumedY = 0
dispatchNestedPreScrollInternal(dx, dy, consumed, type)
consumedY += consumed[1]
consumedY += handleScrollSelf(dy - consumedY, type)
val consumedX = consumed[0]
// 复用数组
consumed[0] = 0
consumed[1] = 0
dispatchNestedScrollInternal(consumedX, consumedY, dx - consumedX, dy - consumedY, type, consumed)
}
}
}
复制代码
handleScrollSelf
是真正到了自身滚动的时刻,会先回调 NestedScrollBehavior.handleScrollSelf
判断是否处理该滚动量,一样的有三种返回值:
null
表示 NestedScrollBehavior
不作特殊处理,此时 BehavioralScrollView
会根据自身是否能够滚动进行滚动,并返回消耗的滚动量true
表示处理,消耗全部的滚动量false
表示不处理,不消耗滚动量handleScrollSelf
主要用于在 BehavioralScrollView
自身滚动时作特殊处理,好比下拉刷新等不但愿 fling 的 ViewCompat.TYPE_NON_TOUCH
类型滚动形成自身的位移,有些弹性滚动的场合但愿自身的滚动带有阻尼效果等均可以在这里处理。
/** * 处理自身滚动 */
private fun handleScrollSelf(scroll: Int, @ViewCompat.NestedScrollType type: Int): Int {
// behavior 优先决定是否滚动自身
val handle = behavior?.handleScrollSelf(this, scroll, type)
val consumed = when(handle) {
true -> scroll
false -> 0
else -> if (canScrollSelf(scroll)) {
scrollBy(scroll, scroll)
scroll
} else {
0
}
}
return consumed
}
复制代码
自身的滚动最终是经过 scrollBy
实现的,经过 getScrollByX/getScrollByY
实现了边界控制。同时 scrollX/scrollY
在 0 处作了特殊处理,如 scrollY > 0
时,滚动范围是 从 0 到 maxScroll
,这和「3.1 布局」中说的边界处的特殊处理有关,须要在 scrollY
小于 0、等于 0 或大于 0 时使用不用的优先级策略。
override fun scrollBy(x: Int, y: Int) {
val xx = getScrollByX(x)
val yy = getScrollByY(y)
super.scrollBy(xx, yy)
}
/** * 根据方向计算 y 轴的真正滚动量 */
private fun getScrollByY(dy: Int): Int {
val newY = scrollY + dy
return when {
scrollAxis != ViewCompat.SCROLL_AXIS_VERTICAL -> scrollY
scrollY > 0 -> newY.constrains(0, maxScroll)
scrollY < 0 -> newY.constrains(minScroll, 0)
else -> newY.constrains(minScroll, maxScroll)
} - scrollY
}
复制代码
fling 和动画都是经过 Scroller
处理的,fling 须要 VelocityTracker
帮助类在 touch 事件中记录手指移动速度。
这里须要介绍 BehavioralScrollView
保存当前状态的一个属性 NestedScrollState
,方便嵌套滚动事件的优先级判断。
/** * 用于描述 [BehavioralScrollView] 正处于的嵌套滚动状态,和滚动类型 [ViewCompat.NestedScrollType] 共同描述滚动量 */
@IntDef(NestedScrollState.NONE, NestedScrollState.DRAGGING, NestedScrollState.ANIMATION, NestedScrollState.FLING)
@Retention(AnnotationRetention.SOURCE)
annotation class NestedScrollState {
companion object {
/** * 无状态 */
const val NONE = 0
/** * 正在拖拽 */
const val DRAGGING = 1
/** * 正在动画,动画产生的滚动不会被分发 */
const val ANIMATION = 2
/** * 正在 fling */
const val FLING = 3
}
}
复制代码
fling 和动画最终都会回调到 computeScroll
中处理,不一样的是动画产生的滚动不须要进行分发(由于动画不是 touch 事件产生的,而是外部明确调用的),而 fling 的须要 dispatchScrollInternal
进行分发。
override fun computeScroll() {
when {
scroller.computeScrollOffset() -> {
val dx = (scroller.currX - lastX).toInt()
val dy = (scroller.currY - lastY).toInt()
lastX = scroller.currX.toFloat()
lastY = scroller.currY.toFloat()
// 不分发来自动画的滚动
if (state == NestedScrollState.ANIMATION) {
scrollBy(dx, dy)
} else {
dispatchScrollInternal(dx, dy, ViewCompat.TYPE_NON_TOUCH)
}
invalidate()
}
// ...
}
}
复制代码
BehavioralScrollView
已经处理了共性的东西,个性化的部分是 NestedScrollBehavior
实现的,所以这里的示例可能不具有通用性。当有特殊须要是,能够很方便地自定义 NestedScrollBehavior
实现,这也正是 BehavioralScrollView
但愿达到的效果。
这里以底部浮层 BottomSheetBehavior
为例大体介绍下 NestedScrollBehavior
的使用。
构造 BottomSheetBehavior
须要知道内容视图 contentView
以及浮层弹出的范围和初始位置。
class BottomSheetBehavior(
/**
* 浮层的内容视图
*/
contentView: View,
/**
* 初始位置,最低高度 [POSITION_MIN]、中间高度 [POSITION_MID] 或最大高度 [POSITION_MAX]
*/
private val initPosition: Int,
/**
* 内容视图的最低显示高度
*/
private val minHeight: Int,
/**
* 内容视图中间停留的显示高度,默认等于最低高度
*/
private val midHeight: Int = minHeight
)
复制代码
因为滚动范围是由 prevView
、midView
和 nextView
肯定的,顶部的空白区域须要设置 prevView
进行占位,经过 topMargin
控制其高度,从而控制滚动的范围,midView
设置为 contentView
,这里不须要 nextView
设为 null
。
/** * 用于控制滚动范围 */
override val prevView: View? = Space(contentView.context).also {
val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
lp.topMargin = minHeight
it.layoutParams = lp
}
override val midView: View = contentView
override val nextView: View? = null
复制代码
在 afterLayout
时计算中间高度时 scrollY
的值,并在第一次 layout 后直接滚动指定的初始位置。
override fun afterLayout(v: BehavioralScrollView) {
// 计算中间高度时的 scrollY
midScroll = v.minScroll + midHeight - minHeight
// 第一次 layout 滚动到初始位置
if (firstLayout) {
firstLayout = false
v.scrollTo(
v.scrollX,
when (initPosition) {
POSITION_MIN -> v.minScroll
POSITION_MAX -> v.maxScroll
else -> midScroll
}
)
}
}
复制代码
简单画了下布局的示意图
在 handleDispatchTouchEvent
的 up 或 cancel 时,须要根据当前滚动位置和上次滚动的方向,决定动画的目标位置。
override fun handleDispatchTouchEvent( v: BehavioralScrollView, e: MotionEvent ): Boolean? {
if ((e.action == MotionEvent.ACTION_CANCEL || e.action == MotionEvent.ACTION_UP)
&& v.scrollY != 0) {
// 在 up 或 cancel 时,根据当前滚动位置和上次滚动的方向,决定动画的目标位置
v.smoothScrollTo(
if (v.scrollY > midScroll) {
if (v.lastScrollDir > 0) { v.maxScroll } else { midScroll }
} else {
if (v.lastScrollDir > 0) { midScroll } else { v.minScroll }
}
)
return true
}
return super.handleDispatchTouchEvent(v, e)
}
复制代码
handleTouchEvent
须要在 down 在 prevView
时不进行处理,由于它只是个占位的,这样不会影响下层视图对事件的处理。
override fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? {
// down 事件触点在 prevView 上时不作处理
return if (e.action == MotionEvent.ACTION_DOWN && prevView?.isUnder(e.rawX, e.rawY) == true) {
false
} else {
null
}
}
复制代码
嵌套滚动的优先级处理比较简单,handleNestedPreScrollFirst
只在 contentView
没有彻底展开,即 v.scrollY != 0
时处理,而 handleNestedScrollFirst
老是优先处理。
override fun handleNestedPreScrollFirst( v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int ): Boolean? {
// 只要 contentView 没有彻底展开,就在子 View 滚动前处理
return if (v.scrollY != 0) { true } else { null }
}
override fun handleNestedScrollFirst( v: BehavioralScrollView, scroll: Int, type: Int ): Boolean? {
return true
}
复制代码
自身的滚动只处理 touch 类型的,其余的过滤掉。
override fun handleScrollSelf( v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int ): Boolean? {
// 只容许 touch 类型用于自身的滚动
return if (type == ViewCompat.TYPE_NON_TOUCH) { true } else { null }
}
复制代码
Demo 中还有其余各类类型的 NestedScrollBehavior
,如实现顶部 TabLayout
悬浮效果的 FloatingHeaderBehavior
,兼容嵌套的下拉刷新 SwipeRefreshBehavior
等。这里简单说明下为何 SwipeRefreshLayout
已经实现了 NestedScrollingParent
和 NestedScrollingChild
,却没法适用于嵌套滚动呢?
NestedScrollingChild.dispatchNestedScroll
缺乏 NestedScrollingChild3.dispatchNestedScroll
中的 consumed
参数,因此在向父 View 分发时,没法得知父 View 消耗了多少滚动量,嵌套使用就会存在问题,来看下 SwipeRefreshLayout.onNestedScroll
方法。
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
// ...
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed);
}
}
复制代码
它在 dispatchNestedScroll
以后是不知道父 View 有没有消耗滚动量的,而函数中的 mParentOffsetInWindow
获得的是 SwipeRefreshLayout
在屏幕上的位移,SwipeRefreshLayout
认为的父 View 没有消耗的滚动量等于 dyUnconsumed + mParentOffsetInWindow[1]
。
这样看起来没啥问题,但当父 View 消耗的滚动量不等于其子 View 在屏幕上的位移时(好比增长了阻尼效果,消耗了 n 的滚动量,却只移动了 n/2)就会出问题,即便滚动量已经所有被外部消耗了,SwipeRefreshLayout
仍是有下拉效果:
因此为了解决这种问题,就须要实现了 NestedScrollingChild3
的接口,下面是 BehavioralScrollView
+ SwipeRefreshBehavior
的效果:
嵌套滚动的核心问题是优先级问题,咱们应该专一于优先级的策略而不是各类事件的处理和分发问题,这也真是 BehavioralScrollView
在尝试作到的,但愿这篇文章可以对你有所帮助,有不一样思路的也欢迎相互探讨。