Jetpack Compose 实战:自定义自适应分段按钮 (Segmented Button)

前言

如果要从并排显示的选项中选择,我们可以使用分段按钮(Segmented Button)

图片来自:分段按钮

分段按钮有两种类型:单选多选 ,对应了 SingleChoiceSegmentedButtonRowMultiChoiceSegmentedButtonRow 布局。

我这里只演示一下单选分段按钮。

单选分段按钮

代码如下:

kotlin 复制代码
@Preview
@Composable
fun SingleChoiceSegmentedButton() {
    // 当前选中按钮的索引
    var selectedIndex by remember { mutableIntStateOf(0) }
    val options = listOf("Sleep", "Work")

    SingleChoiceSegmentedButtonRow {
        options.forEachIndexed { index, label ->
            SegmentedButton(
                // 根据按钮索引和按钮总数获取按钮的形状
                // 只有一项时:全圆;
                // 第一项:起始端圆角(Start);
                // 最后一项:末端圆角(End);
                // 中间项:直角
                shape = SegmentedButtonDefaults.itemShape(
                    index = index,
                    count = options.size
                ),
                onClick = { selectedIndex = index },
                selected = index == selectedIndex,
                label = { Text(label) }
            )
        }
    }
}

运行效果:

上述代码逻辑并不难,我们只是提供了一个标签列表 [ "Sleep", "Work" ],然后使用了 SingleChoiceSegmentedButtonRowSegmentedButton 的组合,就实现了以上效果。

这个效果在大多时候都很不错,但有时按钮中的文本会显得有些拥挤,例如:

这时,我们就需要自定义组件,让被选中的按钮的大小动态变化。

自适应分段按钮

分段按钮的 Material 3 规范:Segmented buttons

首先定义出可选项的数据模型:

kotlin 复制代码
@Immutable // 优化重组检查
data class OptionItem<T>(
    val value: T,
    val label: String,
    val selected: Boolean = false
)

然后就来完成外面的布局和内部的每一项:

kotlin 复制代码
@Composable
fun <T> LitheSegmentedButton(
    modifier: Modifier = Modifier,
    items: List<OptionItem<T>>,
    onClick: (T) -> Unit
) {
    val itemSize = items.size
    
    // 外层容器:负责绘制整体的边框和圆角
    Row(
        modifier = modifier
            .clip(CircleShape)
            .border(
                width = 0.5.dp,
                color = SegmentedButtonDefaults.colors().activeBorderColor,
                shape = CircleShape
            )
            .padding(0.5.dp)
    ) {
        items.forEachIndexed { index, item ->
            // 缓存 Shape 对象,避免每次重组时都重新创建
            val shape = remember(index, itemSize) {
                itemShape(index, itemSize)
            }

            SingleSegmentedButton(
                item = item,
                shape = shape,
                onClick = { onClick(item.value) }
            )
        }
    }
}

@Composable
private fun <T> SingleSegmentedButton(
    item: OptionItem<T>,
    shape: Shape,
    colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
    onClick: () -> Unit
) {
    Row(
        Modifier
            .height(40.dp)
            .clip(shape)
            .clickable(onClick = onClick)
            .border(
                width = 0.5.dp,
                color = colors.activeBorderColor,
                shape = shape
            )
            .padding(0.5.dp)
            .background( // 选中状态下的背景色
                if (item.selected) colors.activeContainerColor
                else Color.Transparent,
                shape = shape
            )
            .padding(horizontal = 18.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (item.selected) {
            Row {
                Icon(
                    imageVector = Icons.Default.Done,
                    contentDescription = null,
                    modifier = Modifier
                        .size(18.dp),
                    tint = colors.activeContentColor
                )
                Spacer(modifier = Modifier.width(8.dp))
            }
        }

        Text(
            text = item.label,
            style = MaterialTheme.typography.labelLarge.copy(
                color = if (item.selected) colors.activeContentColor
                else colors.inactiveContentColor
            )
        )
    }
}

/**
 * 根据索引和总数计算按钮的形状
 */
fun itemShape(index: Int, count: Int): Shape {
    if (count == 1) {
        return CircleShape
    }
    return when (index) {
        // 第一个:起始端(左侧)圆角
        0 -> RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 50)
        // 最后一个:末端(右侧)圆角
        count - 1 -> RoundedCornerShape(topEndPercent = 50, bottomEndPercent = 50)
        // 中间:直角
        else -> RoundedCornerShape(0)
    }
}

预览一下:

kotlin 复制代码
@Preview(showBackground = true)
@Composable
fun LitheSegmentedButtonPreview() {
    var selectedIndex by remember { mutableIntStateOf(0) }
    val items = listOf(
        OptionItem(0, "Day", selected = selectedIndex == 0),
        OptionItem(1, "Month", selected = selectedIndex == 1),
        OptionItem(2, "Week", selected = selectedIndex == 2),
    )
    Box(modifier = Modifier.fillMaxSize()) {
        LitheSegmentedButton(
            items = items,
            onClick = {
                selectedIndex = it
            },
            modifier = Modifier.align(Alignment.Center)
        )
    }
}

运行效果:

最后一步就是加上动画,我们使用 AnimatedVisibility 来完成。

具体内容可以看我的这篇博客:控制内容的显隐:AnimatedVisibility

为了实现"推动"效果,我们使用了 expandInshrinkOut。并且为了让这个效果看起来更好,又加上淡入淡出、缩放以及滑动效果:

  • 出现时,是淡入、水平展开、缩放以及轻微的纵向滑入效果。
  • 离开时,则是淡出、水平收缩、缩放以及纵向滑出的组合。
kotlin 复制代码
AnimatedVisibility(
    visible = item.selected,
    enter = fadeIn() + expandHorizontally() + scaleIn() + slideInVertically(initialOffsetY = { it / 2 }),
    exit = fadeOut() + shrinkHorizontally() + scaleOut() + slideOutVertically(targetOffsetY = { it / 2 })
) {
    Row {
        Icon(
            imageVector = Icons.Default.Done,
            contentDescription = null,
            modifier = Modifier
                .size(18.dp),
            tint = colors.activeContentColor
        )
        Spacer(modifier = Modifier.width(8.dp))
    }
}

最终效果:

相关推荐
RainyJiang8 小时前
谱写Kotlin协程面试进行曲-进阶篇(第二乐章)
面试·kotlin·android jetpack
mygljx11 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
xinhuanjieyi12 小时前
ruoyimate导入sql\antflow\bpm_init_db.sql报错
android·数据库·sql
闲猫13 小时前
基于RABC的权限控制设计
android
星霜笔记16 小时前
GitMob — 手机端 GitHub 管理工具
android·kotlin·github·android jetpack
LiuYaoheng17 小时前
问题记录:Android Studio Low memory
android·ide·android studio
独隅17 小时前
Python 标准库 (Standard Library) 全面使用指南
android·开发语言·python
always_TT18 小时前
strlen、strcpy、strcat等常用字符串函数
android
qqty121718 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
2401_8955213418 小时前
MySQL中between and的基本用法
android·数据库·mysql