背景
此前在Jetpack Compose中完成一个首页嵌套滑动吸顶作用的需求,研讨了好久,不像在原生上资料比较多,网上大把的方案,而在compose上即便你知道了有一个nestedScroll
修饰符,可是查看官方文档你会得到以下的描绘。
嵌套翻滚互操作性(从Compose 1.2.0开始)
当您测验在可翻滚的组合中嵌套可翻滚的View元素,或许反过来时,您或许会遇到问题。最明显的问题会在您翻滚子元素并达到其开始或完毕鸿沟时产生,然后期望父元素接管翻滚。可是,这种预期的行为或许不会产生,或许或许无法按预期工作。
这个问题是因为可翻滚的组合内置的期望所导致的。可翻滚的组合具有“默许嵌套翻滚”规矩,这意味着任何可翻滚的容器都必须参加嵌套翻滚链,既要作为父级通过NestedScrollConnection参加,也要作为子级通过NestedScrollDispatcher参加。当子级抵达鸿沟时,子级会驱动父级的嵌套翻滚。例如,这个规矩允许Compose Pager和Compose LazyRow很好地协同工作。可是,当使用ViewPager2或RecyclerView进行互操作翻滚时,因为它们没有完成NestedScrollingParent3,因而从子级到父级的接连翻滚是不或许的。
Ok可以提炼出以下两个要点:
1、默许的嵌套行为是子组件翻滚到鸿沟时再传给父组件
2、假如你想自定义规矩,就要用到NestedScrollConnection
这个东西
然后给了一个完成CollapsingToolbarLayout的案例。
// Sets up the nested scroll connection between the Box composable parent
//1 and the child AndroidView containing the RecyclerView
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Updates the toolbar offset based on the scroll to enable
// collapsible behaviour
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection) // 2
) {
TopAppBar(
modifier = Modifier
.height(ToolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
)
AndroidView(
{ context ->
LayoutInflater.from(context)
.inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
with(findViewById<RecyclerView>(R.id.main_list)) {
layoutManager = LinearLayoutManager(context, VERTICAL, false)
adapter = NestedScrollInteropAdapter()
}
}.also {
// Nested scrolling interop is enabled when
// nested scroll is enabled for the root View
ViewCompat.setNestedScrollingEnabled(it, true)
}
},
// ...
)
}
关键在于注释1处的NestedScrollConnection重写onPreScroll办法和注释2处的Box父容器使用nestedScroll修饰符,除此之外没有更多详细介绍,为什么是这样?得理解NestedScrollConnection的API。
它供给了四个回调函数:
onPreScroll
办法描绘:预先绑架滑动事情,消费后再交由子布局。
参数列表:
available:当时可用的滑动事情偏移量
source:滑动事情的类型
回来值:当时组件消费的滑动事情偏移量,假如不想消费可回来Offset.Zero
onPostScroll
办法描绘:通过onPreScroll然后子组件滑动后的回调
参数列表:
consumed:之前消费的一切滑动事情偏移量
available:当时剩余还可用的滑动事情偏移量
source:滑动事情的类型
回来值:当时组件消费的滑动事情偏移量,假如不想消费可回来 Offset.Zero ,则剩余偏移量会持续交由当时布局的父布局进行处理
onPreFling
办法描绘:获取 Fling 开始时的速度。
参数列表:
available:Fling 开始时的速度
回来值:当时组件消费的速度,假如不想消费可回来 Velocity.Zero
onPostFling
办法描绘:获取 Fling 完毕时的速度信息。
参数列表:
consumed:之前消费的一切速度
available:当时剩余还可用的速度
回来值:当时组件消费的速度,假如不想消费可回来Velocity.Zero,剩余速度会持续交由当时布局的父布局进行处理。
解决方案
了解了这四个回调函数的定义,再回到我们的嵌套滑动吸顶需求,核心处理逻辑便是:
在onPreScroll中处理向上滑动时,假如父组件还能滑动,则父组件消费available偏移量,回来值也回来消费的偏移量,反之回来Offset.Zero
val outerScrollState = rememberLazyListState()
val scope = rememberCoroutineScope { Dispatchers.Main }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
//预先绑架滑动事情,消费后再交由子布局
//回来值:当时组件消费的滑动事情偏移量,假如不想消费可回来Offset.Zero
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
logDebug("testt", "onPreScroll.. y=${available.y}")
val delta = available.y
//向上滑动且父组件还能滑动
val consumed = if (delta < 0 && outerScrollState.canScrollForward) {
// 向上翻滚时,优先翻滚外部LazyColumn
//注意这儿取反
scope.launch {
outerScrollState.scrollBy(-available.y)
logDebug("testt", "走了scrollBy")
}
available.y
} else {
// 向下翻滚时,优先翻滚内部LazyColumn,所以这儿不耗费事情
0f
}
return Offset(0f, consumed)
}
}
}
//父组件
LazyColumn(
Modifier.fillMaxSize(),
state = outerScrollState
){
item {
// 父组件的其它item
}
item {
Box(Modifier.nestedScroll(nestedScrollConnection))(
//子组件
LazyColumn() {
item {
//子组件的item
}
}
)
}
}
注意:nestedScroll修饰符要放在子组件上一层的那个组件,假如你放在父组件上,你会发现当你滑动子组件以外的父组件区域时,父组件没反应,可是scrollBy函数的确走了,原因不得而知
总结
Jetpack compose中使用nestedScroll修饰符和NestedScrollConnection来影响子组件的滑动,想要完成吸顶作用只需在NestedScrollConnection的onPreScroll回调中判断父组件是否能滑动,能滑动则耗费对应的Offset且return。
参考
- jetpackcompose.cn/docs/design…