compose提供了很多开箱即用的功能。对于嵌套滑动来说打造了一个嵌套滑动的系统,所以有很多组件和修饰符也是可以开箱即用根本不需要关心嵌套滑动,比如:(verticalScroll
、horizontalScroll
、scrollable
、Lazy
API 和 TextField
)。
可是并不可能将所有的组件都考虑进去,那么当出现滑动冲突的时候就需要我们自己去处理这些嵌套滑动了。
nestedScroll修饰符
该修饰符能让那些不在嵌套系统中的组件加入到嵌套系统中。它接收两个参数:NestedScrollConnection
和NestedScrollDispatcher
。这两个参数就是加入到嵌套系统中的关键,他们都有四个主要的方法,先来看connection。
NestedScrollConnection
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset
fun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource):Offset
suspend fun onPreFling(available: Velocity): Velocity
suspend fun onPostFling(consumed: Velocity, available: Velocity)
onPreScroll
:在子控件滑动之前,会先使用NestedScrollDispatcher
询问父控件是否需要消费available
的偏移量,父控件可以在该方法内计算自身需要消费的量,然后返回自身消费了的偏移量。
onPostScroll
:在子控件滑动之后,会使用NestedScrollDispatcher
通知父控件,告知其consumed
的偏移量以及剩余available
的偏移量,而父控件则可以根据情况判断是否还要再偏移,以及使用和子控件同等的偏移还是剩余的偏移。完成之后返回自身消费了的偏移量
onPreScroll和onPostScroll的返回值都是Offset,都是自身消费了的偏移量
onPreFling
:在子控件进行惯性滑行之前,会先使用NestedScrollDispatcher
询问父控件是否需要消费available
的速度值,父控件可以在该方法内计算自身需要消费的量,然后返回自身消费了的速度值。
onPostFling
:在子控件进行惯性滑行之后,会使用NestedScrollDispatcher
通知父控件,告知其consumed
的速度值以及剩余available
的速度值,而父控件则可以根据情况判断是否还要再偏移,以及使用和子控件同等的速度还是剩余的速度。完成之后返回自身消费了的速度值。
一句话概括:在滑动前父控件可以通过
onPreScroll
回调先消费部分或全部偏移量;待子控件消费完后,父控件依旧可以通过onPostScroll
方法进行消费,区别在于在onPreScroll
回调中,父控件是优先消费的,而onPostScroll
则是子控件优先消费,fling
的两个方法同理。
NestedScrollDispatcher
在介绍NestedScrollConnection
的时候,你应该已经注意到我听到了它,它同样也有四个主要方法,是和NestedScrollConnection
一一对应的。
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset
fun dispatchPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset
suspend fun dispatchPreFling(available: Velocity): Velocity
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity
这四个方法的实现很简单,就是调用NestedScrollConnection
的四个方法。
dispatchePreScroll
在滑动之前调用,将可用的偏移量传递给父控件
dispatchPostScroll
在滑动之后调用,将已经消费的偏移量以及剩余可用的偏移量传递给父控件
dispatchPreFling
在滑动之后,如果产生了惯性滑动,那么需要将相应的速度值传递给父控件
dispatchPostFling
在惯性滑动之后,需要将已经消费了的速度值以及剩余可用的速度值传递给父控件
一句话概括:通过dispatchPreScroll方法询问父控件是否需要消费,方法返回的就是父控件消费了的量,然后就可以做自己的滑动操作了,在滑动完成之后,再通过dispatchPostScroll方法通知父控件。
浅析
从nestedScroll修饰符可以知道,connection是必须的,而dispatcher是非必需的,那是因为如果控件是顶层控件,那么便不需要实现dispatcher,connection则是接收自下而上的滑动数据。需要注意的是,对于处于中间层级的滑动组件一定要做好向上传递可用的偏移量,遵守相应的规则。
任何可滚动容器都通过
NestedScrollConnection
作为父项参与嵌套滚动链,并通过NestedScrollDispatcher
作为子项参与嵌套滚动链
实战分析
官方也给出了一个示例,这个示例是将竖直方向上的拖拽加入到嵌套系统中。这里和大家一起来分析一下这个示例:
// 最终响应嵌套滑动的状态值
val basicState = remember { mutableStateOf(0f) }
val minBound = -100f //最小的值
val maxBound = 100f //最大的值
//计算消费
val onNewDelta: (Float) -> Float = { delta ->
val oldState = basicState.value
val newState = (basicState.value + delta).coerceIn(minBound, maxBound)
basicState.value = newState
newState - oldState
}
首先是准备用到的一些变量和方法。onNewDelta方法是当前控件消费滑动的方法。
//2
// create nested scroll connection to react to nested scroll events (participate like a parent)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
// we have no fling, so we're interested in the regular post scroll cycle
// let's try to consume what's left if we need and return the amount consumed
val vertical = available.y
val weConsumed = onNewDelta(vertical)
return Offset(x = 0f, y = weConsumed)
}
}
}
实现NestedScrollConnection,重写其中的onPostScroll,这里很简单首先拿到y轴的可用偏移量,然后消费,然后将消费完的结果返回回去。需要注意的是,根据你实际的需求,并不一定是重写onPostScroll这个方法,这里的消费实际意思是:子控件消费了一部分,本控件只需要消费其没有消费的。如果你希望你先消费,那么你需要实现的应该是onPreScroll,如果有更加复杂的消费行为,两个方法同时重写也是可以的。
//3
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
Box(
Modifier
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
// here's regular drag. Let's be good citizens and ask parents first if they
// want to pre consume (it's a nested scroll contract)
//通知父控件,并得到父控件消费了的偏移量
val parentsConsumed = nestedScrollDispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = delta),
source = NestedScrollSource.Drag
)
// adjust what's available to us since might have consumed smth
//计算出剩余可用的偏移量
val adjustedAvailable = delta - parentsConsumed.y
// we consume
//消费,得到我的消费的量
val weConsumed = onNewDelta(adjustedAvailable)
// dispatch as a post scroll what's left after pre-scroll and our consumption
//计算总的消费量
val totalConsumed = Offset(x = 0f, y = weConsumed) + parentsConsumed
//计算剩余的可用量
val left = adjustedAvailable - weConsumed
//通知父控件,目前消费的量和剩余的量
nestedScrollDispatcher.dispatchPostScroll(
consumed = totalConsumed,
available = Offset(x = 0f, y = left),
source = NestedScrollSource.Drag
)
//如果在这之后还有惯性的需求,可以继续使用剩余的两个方法。
// we won't dispatch pre/post fling events as we have no flinging here, but the
// idea is very similar:
// 1. dispatch pre fling, asking parents to pre consume
// 2. fling (while dispatching scroll events like above for any fling tick)
// 3. dispatch post fling, allowing parent to react to velocity left
}
)
)
从rememberDraggableState里的第一行代码就可以知道,当你的操作加入到嵌套系统中时,需要遵守的第一件事就是:在第一时间需要先讲可用的偏移量使用dispatchPreScroll方法通知父控件。
这是一个标准的处理嵌套滑动的做法。
最后
nestedScroll(connection = nestedScrollConnection, dispatcher = nestedScrollDispatcher)
给你的控件加上nestedScroll修饰符。以下是完整的代码:
@Sampled
@Composable
fun NestedScrollDispatcherSample() {
// Let's take Modifier.draggable (which doesn't have nested scroll build in, unlike Modifier
// .scrollable) and add nested scroll support our component that contains draggable
// this will be a generic components that will work inside other nested scroll components.
// put it inside LazyColumn or / Modifier.verticalScroll to see how they will interact
// 最终响应嵌套滑动的状态值
val basicState = remember { mutableStateOf(0f) }
val minBound = -100f
val maxBound = 100f
//计算消费
val onNewDelta: (Float) -> Float = { delta ->
val oldState = basicState.value
val newState = (basicState.value + delta).coerceIn(minBound, maxBound)
basicState.value = newState
newState - oldState
}
// create a dispatcher to dispatch nested scroll events (participate like a nested scroll child)
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
// create nested scroll connection to react to nested scroll events (participate like a parent)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
// we have no fling, so we're interested in the regular post scroll cycle
// let's try to consume what's left if we need and return the amount consumed
val vertical = available.y
//消费子控件剩余的可用偏移量
val weConsumed = onNewDelta(vertical)
//返回本控件消费的偏移量
return Offset(x = 0f, y = weConsumed)
}
}
}
Box(
Modifier
.size(100.dp)
.background(Color.LightGray)
// attach ourselves to nested scroll system
.nestedScroll(connection = nestedScrollConnection, dispatcher = nestedScrollDispatcher)
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
// here's regular drag. Let's be good citizens and ask parents first if they
// want to pre consume (it's a nested scroll contract)
val parentsConsumed = nestedScrollDispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = delta),
source = NestedScrollSource.Drag
)
// 计算剩余的可用偏移量
val adjustedAvailable = delta - parentsConsumed.y
// 消费
val weConsumed = onNewDelta(adjustedAvailable)
// dispatch as a post scroll what's left after pre-scroll and our consumption
//计算总消费了的偏移量
val totalConsumed = Offset(x = 0f, y = weConsumed) + parentsConsumed
//计算剩余的可用偏移量
val left = adjustedAvailable - weConsumed
//滑动结束,通知父控件
nestedScrollDispatcher.dispatchPostScroll(
consumed = totalConsumed,
available = Offset(x = 0f, y = left),
source = NestedScrollSource.Drag
)
// we won't dispatch pre/post fling events as we have no flinging here, but the
// idea is very similar:
// 1. dispatch pre fling, asking parents to pre consume
// 2. fling (while dispatching scroll events like above for any fling tick)
// 3. dispatch post fling, allowing parent to react to velocity left
}
)
) {
//展示拖动过程中的值
Text(
"State: ${basicState.value.roundToInt()}",
modifier = Modifier.align(Alignment.Center)
)
}
}
和View的协作
对于和View的嵌套滚动互操作性这里就不深入了,有需要可以参考官方文章。
总结
相对于View系统来说,compose中处理嵌套滑动的方式我认为是更优雅和更简单的。通过NestScrollConnection
处理子控件产生的滑动数据,使用NestScrollDispatcher
将本身产生的滑动数据通知出去,剩下的就是简单的计算了。在MRouter中,处理modal
手势模式的时候也用到了嵌套滑动,通过modal
的方式打开的页面,其手势区域是一整个页面,那么当页面中存在列表时,滑动冲突就会产生,这是就需要通过nestScroll
去解决了。还不知道什么是MRouter可以看这篇文章:MRouter,一款compose-multiplatform的路由库。