Search code examples
androidkotlinandroid-jetpack-compose

Text with stroke or borders in jetpack compose


I've been trying to get text with actual borders working on Jetpack Compose.

I tried using the shadow style in the Text composable but it doesn't work nor look very good

Text(
     text = "lorem ipsum",
     style = TextStyle(shadow = Shadow(color = borderColor, blurRadius = 7.5f)),
)

using shadow results in something like this
enter image description here

but what i need should more akin to this
enter image description here


Solution

  • Only outlining the text can be accomplished with a different DrawStyle:

    Text(
        text = "lorem ipsum",
        style = TextStyle(
            color = borderColor,
            drawStyle = Stroke(),
        ),
    )
    

    There are various customizations available to Stroke, take a look at its parameters.

    This will, however, only draw the outline, the interior stays empty and the background will be seen instead. I don't think there's a built-in way to also fill the interior. As a workaround you could draw the Text twice, the first with the outline only, then ontop regular, filled text:

    @ExperimentalComposeUiApi
    @Composable
    fun OutlinedText(
        text: String,
        modifier: Modifier = Modifier,
        fillColor: Color = Color.Unspecified,
        outlineColor: Color,
        fontSize: TextUnit = TextUnit.Unspecified,
        fontStyle: FontStyle? = null,
        fontWeight: FontWeight? = null,
        fontFamily: FontFamily? = null,
        letterSpacing: TextUnit = TextUnit.Unspecified,
        textDecoration: TextDecoration? = null,
        textAlign: TextAlign? = null,
        lineHeight: TextUnit = TextUnit.Unspecified,
        overflow: TextOverflow = TextOverflow.Clip,
        softWrap: Boolean = true,
        maxLines: Int = Int.MAX_VALUE,
        minLines: Int = 1,
        onTextLayout: (TextLayoutResult) -> Unit = {},
        style: TextStyle = LocalTextStyle.current,
        outlineDrawStyle: Stroke = Stroke(),
    ) {
        Box(modifier = modifier) {
            Text(
                text = text,
                modifier = Modifier.semantics { invisibleToUser() },
                color = outlineColor,
                fontSize = fontSize,
                fontStyle = fontStyle,
                fontWeight = fontWeight,
                fontFamily = fontFamily,
                letterSpacing = letterSpacing,
                textDecoration = null,
                textAlign = textAlign,
                lineHeight = lineHeight,
                overflow = overflow,
                softWrap = softWrap,
                maxLines = maxLines,
                minLines = minLines,
                onTextLayout = onTextLayout,
                style = style.copy(
                    shadow = null,
                    drawStyle = outlineDrawStyle,
                ),
            )
    
            Text(
                text = text,
                color = fillColor,
                fontSize = fontSize,
                fontStyle = fontStyle,
                fontWeight = fontWeight,
                fontFamily = fontFamily,
                letterSpacing = letterSpacing,
                textDecoration = textDecoration,
                textAlign = textAlign,
                lineHeight = lineHeight,
                overflow = overflow,
                softWrap = softWrap,
                maxLines = maxLines,
                minLines = minLines,
                onTextLayout = onTextLayout,
                style = style,
            )
        }
    }
    

    Now you can simply use this:

    OutlinedText(
        text = "lorem ipsum",
        outlineColor = borderColor,
    )
    

    The Stroke customization for the outline can be supplied by the optional outlineDrawStyle parameter, the fill color can be specified with fillColor.

    This new OutlinedText should work the same as the regular Text, so you can also use all the other parameters, even the style parameter. I am not sure how robust it will handle the modifier parameter though, I can imagine some edge cases where this will behave differently.

    To accomodate for the change in semantics (because there are now two Texts, even if it only looks like one), I hid the second text from the semantics tree. That's necessary for accessibilty (screen readers/TalkBack), instrumentation tests and so on, but I haven't actually tested its behavior.