🫠
`Layout()`でサイズを強制したTextがはみ出る問題を対処する
以前作成した自動スクロールするtextでは以下のように配置すると表示範囲外でもtextが表示されてしまう問題があった。
以前の記事↓
擬似的な再現のPreview
@Preview
@Composable
private fun AutoScrollTextPreview() {
Box(
modifier = Modifier
.width(300.dp)
.background(Color.Gray),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.background(Color.White)
.width(100.dp)
) {
AutoScrollText(
text = "abcdefghijklmnopqrstuvwxyz",
autoScrollType = AutoScrollType.Duration(3000, 500),
startVisualize = true,
modifier = Modifier
)
AutoScrollText(
text = "abcdefghijklmnopqrstuvwxyz",
autoScrollType = AutoScrollType.Duration(3000),
startVisualize = false
)
AutoScrollText(
text = "abcdefghijklmnopqrstuvwxyz",
autoScrollType = AutoScrollType.PerCharacterLength(20),
startVisualize = true
)
AutoScrollText(
text = "abcdefghijklmnopqrstuvwxyz",
autoScrollType = AutoScrollType.PerCharacterLength(10),
startVisualize = false
)
}
}
}
gray
の部分がcomposable外の想定で、white
が親Layoutの表示範囲。
これでは他のcomposableと組み合わせて使う時に不便
これをtextの表示されている範囲のインデックスを計算して、それ以外はColor.Transparent
で上書きする。
@Composable
fun AutoScrollText(
modifier: Modifier = Modifier,
text: String,
style: TextStyle = LocalTextStyle.current,
autoScrollType: AutoScrollType = AutoScrollType.Duration(5000),
startVisualize: Boolean = true,
) {
val textWidth = remember { mutableIntStateOf(0) }
val layoutWidth = remember { mutableIntStateOf(0) }
val transitionWidth =
if (startVisualize) textWidth.intValue - layoutWidth.intValue
else textWidth.intValue
val animatedOffsetX = remember { Animatable(0f) }
val visibleCharRange = remember(text) { mutableStateOf(text.indices) }
LaunchedEffect(transitionWidth) {
while (
// layoutサイズに収まっていれば何もしない
transitionWidth > 0
) {
animatedOffsetX.snapTo(if (startVisualize) 0f else layoutWidth.intValue.toFloat())
delay(autoScrollType.delayMillis)
animatedOffsetX.animateTo(
targetValue = -transitionWidth.toFloat(),
animationSpec = tween(
durationMillis = when (autoScrollType) {
is AutoScrollType.Duration -> autoScrollType.millis
is AutoScrollType.PerCharacterLength -> transitionWidth * autoScrollType.millis
}.toInt(),
easing = LinearEasing,
)
)
delay(autoScrollType.delayMillis)
}
}
LaunchedEffect(Unit) {
// 1文字あたりの幅
val charWidth = textWidth.intValue / text.length.toFloat()
snapshotFlow {
animatedOffsetX.value
}.collect {
// 現在のアニメーションオフセットに基づく開始インデックスと終了インデックスを計算
val startIndex = ceil(-animatedOffsetX.value / charWidth).toInt()
val visibleSize = floor(layoutWidth.intValue / charWidth).toInt()
val endIndex = minOf(startIndex + visibleSize, text.length)
// 表示中の文字範囲を更新
visibleCharRange.value = startIndex until endIndex
}
}
Layout(
modifier = modifier,
content = {
Text(
text = buildAnnotatedString {
text.forEachIndexed { index, c ->
if (visibleCharRange.value.contains(index)) {
append(c)
} else {
// 表示範囲でなければ透過色にする
withStyle(style = SpanStyle(color = Color.Transparent)) {
append(c)
}
}
}
}
,
style = style,
maxLines = 1,
overflow = TextOverflow.Clip,
)
},
measurePolicy = { measurable, constraints ->
val textMeasurable = measurable.first()
layoutWidth.intValue = constraints.maxWidth
val placeable = textMeasurable.measure(
// layoutのサイズに関わらず全てを表示する
constraints.copy(maxWidth = Int.MAX_VALUE)
)
// textのサイズを取得する
textWidth.intValue = placeable.width
layout(layoutWidth.intValue, placeable.height) {
placeable.place(animatedOffsetX.value.toInt(), 0)
}
}
)
}
結果↓
いい感じに非表示?に出来るようになる
Discussion