背景

此前在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。

参考

  1. jetpackcompose.cn/docs/design…