插一下,怎样在 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 相关内容参考

NestedScrollConnection

nestedScroll

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 容器做出相应改动。

效果如下

折叠布局

22.2 Compose NestedScrollConnection 完结折叠/掩盖布局

掩盖布局

22.2 Compose NestedScrollConnection 完结折叠/掩盖布局

NetedScrollConnection 完结

首先在 connect 的 onPreScroll() 方法中判别是否让 connect 消耗掉工作值。

定义 TopStates 来辅佐判别

enum class TopStates {
    EXPANDED,// 默许翻开情况 
    SCROLLING,//中心情况 ,中心情况时 connection 也要消耗掉悉数工作 
    COLLAPSED// 折叠情况 
}

将判别方法封装到 CollapsableLayoutState 中

class CollapsableLayoutState{
    fun shouldConsumeAvailable(available: Offset): Boolean {
      //省掉
    }  
}

还需求考虑到 bottom 容器中假如有能够翻滚的子组件发生翻滚的情况

22.2 Compose NestedScrollConnection 完结折叠/掩盖布局

这种情况下 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 容器的高度。

  1. 这个改动的最大值是top 容器的高度 ,最小值之能够通过传参来指定。
  2. 核算改动,根据核算出的差值设置其时 topState.value 的值
  3. 核算出差值在 [最小值,最大值] 中百分比 供给一个跟从改动的 [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
    )
}

源码 git 地址