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))
    }
}

最终效果:

相关推荐
2501_916007471 天前
苹果手机iOS应用管理全指南与隐藏功能详解
android·ios·智能手机·小程序·uni-app·iphone·webview
LFly_ice1 天前
Nest-管道
android·java·数据库
ab_dg_dp1 天前
android bugreport 模块源码分析
android
2501_915106321 天前
全面理解 iOS 帧率,构建从渲染到系统行为的多工具协同流畅度分析体系
android·ios·小程序·https·uni-app·iphone·webview
繁星星繁1 天前
【Mysql】数据库基础
android·数据库·mysql
李坤林1 天前
Android 12 中 App 与 SurfaceFlinger(SF)的 Vsync 通信机制
android·surfaceflinger
高远-临客1 天前
unity IL2CPP模式下中使用UMP插件打包后无法播放视频监控报错问题解决方案
android·unity·音视频
装不满的克莱因瓶1 天前
Windows下安装Dart
android·flutter·dart·移动端
Yao_YongChao1 天前
adb wifi连接Android手机
android·adb·智能手机·无线连接手机·wifi连接手机
安果移不动1 天前
git Cherry-Pick合并分支上的某些commits-》Android studio
android·git·android studio