TextShaper


public class TextShaper
extends Object

java.lang.Object
   ↳ android.text.TextShaper


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

Summary

Nested classes

interface TextShaper.GlyphsConsumer

A consumer interface for accepting text shape result. 

Public methods

static void shapeText(CharSequence text, int start, int count, TextDirectionHeuristic dir, TextPaint paint, TextShaper.GlyphsConsumer consumer)

Shape multi-styled text.

Inherited methods

Public methods

shapeText

Added in API level 31
public static void shapeText (CharSequence text, 
                int start, 
                int count, 
                TextDirectionHeuristic dir, 
                TextPaint paint, 
                TextShaper.GlyphsConsumer consumer)

Shape multi-styled text. In the LTR context, the shape result will go from left to right, thus you may want to draw glyphs from left most position of the canvas. In the RTL context, the shape result will go from right to left, thus you may want to draw glyphs from right most position of the canvas.

Parameters
text CharSequence: a styled text. This value cannot be null.

start int: a start index of shaping target in the text. Value is 0 or greater

count int: a length of shaping target in the text. Value is 0 or greater

dir TextDirectionHeuristic: a text direction. This value cannot be null.

paint TextPaint: a paint This value cannot be null.

consumer TextShaper.GlyphsConsumer: a consumer of the shape result. This value cannot be null.