插一下,如何在 composable 函数中获取状态栏高度
val statusBarHeightDp = with(LocalDensity.current) {
WindowInsets.Companion.statusBars.getTop(this).toDp()
}
// 打印 WindowInsets.Companion.statusBars 结果是:
// statusBars(0, 136, 0, 0)
// AndroidWindowInsets toString() 方法实现是(左上右下)
// return "$name(${insets.left}, ${insets.top}, ${insets.right},${insets.bottom})"
实现折叠/覆盖布局
NetedScrollConnection
Compose 中默认支持嵌套滚动。
父容器添加 Modifier.nestedScroll(connection) 后子组件产生滚动事件后会先经过 NetedScrollConnection 的 onPreScroll() 、 onPostScroll() 或 onPreFling() 、onPostFling() 方法。
这些方法的返回值代表 connection 消耗了多少事件值,剩余的事件值会继续交给子组件处理。如果 connection 全部事件值,子组件就不会触发滚动事件。
NetedScrollConnection 相关内容参考
Jetpack compose 仿QQ音乐实现下拉刷新上拉加载更多
我们这里只用到 onPreScroll()
/*
available 可用的 事件值
source 事件来源 Drag / Fling
返回 connect 消耗的 Offset , 默认 Offset.Zero 既不消耗
*/
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
布局结构和最终效果
两种布局结构如下
|-布局容器(modifier = Modifier.nestedScroll(connection))
|-Top容器
|-Bottom容器(modifier = Modifier.fillMaxSize().scrollable(rememberScrollState(), Orientation.Vertical))
都使用 NestedScrollConnection 实现,Bottom 容器产生滑动事件后使用 NestedScrollConnection 拦截,根据事件值对 Top 或 Bottom 容器做出相应改变。
效果如下
折叠布局
覆盖布局
NetedScrollConnection 实现
首先在 connect 的 onPreScroll() 方法中判断是否让 connect 消耗掉事件值。
定义 TopStates 来辅助判断
enum class TopStates {
EXPANDED,// 默认展开状态
SCROLLING,//中间状态 ,中间状态时 connection 也要消耗掉全部事件
COLLAPSED// 折叠状态
}
将判断方法封装到 CollapsableLayoutState 中
class CollapsableLayoutState{
fun shouldConsumeAvailable(available: Offset): Boolean {
//省略
}
}
还需要考虑到 bottom 容器中如果有可以滚动的子组件发生滚动的情况
这种情况下 connection 不应该消耗事件
class CollapsableScrollConnection(
private val isChildScrolled: State<Boolean> = mutableStateOf(false),
private val state: CollapsableLayoutState
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (isChildScrolled.value) return Offset.Zero
if (state.shouldConsumeAvailable(available)) {
state.plusOffset(available)
return available
}
return Offset.Zero
}
}
需要消耗事件的情况下,我们将变化记录在 CollapsableLayoutState 的offsetState 中,具体如何处理不在 connection 中实现。
CollapsableLayoutState
先实现上面提到的内容
class CollapsableLayoutState{
private val offsetState: MutableState<Offset> = mutableStateOf(Offset.Zero)
private val topState: MutableState<TopStates> = mutableStateOf(TopStates.EXPANDED)
fun plusOffset(offset: Offset) {
offsetState.value = offsetState.value + offset
}
fun shouldConsumeAvailable(available: Offset): Boolean {
return topState.value == TopStates.SCROLLING //处于折叠和展开状态
//展开状态 向上滑动
|| topState.value == TopStates.EXPANDED && available.y < 0
//折叠状态 向下滑动
|| topState.value == TopStates.COLLAPSED && available.y > 0
}
}
这两个布局效果的实现就是根据 offsetState 值的变化来改变 top 容器或 bottom 容器的高度。
- 这个变化的最大值是top 容器的高度 ,最小值之可以通过传参来指定。
- 计算变化,根据计算出的差值设置当前 topState.value 的值
- 计算出差值在 [最小值,最大值] 中百分比 提供一个跟随变化的 [0,1] 的 state 供外部监听实现动画
@Composable
fun rememberCollapsableLayoutState(minTopHeightDp: Dp = 0.dp): CollapsableLayoutState {
val density = LocalDensity.current
return remember(minTopHeightDp,density) {
CollapsableLayoutState(minTopHeightDp, density)
}
}
class CollapsableLayoutState(
private val minTopHeightDp: Dp,
density: Density
){
private val offsetState: MutableState<Offset> = mutableStateOf(Offset.Zero)
private val topState: MutableState<TopStates> = mutableStateOf(TopStates.EXPANDED)
//top 最大高度需要 top 容器经过测量后得到
private val maxHeightState: MutableState<Int> = mutableStateOf(-1)
//默认 EXPANDED 状态 ,progress 默认为 1
private val expendProgressState = mutableStateOf(1f)
val currentTopState
get() = topState.value
val minTopHeightPx = with(density) {
minTopHeightDp.toPx().toInt()
}
val maxTopHeightPx
get() = maxHeightState.value
val maxOffsetY
get() = maxTopHeightPx - minTopHeightPx
val expendProgress
get() = expendProgressState.value
fun shouldConsumeAvailable(available: Offset): Boolean {
return topState.value == TopStates.SCROLLING //处于折叠和展开状态
//展开状态 向上滑动
|| topState.value == TopStates.EXPANDED && available.y < 0
//折叠状态 向下滑动
|| topState.value == TopStates.COLLAPSED && available.y > 0
}
fun plusOffset(offset: Offset) {
offsetState.value = offsetState.value + offset
}
/**
* 设置 top 容器最大高度
* @param maxHeight Int
*/
fun updateMaxTopHeight(maxHeight: Int) {
if (maxHeight == maxHeightState.value) return
maxHeightState.value = maxHeight
}
/*
根据 offsetState 计算当前 top 容器的高度
*/
fun calcTopHeight():Int{
val curTopHeight = (maxTopHeightPx + offsetState.value.y.toInt()).coerceIn(minTopHeightPx,maxTopHeightPx)
//根据 curTopHeight 设置 LayoutState ,计算 expendProgressState
when (curTopHeight) {
minTopHeightPx -> {
offsetState.value = Offset(0f, -maxOffsetY.toFloat())
expendProgressState.value = 0f
updateLayoutState(TopStates.COLLAPSED)
}
maxTopHeightPx -> {
offsetState.value = Offset.Zero
expendProgressState.value = 1f
updateLayoutState(TopStates.EXPANDED)
}
else -> {
val offsetY = (maxTopHeightPx - curTopHeight).toFloat()
expendProgressState.value = 1 - offsetY / maxOffsetY
updateLayoutState(TopStates.SCROLLING)
}
}
return curTopHeight
}
private fun updateLayoutState(state:TopStates){
if (state == topState.value) return
topState.value = state
}
}
折叠布局实现
使用 Column 最为父容器, 根据 CollapsableLayoutState 计算后 top 的高度来改变 top 容器的大小 , bottom 容器在 fillMaxSize 的情况下也会随之改变。
@Composable
fun CollapsableLayout(
topContent: @Composable () -> Unit,
bottomContent: @Composable () -> Unit,
bottomContentScrolled: State<Boolean> = mutableStateOf(false),
state: CollapsableLayoutState = rememberCollapsableLayoutState(0.dp)
) {
val connection: CollapsableScrollConnection = remember {
CollapsableScrollConnection(bottomContentScrolled, state)
}
val heightModifier = if (state.maxTopHeightPx != -1) {
Modifier.height(with(LocalDensity.current){
state.calcTopHeight().toDp()
})
} else {
Modifier
}
Column(modifier = Modifier
.nestedScroll(connection)
) {
Box(
modifier = Modifier.then(heightModifier)
.onSizeChanged {
//设置 top 最大高度
if (state.maxTopHeightPx == -1) {
state.updateMaxTopHeight(it.height)
}
}
) { topContent() }
Box(
modifier = Modifier.fillMaxSize()
.scrollable(rememberScrollState(), Orientation.Vertical)
) { bottomContent() }
}
}
覆盖布局实现
使用 Layout 做为父容器,实现 MeasurePolicy , 根据 CollapsableLayoutState 计算后 top 的高度来对 bottom 容器进行测绘和布局达到覆盖的效果
@Composable
fun CoverLayout(
topContent: @Composable BoxScope.()-> Unit,
bottomContent: @Composable BoxScope.() -> Unit,
bottomContentScrolled: State<Boolean> = mutableStateOf(false),
state:CollapsableLayoutState = rememberCollapsableLayoutState()
) {
val connection: CollapsableScrollConnection = remember {
CollapsableScrollConnection(bottomContentScrolled, state)
}
Layout(modifier = Modifier
.nestedScroll(connection), content = {
Box(content = topContent )
Box(
modifier = Modifier
.fillMaxSize()
.scrollable(rememberScrollState(), Orientation.Vertical)
.background(Color.Green),
content = bottomContent
)
}) { measurables, constraints ->
val placeableTop = measurables[0].measure(constraints)
if (state.maxTopHeightPx == -1){
state.updateMaxTopHeight(placeableTop.height)
}
val topHeight = state.calcTopHeight()
//bottom 的最大高度约束要减去 top 容器的高度
val bottomConstraints =
constraints.copy(maxHeight = constraints.maxHeight - topHeight)
val placeableBottom = measurables[1].measure(bottomConstraints)
layout(constraints.maxWidth, constraints.maxHeight) {
placeableTop.placeRelative(0, 0)
//bottom 容器放在 top 之下
placeableBottom.placeRelative(0, topHeight)
}
}
}
使用
@Composable
fun Test() {
val listState = rememberLazyListState()
// bottom 中 LazyColumn 内容是否滚动过
val bottomContentScrolled: State<Boolean> = remember {
derivedStateOf {
!(listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0)
}
}
val collapsableLayoutState = rememberCollapsableLayoutState()
CollapsableLayout(
topContent = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
//根据 expendProgress 设置 alpha 动画效果
.alpha(collapsableLayoutState.expendProgress)
){
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.c19e2e81da3ede74c24c29bf6b1a800b),
contentScale = ContentScale.FillBounds,
contentDescription =""
)
}
},
bottomContent = {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
state = listState,
verticalArrangement = Arrangement.spacedBy(22.dp)
) {
items(30) {
Text(text = "第 $it 项")
}
}
},
bottomContentScrolled = bottomContentScrolled,
state = collapsableLayoutState
)
}