🎉

Jetpack Composeで自動スクロールするテキストの作成

2024/11/25に公開1

Layout()を使って文字列のサイズに合わせてアニメーションさせる

@Composable
fun AutoScrollText(
    modifier: Modifier = Modifier,
    text: String,
    style: TextStyle = LocalTextStyle.current,
) {
    val transition = rememberInfiniteTransition(
        label = "AutoScrollTextRepeater"
    )
    val textWidth = remember { mutableIntStateOf(0) }
    val layoutWidth = remember { mutableIntStateOf(0) }

    val transitionWidth = textWidth.intValue

    val animateX: Int by transition.animateValue(
        initialValue = layoutWidth.intValue,
        targetValue = -transitionWidth,
        typeConverter = Int.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = transitionWidth * 15,
                delayMillis = 300,
                easing = LinearEasing
            ),
        )
    )

    Layout(
        modifier = modifier,
        content = {
            Text(
                text = text,
                style = style,
                maxLines = 1,
            )
        },
        measurePolicy = { measurable, constraints ->
            val textMeasurable = measurable.first()

            layoutWidth.intValue = constraints.maxWidth

            val placeable = textMeasurable.measure(
                // layoutのサイズに関わらず全てを表示する
                constraints.copy(maxWidth = Int.MAX_VALUE)
            )
            val fixedWidth = placeable.width
            textWidth.intValue = fixedWidth
            layout(layoutWidth.intValue, placeable.height) {
                placeable.place(animateX, 0)
            }
        }
    )
}

@Preview
@Composable
private fun AutoScrollTextPreview0() {
    Column(
        modifier = Modifier
            .width(100.dp)
    ) {
        AutoScrollText(
            text = "abcdefghijklmnopqrstuvwxyz",
        )
    }
}

バリエーションを作ってみる


sealed class AutoScrollType(open val delayMillis: Long) {
    // 指定秒数かけてscrollする
    data class Duration(val millis: Long, override val delayMillis: Long = 0) : AutoScrollType(delayMillis)
    // 文字列の長さに関わらず一定の速度で移動する
    data class PerCharacterLength(val millis: Long, override val delayMillis: Long = 0) : AutoScrollType(delayMillis)
}

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

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

    Layout(
        modifier = modifier,
        content = {
            Text(
                text = text,
                style = style,
                maxLines = 1
            )
        },
        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)
            }
        }
    )
}

@Preview
@Composable
private fun AutoScrollTextPreview() {
    Column(
        modifier = Modifier
            .width(100.dp)
    ) {
        AutoScrollText(
            text = "abcdefghijklmnopqrstuvwxyz",
            autoScrollType = AutoScrollType.Duration(3000, 500),
            startVisualize = true
        )
        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
        )
    }
}
GitHubで編集を提案