{
"$type": "site.standard.document",
"canonicalUrl": "https://segunfamisa.com/posts/exploring-custom-text-rendering-in-compose",
"description": "Notes from exploring custom text rendering in Jetpack Compose",
"path": "/posts/exploring-custom-text-rendering-in-compose",
"publishedAt": "2026-01-11T06:00:00.000Z",
"site": "at://did:plc:a5mekodp4afxadlpr4hp2wci/site.standard.publication/3mm2oa7vz5327",
"tags": [
"android",
"jetpack compose"
],
"textContent": "> TL;DR: exploring TextMeasurer, TextLayoutResult and Canvas for custom text rendering with Jetpack compose\n\nMy coworker showed me this article from devtechie on how to implement custom text rendering in SwiftUI. I was a bit envious of how easy it looked and I thought...\"hmm, I think we should actually be able to do this in compose quite easily too\". So, I decided to explore the lower level text APIs available on Compose and challenged myself to recreate the renderings with Compose.\n\nBefore I talk through my take on some of the custom text renderings I want to recreate, I want to get straight to the main APIs that we can use to achieve the effects.\n\nJetpack Compose Lower-Level Text APIs\n\nTextMeasurer\n\nThe TextMeasurer API allows us to - you guessed it - measure the text before we draw it. It takes into account the style, text, constraints, and other things that could influence the size of the text. TextMeasurer has a measure function that returns a TextLayoutResult. The typical way one would create a text measurer is:\n\nTextLayoutResult\n\nThe TextLayoutResult is where the fun is. It contains information about the text being laid out. With TextLayoutResult, you can do a lot of cool things. You can get the number of lines required to draw the text via TextLayoutResult.lineCount. It also allows us to get the size of the text via TextLayoutResult.size.\n\nI really like this API because it's so powerful - it even gives us character by character information. You can get the bounding box Rect of a particular index in the text, and this is powerful for influencing the text rendering on a character-by-character basis. \n\n> I strongly recommend going through the TextResultLayout docs to see what you can do. \n\nSome APIs from the class that we will use in the rest of this post include:\n\n getLineStart) / getLineEnd) - allows you to get the start or end character index drawn on a specific line. You can use that to determine which character is first or last on a specific line.\n getLineLeft) / getLineRight). I found this a bit confusing, but unlike the start / end equivalents which return the character index in the text, these APIs return the x coordinates of the beginning or end of a specific line. \n getLineTop) / getLineBottom) - These return the top / bottom coordinates of a specific line. You can combine these with the left / right APIs to determine the bounding rectangle for each line.\n\nCanvas\n\nThen finally, the Canvas API - this is where we actually get to draw our text, applying various transforms as required.\n\nRecreating the custom renderings with Compose\n\nNow that we've talked through the basics of the lower-level text APIs, we can now try to recreate some of the examples in the reference article.\n\n> These are by no means the only way to solve these problems.\n\nFaded Text\n\nLet's start with the faded text. A multi-line text that fades gradually, until the last line is fully visible.\n\n<figure style=\"text-align: center;\">\n <img src=\"/assets/r-LREnBLvnX1Yfrrgk8b2X2M0F5jDTS4nH72yKH4sc8=.png\" alt=\"Faded Text from https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui \">\n <figcaption>Faded Text from <a href=\"https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui\">https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui</a></figcaption>\n</figure>\n\nConceptually, how I thought of this was: \"if we can figure out how many lines, we can then apply an alpha to the text on each line, but the alpha value should uniformly increase across the lines\".\n\nLuckily for us, we can use the TextMeasurer to do this, as I described previously, and then calculate the alpha values.\n\nAnd here's the result:\n\n<figure style=\"text-align: center;\">\n <img \n src=\"/assets/peujt8PuYte48kXDEcQeNx2wv_5MOgPzsOLb_jKUEPM=.png\"\n width=\"300\"\n alt=\"FadedText implementation with Jetpack Compose\"\n >\n <figcaption>\n FadedText implementation with Jetpack Compose.\n </figcaption>\n</figure>\n\nIt took a little more code, compared to the SwiftUI equivalent by DevTechie, but it still seems rather easy. I cannot imagine how non-trivial this would have been in the old View system on Android.\n\nWarped Text\n\nLet's try to recreate the warped text. \n\n<figure style=\"text-align: center;\">\n <img \n src=\"/assets/CAToFxFCVinlzhsbAEf18m5eWCj21Rz30qSOkCX4ld4=.png\"\n width=\"300\"\n alt=\"Warped text in SwiftUI by DevTechie - https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui\"\n > \n <figcaption>\n Warped text in SwiftUI by DevTechie - <a href=\"https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui\">https://www.devtechie.com/blog/textrenderer-protocol-in-ios-18-and-swiftui</a>\n </figcaption>\n</figure>\n\nThis one looked interesting. My intuition was that it looks like we need to move each character up or down in a wave. Like a Sine wave. Unlike the previous example, which we drew line-by-line, we have to do this one character-by-character. So let's see how that will work.\n\nAnd the output looks like this. Not bad at all.\n\n<figure style=\"text-align: center;\">\n <img \n src=\"/assets/af1_fneBeGPfESIVXljm196BU9ipTPBFakk9igp7Occ=.png\"\n width=\"300\"\n alt=\"WarpedText with Jetpack Compose\"\n >\n <figcaption>\n WarpedText with Jetpack Compose.\n </figcaption>\n</figure>\n\nAnimated Warped Text\n\nThis one was interesting. It works mostly like the previous solution, but what if we wanted to animate it going up and down? Thankfully, we can easily create an infinite animation in Jetpack Compose using the rememberInfiniteTransition API.\n\nSo we want the amplitude of the sine wave. This one requires a bit of knowing how a sine wave works. A quick crash course is that the amplitude is the \"height\" of the warping. In the previous example, we set it to 5. So, that is what we want to animate.\n\nAnd here's the result:\n\n<figure style=\"text-align: center;\">\n <img \n src=\"/assets/8QlJs2X4iSE3xy0o1ryO08S3do3V7WmiLmoN0BcYUVA=.gif\"\n width=\"300\"\n alt=\"Animated warped text in Compose\"\n >\n <figcaption>\n Animated warped text in Compose.\n </figcaption>\n</figure>\n\nTypewriter Text\n\nSince I was already doing this, I thought to go one extra step, to recreate a typewriter effect such that each character appears.\n\nThis time, I'll show the final effect first, before the code, so that we can think about it.\n\n<figure style=\"text-align: center;\">\n <img \n src=\"/assets/Uso8XrdeQydfUsWcNnZATbphQAnosJyrBUdQb-mxuYA=.gif\"\n width=\"300\"\n alt=\"Typewriter effect\"\n >\n <figcaption>\n Typewriter effect.\n </figcaption>\n</figure>\n\nTo achieve this kind of thing, we need to smoothly display each character. My approach here was to use a value animation for the text length, and always draw the substring only as far as the animation has played.\n\nWrapping up\n\nThat turned out not to be difficult at all. The code was a bit more verbose than the SwiftUI equivalents. \nDespite that I have never had to do custom text rendering on compose, it was still fairly straightforward to do. I was mostly curious to see how easily we could replicate the things from the article and I was pleasantly surprised.\n\nYou can see the full source code for these explorations here:\n FadedText\n WarpedText\n AnimatedWarpedText\n* TypewriterText",
"title": "Exploring Custom Text Rendering with Jetpack Compose"
}