TextShaper  |  API reference  |  Android Developers

Provides text shaping for multi-styled text. Here is an example of animating text size and letter spacing for simple text.

 
 // In this example, shape the text once for start and end state, then animate between two shape
 // result without re-shaping in each frame.
 class SimpleAnimationView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : View(context, attrs, defStyleAttr) {
     private val textDir = TextDirectionHeuristics.LOCALE
     private val text = "Hello, World."  // The text to be displayed

     // Class for keeping drawing parameters.
     data class DrawStyle(val textSize: Float, val alpha: Int)

     // The start and end text shaping result. This class will animate between these two.
     private val start = mutableListOf<Pair<PositionedGlyphs, DrawStyle>>()
     private val end = mutableListOf<Pair<PositionedGlyphs, DrawStyle>>()

     init {
         val startPaint = TextPaint().apply {
             alpha = 0 // Alpha only affect text drawing but not text shaping
             textSize = 36f // TextSize affect both text shaping and drawing.
             letterSpacing = 0f // Letter spacing only affect text shaping but not drawing.
         }

         val endPaint = TextPaint().apply {
             alpha = 255
             textSize =128f
             letterSpacing = 0.1f
         }

         TextShaper.shapeText(text, 0, text.length, textDir, startPaint) { _, _, glyphs, paint ->
             start.add(Pair(glyphs, DrawStyle(paint.textSize, paint.alpha)))
         }
         TextShaper.shapeText(text, 0, text.length, textDir, endPaint) { _, _, glyphs, paint ->
             end.add(Pair(glyphs, DrawStyle(paint.textSize, paint.alpha)))
         }
     }

     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)

         // Set the baseline to the vertical center of the view.
         canvas.translate(0f, height / 2f)

         // Assume the number of PositionedGlyphs are the same. If different, you may want to
         // animate in a different way, e.g. cross fading.
         start.zip(end) { (startGlyphs, startDrawStyle), (endGlyphs, endDrawStyle) ->
             // Tween the style and set to paint.
             paint.textSize = lerp(startDrawStyle.textSize, endDrawStyle.textSize, progress)
             paint.alpha = lerp(startDrawStyle.alpha, endDrawStyle.alpha, progress)

             // Assume the number of glyphs are the same. If different, you may want to animate in
             // a different way, e.g. cross fading.
             require(startGlyphs.glyphCount() == endGlyphs.glyphCount())

             if (startGlyphs.glyphCount() == 0) return@zip

             var curFont = startGlyphs.getFont(0)
             var drawStart = 0
             for (i in 1 until startGlyphs.glyphCount()) {
                 // Assume the pair of Glyph ID and font is the same. If different, you may want
                 // to animate in a different way, e.g. cross fading.
                 require(startGlyphs.getGlyphId(i) == endGlyphs.getGlyphId(i))
                 require(startGlyphs.getFont(i) === endGlyphs.getFont(i))

                 val font = startGlyphs.getFont(i)
                 if (curFont != font) {
                     drawGlyphs(canvas, startGlyphs, endGlyphs, drawStart, i, curFont, paint)
                     curFont = font
                     drawStart = i
                 }
             }
             if (drawStart != startGlyphs.glyphCount() - 1) {
                 drawGlyphs(canvas, startGlyphs, endGlyphs, drawStart, startGlyphs.glyphCount(),
                         curFont, paint)
             }
         }
     }

     // Draws Glyphs for the same font run.
     private fun drawGlyphs(canvas: Canvas, startGlyph: PositionedGlyphs,
                            endGlyph: PositionedGlyphs, start: Int, end: Int, font: Font,
                            paint: Paint) {
         var cacheIndex = 0
         for (i in start until end) {
             intArrayCache[cacheIndex] = startGlyph.getGlyphId(i)
             // The glyph positions are different from start to end since they are shaped
             // with different letter spacing. Use linear interpolation for positions
             // during animation.
             floatArrayCache[cacheIndex * 2] =
                     lerp(startGlyph.getGlyphX(i), endGlyph.getGlyphX(i), progress)
             floatArrayCache[cacheIndex * 2 + 1] =
                     lerp(startGlyph.getGlyphY(i), endGlyph.getGlyphY(i), progress)
             if (cacheIndex == CACHE_SIZE) {  // Cached int array is full. Flashing.
                 canvas.drawGlyphs(
                         intArrayCache, 0, // glyphID array and its starting offset
                         floatArrayCache, 0, // position array and its starting offset
                         cacheIndex, // glyph count
                         font,
                         paint
                 )
                 cacheIndex = 0
             }
             cacheIndex++
         }
         if (cacheIndex != 0) {
             canvas.drawGlyphs(
                     intArrayCache, 0, // glyphID array and its starting offset
                     floatArrayCache, 0, // position array and its starting offset
                     cacheIndex, // glyph count
                     font,
                     paint
             )
         }
     }

     // Linear Interpolator
     private fun lerp(start: Float, end: Float, t: Float) = start * (1f - t) + end * t
     private fun lerp(start: Int, end: Int, t: Float) = (start * (1f - t) + end * t).toInt()

     // The animation progress.
     var progress: Float = 0f
         set(value) {
             field = value
             invalidate()
         }

     // working copy of paint.
     private val paint = Paint()

     // Array cache for reducing allocation during drawing.
     private var intArrayCache = IntArray(CACHE_SIZE)
     private var floatArrayCache = FloatArray(CACHE_SIZE * 2)
 }