はじめに
今回はトグルボタンを独自で実装していきます
コード
/**
* テキストベースのトグルスイッチコンポーネント
*
* 2つのテキストオプションを切り替えるセグメントコントロール風のUI
* 例: "今回の記録" ⇔ "学習済の記録"
*
* @param leftText 左側のテキスト
* @param rightText 右側のテキスト
* @param selectedIndex 選択されているインデックス(0: 左, 1: 右)
* @param onSelectedChange 選択が変更されたときのコールバック
* @param modifier Modifier
* @param enabled 有効かどうか
* @param backgroundColor 背景色
* @param selectedBackgroundColor 選択された部分の背景色
* @param selectedTextColor 選択されたテキストの色
* @param unselectedTextColor 選択されていないテキストの色
* @param height コンポーネントの高さ
*/
@Composable
fun TextToggle(
leftText: String,
rightText: String,
selectedIndex: Int,
onSelectedChange: (Int) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
backgroundColor: Color = AppColors.Gray300,
selectedBackgroundColor: Color = AppColors.Gray800,
selectedTextColor: Color = AppColors.White,
unselectedTextColor: Color = AppColors.Gray900,
disabledBackgroundColor: Color = AppColors.Gray300,
disabledTextColor: Color = AppColors.Gray600,
height: Dp = 56.dp,
) {
var containerWidthPx by remember { mutableStateOf(0) }
// アニメーション設定
val animationSpec = tween<Float>(durationMillis = 250)
val slideOffset by animateFloatAsState(
targetValue = if (selectedIndex == 0) 0f else 1f,
animationSpec = animationSpec,
label = "slideOffset",
)
// 有効/無効に応じた色の設定
val currentBackgroundColor = if (enabled) backgroundColor else disabledBackgroundColor
// 選択されている部分の背景色は常に同じ(有効/無効に関わらず)
val currentSelectedBackgroundColor = selectedBackgroundColor
Box(
modifier = modifier
.fillMaxWidth()
.height(height)
.clip(RoundedCornerShape(height / 2))
.background(currentBackgroundColor)
.padding(4.dp)
.onSizeChanged { size ->
containerWidthPx = size.width
},
) {
// 選択されている部分の背景(スライドするボックス)
Box(
modifier = Modifier
.fillMaxWidth(0.5f)
.height(height - 8.dp)
.offset {
val maxOffset = (containerWidthPx / 2)
IntOffset((maxOffset * slideOffset).toInt(), 0)
}
.clip(RoundedCornerShape((height - 8.dp) / 2))
.background(currentSelectedBackgroundColor),
)
// テキストボタン
Row(
modifier = Modifier
.fillMaxWidth()
.height(height - 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// 左側のテキスト
Box(
modifier = Modifier
.weight(1f)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
enabled = enabled,
onClick = { onSelectedChange(0) },
),
contentAlignment = Alignment.Center,
) {
val textColor by animateColorAsState(
targetValue = if (selectedIndex == 0) {
selectedTextColor
} else if (!enabled) {
disabledTextColor
} else {
unselectedTextColor
},
animationSpec = tween(durationMillis = 250),
label = "leftTextColor",
)
Text(
text = leftText,
color = textColor,
fontSize = 16.sp,
fontWeight = if (selectedIndex == 0) FontWeight.Bold else FontWeight.Normal,
textAlign = TextAlign.Center,
)
}
// 右側のテキスト
Box(
modifier = Modifier
.weight(1f)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
enabled = enabled,
onClick = { onSelectedChange(1) },
),
contentAlignment = Alignment.Center,
) {
val textColor by animateColorAsState(
targetValue = if (selectedIndex == 1) {
selectedTextColor
} else if (!enabled) {
disabledTextColor
} else {
unselectedTextColor
},
animationSpec = tween(durationMillis = 250),
label = "rightTextColor",
)
Text(
text = rightText,
color = textColor,
fontSize = 16.sp,
fontWeight = if (selectedIndex == 1) FontWeight.Bold else FontWeight.Normal,
textAlign = TextAlign.Center,
)
}
}
}
}
最後に
テキストベースはないので作ってみましたが、若干入れ子が多くてみづらいなぁというのが感想でした