Jetpack Compose 实现折叠工具栏效果

2,517 阅读1分钟

前言

在Android中 用CollapsingToolbarLayout来实现折叠工具栏效果;

在Jetpack Compose 中 可以用compose-collapsing-toolbar这个第三方库来实现。

implementation "me.onebone:toolbar-compose:2.3.3"

这是最终效果:

o50ly-bxwc6.gif

1、CollapsingToolbar的脚手架

val pic = "https://example.com/image.jpg"
val state = rememberCollapsingToolbarScaffoldState()
//高斯模糊背景
ToolBarBg(state, pic,48f, 220f, 10f)
CollapsingToolbarScaffold(
    modifier = Modifier
        .statusBarsPadding()
        .navigationBarsPadding(),
    state = state,
    scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
    toolbar = {
        CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.surface) {
            MyToolBar(state, "瓢盆大泼泼,瓢泼大盆飘,盆飘,下大雨了", pic, 48.dp, 220.dp)
        }

    }
) {
    Surface(shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp),
        color = MaterialTheme.colors.background) {
        LazyColumn(
            contentPadding = PaddingValues(5.dp),
            verticalArrangement = Arrangement.spacedBy(5.dp)) {

            itemsIndexed(items = lazyPagingItems,
                key = { _, item -> item.id }) { _, item ->
                item?.let {
                    RepositorCard(item)
                }
            }
            if (lazyPagingItems.loadState.append is LoadState.Loading) {
                //list 底部loading
                item {
                    Box(modifier = Modifier
                        .fillMaxWidth()
                        .height(50.dp)) {
                        CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
                    }
                }
            }
        }
    }
}

lazyPagingItems是用的我上一篇的paging3的分页,大家可以看看【Jetpack Compose】LazyColumn 使用Paging3分页+SwipeRefresh下拉刷新 - 掘金 (juejin.cn)

2、toolbar

/**
 * @param collapseHeight  toolbar收缩的高度
 * @param expandHeight    toolbar展开的高度
 */
@Composable
fun CollapsingToolbarScope.MyToolBar(
    state: CollapsingToolbarScaffoldState,
    title: String,
    pic: String,
    collapseHeight: Dp,
    expandHeight: Dp,
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(expandHeight)
            .pin()//表示固定那里不跟随toolbar变化
    )

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(collapseHeight)
            .pin()
    ) {
        IconButton(
            onClick = { /*navController.popBackStack() */ },
            modifier = Modifier.align(Alignment.CenterStart).padding(horizontal = 5.dp)
        ) {
            Icon(imageVector = Icons.Default.ArrowBackIos, contentDescription = null)
        }
    }
    
    //收缩state.toolbarState.progress为0 ,展开为1
    val progress = state.toolbarState.progress
    //38 :Toolbar收缩后image的大小,110是Toolbar展开后image的大小
    val imageSize = (38 + (110 - 38) * progress).dp
    //40 :Toolbar收缩后image x轴的偏移量,10:Toolbar展开后image x轴的偏移量
    val imageOffsetX = (40 + (10 - 40) * progress).dp
    //0 :Toolbar收缩后image y轴的偏移量,64:Toolbar展开后image y轴的偏移量
    val imageOffsetY = (0 + (64 - 0) * progress).dp

    Surface(elevation = 2.dp, color = Color.Transparent, shape = RoundedCornerShape(6.dp),
        modifier = Modifier
            .padding(5.dp)
            .size(imageSize)
            .offset(x = imageOffsetX, y = imageOffsetY)) {
        AsyncImage(
            model = pic,
            contentDescription = null,
        )
    }

    //90 :Toolbar收缩后Text X轴的偏移量,135:Toolbar展开后Text X轴的偏移量
    val offsetTextX = (90 + (135 - 90) * progress).dp
    Text(
        text = title,
        modifier = Modifier
            .padding(start = offsetTextX)
            .fillMaxWidth()
            .height(collapseHeight)
            .wrapContentHeight(if (progress < 0.2f) Alignment.CenterVertically else Alignment.Top)
            .offset(y = imageOffsetY),
        maxLines = if (progress < 0.2f) 1 else 2,
        overflow = TextOverflow.Ellipsis,
        textAlign = TextAlign.Start,
        fontSize = 18.sp
    )
}

toolbar完全折叠时state.toolbarState.progress = 0,完全展开时state.toolbarState.progress = 1

例如这个图片大小折叠时是38dp,展开大小是110dp图片大小就跟着progress动态去改变

imageSize = (38 + (110 - 38) * state.toolbarState.progress).dp

这里通过lerp方法去实现更好,例如:

val fontSize : TextUnit = androidx.compose.ui.unit.lerp(collapseFontSize, expandSize, progress)
val fontWeight : FontWeight =
    androidx.compose.ui.text.font.lerp(collapseFontWeight, expandFontWeight, progress)
val color :Color=
    androidx.compose.ui.graphics.lerp(collapseColor, expandColor, progress)
val imageSize:Dp = androidx.compose.ui.unit.lerp(collapseSize,expandSize,progress)

compose的单位包含color都有lerp方法

CollapsingToolbarScope 的Modifier扩展方法

  • fun Modifier.pin() :固定在那里不会移动。

  • fun Modifier.parallax(ratio: Float = 0.2f)会随着整体的滑动的偏移量*ratio去滑动,ratio为1就是和整体的滑动保持一致。

  • fun Modifier.road(whenCollapsed: Alignment, whenExpanded: Alignment):直接看它这里官方描述吧

  • fun Modifier.progress(listener: ProgressListener) 就不用再说了吧。

另外注意的一点是,如果scrollStrategy = ScrollStrategy.ExitUntilCollapsed,折叠时toolbar的高度是toolbar里所有的组合项最小的一个组合的高度,展开是最高的一个组合的高度

3、状态栏 toolbar 背景高斯模糊

/**
 * @param collapseHeight  toolbar收缩的高度
 * @param expandHeight    toolbar展开的高度
 * @param radiu           body顶部圆角度数,如果没有则为0
 */
@Composable
fun ToolBarBg(
    state: CollapsingToolbarScaffoldState,
    pic: String,
    collapseHeight: Float,
    expandHeight: Float,
    radiu: Float,
) {
    /**
     * 如果没有圆角可以把背景放到toolbar里面,可以直接用parallax属性
     * Image(painter = painter,
     *        modifier = Modifier
     *       .fillMaxWidth()
     *       .height(expandHeight)
     *       .parallax(1f)//1f 表示和整体的滑动保持一致
     */
    val offsetBgY =
        ((collapseHeight - expandHeight) + (0 - (collapseHeight - expandHeight)) * state.toolbarState.progress).dp
    Box(modifier = Modifier
        .fillMaxWidth()
        //状态栏的高度+toolbar展开的高度+圆角的度数
        .statusBarsHeight((expandHeight + radiu).dp)
        .offset(y = offsetBgY)
    ) {
        val painter = rememberAsyncImagePainter(
            model = ImageRequest.Builder(LocalContext.current)
                .data(pic)
                .transformations(CoilBlurTransformation(LocalContext.current, 25f, 2f))
                .build())
        Image(painter = painter,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .fillMaxSize())
        //蒙板 防止图片全白,导致看不到toolbar的内容
        Box(modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.onSurface.copy(alpha = 0.55f)))
    }
}

关于图片的高斯模糊可以看我另一篇文章Jetpack Compose Coil2.0 高斯模糊 - 掘金 (juejin.cn)

大家觉得这个奇怪我这为什么不把这个高斯模糊的背景放到toolbar里面去,因为toolbar和body之间是圆角的,如果放到toolbar里面去,这body圆角以外的这部分背景就不是这个高斯模糊的背景了,又有童靴说可以把顶部的圆角也放toolbar里面去,这样的话展开没问题收起来就会有问题了。

当然如果设计不是圆角的,可以把高斯模糊背景放到toolbar里面去:

val painter = rememberAsyncImagePainter(
            model = ImageRequest.Builder(LocalContext.current)
                .data(pic)
                .transformations(CoilBlurTransformation(LocalContext.current, 25f, 2f))
                .build())
Image(model = painter,
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .height(expandHeight)
        .parallax(1f))//1f 表示和整体的滑动保持一致

4.最后是activity

class CollapsingToolbarActivity :ComponentActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            ProvideWindowInsets {
               MyAppTheme {
                   val systemUiController: SystemUiController = rememberSystemUiController()
                   //状态栏背景设为透明,icon为白色
                   systemUiController.setStatusBarColor(Color.Transparent, false)

                   CollapsingToolbarExample()
               }
            }
        }
    }
}

5. 拖动toolbar,无法折叠/展开的问题

拖动content可以折叠/展开,但是拖动toolbar,无法折叠/展开,但是在Android的CollapsingToolbarLayout拖动toolbar可以展开和折叠,这个库就不行有bug;然后我也在compose-collapsing-toolbar也找到了这个issues,作者说会在3.0版本解决这个问题,但问题是目前是compose-collapsing-toolbar才2.3.4版本,不知道啥时候更新到3.0版本。

如果toolbar的高度比较高这就是个严重的问题了,用户想要折叠查看content的内容只能向上拖动toolbar下面的content。

解决方案

toolbarModifier = Modifier.verticalScroll(rememberScrollState())

我也在这个issues提交了评论:

image.png

我写这一篇文章Jetpack compose 仿QQ音乐实现下拉刷新上拉加载更多 - 掘金 (juejin.cn)的时候研究过NestedScrollConnection,然后fork了compose-collapsing-toolbar,在ScrollStrategy的NestedScrollConnection里面加了打印,发现拖动工具栏,这个NestedScrollConnection里面没有任何打印,滑动下面的LazyColumn就有打印。

这是由于父compose的NestedScrollConnection的那些方法都是由子compose可组合项把events传递过去的。而toolbar没有设置verticalScroll也不是LazyColumn,它就没有滑动,没有滑动何来把events传递给父compose的NestedScrollConnection

最后效果(拖动toolbar可以折叠、展开工具栏了):

27dbl-y2rtf.gif