You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose滚动时头部折叠至菜单上方的实现方案咨询

Jetpack Compose 实现可折叠头部布局

问题描述

我正尝试在应用中使用Jetpack Compose实现可折叠头部,现有如下Composable组件代码:

ModalNavigationDrawer(
    drawerContent = {
        ModalDrawerSheet(drawerContainerColor = Color.White) {
            // my menu items
        }

    }, drawerState = drawerState, content = {  }, modifier = Modifier
        .background(
            colorResource(id = R.color.white)
        )
){
   Column(
                modifier = Modifier
                    .verticalScroll(rememberScrollState())
                    .background(colorResource(id = R.color.white))
                    .fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                HeaderTile("Good Morning", coroutineScope, drawerState);
                OptionsTile(
                    modifier = Modifier.fillMaxWidth(),
                )
            }
 }

 @Composable
 fun HeaderTile(
    greetMessage: String,
    coroutineScope: CoroutineScope,
    drawerState: DrawerState
) {
    val transparentRed = Color.Red.copy(alpha = 0.8f) // 50% transparent red
    val transparentBlue = Color.Black.copy(alpha = 0.8f)
    val brush = Brush.verticalGradient(listOf(transparentRed, transparentBlue))
    Box(
        modifier = Modifier
            .height(240.dp)
            .fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = painterResource(id = R.drawable.header_image),
            contentDescription = null,
            contentScale = ContentScale.Crop, // Adjust as needed
            modifier = Modifier.fillMaxSize()
        )
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    brush = brush
                ) // Bottom-right corner
        )
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.TopStart)
                .padding(top = 5.dp)
                .wrapContentHeight(),
            contentAlignment = Alignment.TopStart
        ) {
            androidx.compose.material3.IconButton(onClick = { coroutineScope.launch { drawerState.open() } },
                content = {
                    androidx.compose.material3.Icon(
                        imageVector = Icons.Default.Menu, contentDescription = null,
                        Modifier.size(30.dp),
                        tint = colorResource(id = R.color.white)
                    )
                })
            Column(
                modifier = Modifier
                    .fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                androidx.compose.material3.Text(
                    text = "#Hello Everyone",
                    color = White,
                    style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
                    fontStyle = FontStyle.Italic,
                )
            }
        }
    }
}


@Composable
fun OptionsTile(
modifier: Modifier = Modifier
) {
Row(
    modifier = modifier
        .offset(y = -(50).dp)
        .padding(start = 15.dp, end = 15.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
    for (i in 1..3) {
        Card(
            colors = CardDefaults.cardColors(
                containerColor = White
            ),
            modifier = Modifier
                .height(height = 100.dp)
                .weight(1f),
            elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
        ) {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {

            }
        }
    }
}

当前布局效果:
当前布局

期望效果:向上滚动页面时,红色渐变头部区域折叠至三个菜单卡片的上方,效果如下:
期望折叠效果

实现方案

要实现这种折叠效果,核心是利用嵌套滚动监听动态调整头部的高度和内容显示状态,同时适配内容区域的偏移。以下是具体实现步骤:

1. 重构布局结构

将原有的Column滚动布局改为Box层叠头部与滚动内容,让头部独立于滚动内容进行位置和尺寸调整。

2. 监听滚动事件

使用NestedScrollConnection捕获滚动偏移量,计算头部从原始高度(240dp)到折叠后最小高度(比如60dp)的动态变化值。

3. 动态调整头部与内容

根据滚动偏移量,修改头部的高度、背景透明度,控制标题的显示/隐藏,同时调整内容区域的顶部偏移以保持布局连贯性。

修改后的完整代码

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CollapsibleHeaderScreen(
    drawerState: DrawerState,
    coroutineScope: CoroutineScope
) {
    ModalNavigationDrawer(
        drawerContent = {
            ModalDrawerSheet(drawerContainerColor = Color.White) {
                // 菜单内容
            }
        },
        drawerState = drawerState,
        modifier = Modifier.background(colorResource(id = R.color.white))
    ) {
        // 定义头部的原始高度和折叠后的最小高度
        val originalHeaderHeight = 240.dp
        val collapsedHeaderHeight = 60.dp
        val headerHeightRange = originalHeaderHeight - collapsedHeaderHeight

        val scrollState = rememberLazyListState()
        // 从滚动状态中获取偏移量,转换为dp
        val scrollOffset by remember {
            derivedStateOf {
                scrollState.firstVisibleItemScrollOffset.coerceAtMost(headerHeightRange.roundToPx())
            }
        }
        // 计算当前头部高度
        val currentHeaderHeight = originalHeaderHeight - scrollOffset.toDp()

        Box(modifier = Modifier.fillMaxSize()) {
            // 滚动内容区域
            LazyColumn(
                state = scrollState,
                modifier = Modifier
                    .fillMaxSize()
                    .background(colorResource(id = R.color.white)),
                contentPadding = PaddingValues(top = originalHeaderHeight - 50.dp) // 保留原始头部下方的偏移
            ) {
                item {
                    OptionsTile(modifier = Modifier.fillMaxWidth())
                }
                // 可添加更多滚动内容
                items(20) { index ->
                    Text(
                        text = "列表项 $index",
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp)
                    )
                }
            }

            // 可折叠头部
            HeaderTile(
                greetMessage = "Good Morning",
                coroutineScope = coroutineScope,
                drawerState = drawerState,
                currentHeight = currentHeaderHeight,
                scrollOffsetRatio = scrollOffset.toFloat() / headerHeightRange.roundToPx()
            )
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HeaderTile(
    greetMessage: String,
    coroutineScope: CoroutineScope,
    drawerState: DrawerState,
    currentHeight: Dp,
    scrollOffsetRatio: Float // 0=未折叠,1=完全折叠
) {
    val transparentRed = Color.Red.copy(alpha = 0.8f)
    val transparentBlue = Color.Black.copy(alpha = 0.8f)
    val brush = Brush.verticalGradient(listOf(transparentRed, transparentBlue))

    Box(
        modifier = Modifier
            .height(currentHeight)
            .fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        // 背景图,折叠时保持顶部显示
        Image(
            painter = painterResource(id = R.drawable.header_image),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize(),
            alpha = 1 - scrollOffsetRatio * 0.5f // 折叠时稍微降低透明度
        )
        // 渐变层
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(brush = brush)
        )

        // 顶部菜单按钮
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.TopStart)
                .padding(top = 5.dp)
                .wrapContentHeight(),
            contentAlignment = Alignment.TopStart
        ) {
            IconButton(
                onClick = { coroutineScope.launch { drawerState.open() } },
                modifier = Modifier.padding(start = 8.dp)
            ) {
                Icon(
                    imageVector = Icons.Default.Menu,
                    contentDescription = "打开抽屉",
                    Modifier.size(30.dp),
                    tint = colorResource(id = R.color.white)
                )
            }

            // 标题,折叠时逐渐隐藏
            Column(
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "#Hello Everyone",
                    color = Color.White,
                    style = MaterialTheme.typography.headlineSmall,
                    fontStyle = FontStyle.Italic,
                    alpha = 1 - scrollOffsetRatio // 折叠时完全隐藏
                )
            }
        }
    }
}

@Composable
fun OptionsTile(
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier
            .padding(start = 15.dp, end = 15.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        repeat(3) {
            Card(
                colors = CardDefaults.cardColors(containerColor = Color.White),
                modifier = Modifier
                    .height(100.dp)
                    .weight(1f),
                elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
            ) {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    // 卡片内容
                }
            }
        }
    }
}

关键代码解释

  • 滚动状态监听:通过rememberLazyListState和derivedStateOf实时获取滚动偏移量,计算头部当前高度。
  • 动态高度调整:currentHeaderHeight随滚动偏移从原始高度递减至最小高度,实现折叠效果。
  • 内容偏移适配:LazyColumn的contentPadding设置为原始头部高度减去卡片偏移量,确保初始布局与原效果一致。
  • 平滑过渡:利用scrollOffsetRatio控制标题透明度和背景图透明度,实现折叠过程的自然过渡。

内容的提问来源于stack exchange,提问作者Rajeev Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 18:09:52