android-jetpack-composeandroid-jetpack-compose-animation

Simulating vintage blinking cursor in Jetpack Compose


I'm developing a WearOS application, and for a specific piece of data I decided — I'm toying with the idea — that the most compact display would be a matrix of dots.

Each dot can be: OFF (almost black, but not totally), ON (vintage green phosphor) and BLINKING (swinging between ON and OFF).

I did it like this:

internal enum class SegmentStatus { OFF, ON, BLINKING }

internal val segment = Modifier.padding(.2f.dp).width(4.dp).height(6.dp).clip(RoundedCornerShape(2.dp))
internal val mapSegmentStateToModifier = mapOf(
    OFF to segment.background(Color.DarkGray),
    ON to segment.background(Color.Green),
    BLINKING to segment.background(Color.White),
)

@Composable
@Preview
fun PreviewDotMapDisplay() {
    val matrix = listOf(
        listOf(ON, ON, ON, ON),
        listOf(ON, ON, ON, OFF),
        listOf(OFF, OFF, OFF, OFF),
        listOf(ON, ON, ON, OFF),
        listOf(ON, ON, ON, BLINKING),
        listOf(ON, OFF, OFF, OFF),
        listOf(OFF, OFF, OFF, OFF),
    )

    Column(modifier = Modifier.background(Color.Black)) {
        matrix.forEach { row ->
            Row {
                row.map { mapSegmentStateToModifier[it] ?: Modifier }
                    .forEach { Spacer(modifier = it) }
            }
        }
    }
}

Output:

PreviewDotMapDisplay output

How to make that white "dot" blink? I have no idea

It can be a simple blinking, like fully ON then fully OFF. From there I think I can modify it to add some whistles (ramping up and phosphor overshot, then ramping down and ghosting).


Apart from that main question, I'd like also to hear from you about that code above.

Is it too much of a burden on a smart watch to compose that many Columns and Rows and Spacer? The alternative would be to draw those shapes. — consider that I expect at most 10 lines and 5 columns

Also, that elvis operator in row.map { mapSegmentStateToModifier[it] ?: Modifier } annoys me a little, because I know that the else branch is never taken. But kotlin doesn't. Is it possible to create a mapping guaranteed to be exhaustive (with syntax error on constructor if not)?


Solution

  • I did it with:

        val offColor = Color.DarkGray
        val onColor = Color.Green
        val blinkColor = rememberInfiniteTransition("blinking cursor").animateColor(
            initialValue = offColor,
            targetValue = onColor,
            animationSpec = infiniteRepeatable(
                animation = tween(500),
                repeatMode = RepeatMode.Reverse
            ),
            label = "blinking cursor"
        ).value
    

    Then I scoped mapSegmentStateToModifier to the function (instead of the global scope, because rememberInfiniteTransition cannot be called outside @Composable):

    val mapSegmentStateToModifier = mapOf(
        OFF to segment.background(offColor),
        ON to segment.background(onColor),
        BLINKING to segment.background(blinkColor),
    )
    

    It did the trick. Now I'll look forward to the other whistles.