1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.text; 18 19 import android.annotation.IntRange; 20 import android.annotation.NonNull; 21 import android.graphics.Paint; 22 import android.graphics.text.PositionedGlyphs; 23 import android.graphics.text.TextRunShaper; 24 25 /** 26 * Provides text shaping for multi-styled text. 27 * 28 * Here is an example of animating text size and letter spacing for simple text. 29 * <pre> 30 * <code> 31 * // In this example, shape the text once for start and end state, then animate between two shape 32 * // result without re-shaping in each frame. 33 * class SimpleAnimationView @JvmOverloads constructor( 34 * context: Context, 35 * attrs: AttributeSet? = null, 36 * defStyleAttr: Int = 0 37 * ) : View(context, attrs, defStyleAttr) { 38 * private val textDir = TextDirectionHeuristics.LOCALE 39 * private val text = "Hello, World." // The text to be displayed 40 * 41 * // Class for keeping drawing parameters. 42 * data class DrawStyle(val textSize: Float, val alpha: Int) 43 * 44 * // The start and end text shaping result. This class will animate between these two. 45 * private val start = mutableListOf<Pair<PositionedGlyphs, DrawStyle>>() 46 * private val end = mutableListOf<Pair<PositionedGlyphs, DrawStyle>>() 47 * 48 * init { 49 * val startPaint = TextPaint().apply { 50 * alpha = 0 // Alpha only affect text drawing but not text shaping 51 * textSize = 36f // TextSize affect both text shaping and drawing. 52 * letterSpacing = 0f // Letter spacing only affect text shaping but not drawing. 53 * } 54 * 55 * val endPaint = TextPaint().apply { 56 * alpha = 255 57 * textSize =128f 58 * letterSpacing = 0.1f 59 * } 60 * 61 * TextShaper.shapeText(text, 0, text.length, textDir, startPaint) { _, _, glyphs, paint -> 62 * start.add(Pair(glyphs, DrawStyle(paint.textSize, paint.alpha))) 63 * } 64 * TextShaper.shapeText(text, 0, text.length, textDir, endPaint) { _, _, glyphs, paint -> 65 * end.add(Pair(glyphs, DrawStyle(paint.textSize, paint.alpha))) 66 * } 67 * } 68 * 69 * override fun onDraw(canvas: Canvas) { 70 * super.onDraw(canvas) 71 * 72 * // Set the baseline to the vertical center of the view. 73 * canvas.translate(0f, height / 2f) 74 * 75 * // Assume the number of PositionedGlyphs are the same. If different, you may want to 76 * // animate in a different way, e.g. cross fading. 77 * start.zip(end) { (startGlyphs, startDrawStyle), (endGlyphs, endDrawStyle) -> 78 * // Tween the style and set to paint. 79 * paint.textSize = lerp(startDrawStyle.textSize, endDrawStyle.textSize, progress) 80 * paint.alpha = lerp(startDrawStyle.alpha, endDrawStyle.alpha, progress) 81 * 82 * // Assume the number of glyphs are the same. If different, you may want to animate in 83 * // a different way, e.g. cross fading. 84 * require(startGlyphs.glyphCount() == endGlyphs.glyphCount()) 85 * 86 * if (startGlyphs.glyphCount() == 0) return@zip 87 * 88 * var curFont = startGlyphs.getFont(0) 89 * var drawStart = 0 90 * for (i in 1 until startGlyphs.glyphCount()) { 91 * // Assume the pair of Glyph ID and font is the same. If different, you may want 92 * // to animate in a different way, e.g. cross fading. 93 * require(startGlyphs.getGlyphId(i) == endGlyphs.getGlyphId(i)) 94 * require(startGlyphs.getFont(i) === endGlyphs.getFont(i)) 95 * 96 * val font = startGlyphs.getFont(i) 97 * if (curFont != font) { 98 * drawGlyphs(canvas, startGlyphs, endGlyphs, drawStart, i, curFont, paint) 99 * curFont = font 100 * drawStart = i 101 * } 102 * } 103 * if (drawStart != startGlyphs.glyphCount() - 1) { 104 * drawGlyphs(canvas, startGlyphs, endGlyphs, drawStart, startGlyphs.glyphCount(), 105 * curFont, paint) 106 * } 107 * } 108 * } 109 * 110 * // Draws Glyphs for the same font run. 111 * private fun drawGlyphs(canvas: Canvas, startGlyph: PositionedGlyphs, 112 * endGlyph: PositionedGlyphs, start: Int, end: Int, font: Font, 113 * paint: Paint) { 114 * var cacheIndex = 0 115 * for (i in start until end) { 116 * intArrayCache[cacheIndex] = startGlyph.getGlyphId(i) 117 * // The glyph positions are different from start to end since they are shaped 118 * // with different letter spacing. Use linear interpolation for positions 119 * // during animation. 120 * floatArrayCache[cacheIndex * 2] = 121 * lerp(startGlyph.getGlyphX(i), endGlyph.getGlyphX(i), progress) 122 * floatArrayCache[cacheIndex * 2 + 1] = 123 * lerp(startGlyph.getGlyphY(i), endGlyph.getGlyphY(i), progress) 124 * if (cacheIndex == CACHE_SIZE) { // Cached int array is full. Flashing. 125 * canvas.drawGlyphs( 126 * intArrayCache, 0, // glyphID array and its starting offset 127 * floatArrayCache, 0, // position array and its starting offset 128 * cacheIndex, // glyph count 129 * font, 130 * paint 131 * ) 132 * cacheIndex = 0 133 * } 134 * cacheIndex++ 135 * } 136 * if (cacheIndex != 0) { 137 * canvas.drawGlyphs( 138 * intArrayCache, 0, // glyphID array and its starting offset 139 * floatArrayCache, 0, // position array and its starting offset 140 * cacheIndex, // glyph count 141 * font, 142 * paint 143 * ) 144 * } 145 * } 146 * 147 * // Linear Interpolator 148 * private fun lerp(start: Float, end: Float, t: Float) = start * (1f - t) + end * t 149 * private fun lerp(start: Int, end: Int, t: Float) = (start * (1f - t) + end * t).toInt() 150 * 151 * // The animation progress. 152 * var progress: Float = 0f 153 * set(value) { 154 * field = value 155 * invalidate() 156 * } 157 * 158 * // working copy of paint. 159 * private val paint = Paint() 160 * 161 * // Array cache for reducing allocation during drawing. 162 * private var intArrayCache = IntArray(CACHE_SIZE) 163 * private var floatArrayCache = FloatArray(CACHE_SIZE * 2) 164 * } 165 * </code> 166 * </pre> 167 * @see TextRunShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint) 168 * @see TextRunShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint) 169 * @see TextShaper#shapeText(CharSequence, int, int, TextDirectionHeuristic, TextPaint, 170 * GlyphsConsumer) 171 */ 172 public class TextShaper { TextShaper()173 private TextShaper() {} 174 175 /** 176 * A consumer interface for accepting text shape result. 177 */ 178 public interface GlyphsConsumer { 179 /** 180 * Accept text shape result. 181 * 182 * The implementation must not keep reference of paint since it will be mutated for the 183 * subsequent styles. Also, for saving heap size, keep only necessary members in the 184 * {@link TextPaint} instead of copying {@link TextPaint} object. 185 * 186 * @param start The start index of the shaped text. 187 * @param count The length of the shaped text. 188 * @param glyphs The shape result. 189 * @param paint The paint to be used for drawing. 190 */ accept( @ntRangefrom = 0) int start, @IntRange(from = 0) int count, @NonNull PositionedGlyphs glyphs, @NonNull TextPaint paint)191 void accept( 192 @IntRange(from = 0) int start, 193 @IntRange(from = 0) int count, 194 @NonNull PositionedGlyphs glyphs, 195 @NonNull TextPaint paint); 196 } 197 198 /** 199 * Shape multi-styled text. 200 * 201 * In the LTR context, the shape result will go from left to right, thus you may want to draw 202 * glyphs from left most position of the canvas. In the RTL context, the shape result will go 203 * from right to left, thus you may want to draw glyphs from right most position of the canvas. 204 * 205 * @param text a styled text. 206 * @param start a start index of shaping target in the text. 207 * @param count a length of shaping target in the text. 208 * @param dir a text direction. 209 * @param paint a paint 210 * @param consumer a consumer of the shape result. 211 */ shapeText( @onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int count, @NonNull TextDirectionHeuristic dir, @NonNull TextPaint paint, @NonNull GlyphsConsumer consumer)212 public static void shapeText( 213 @NonNull CharSequence text, @IntRange(from = 0) int start, 214 @IntRange(from = 0) int count, @NonNull TextDirectionHeuristic dir, 215 @NonNull TextPaint paint, @NonNull GlyphsConsumer consumer) { 216 MeasuredParagraph mp = MeasuredParagraph.buildForBidi( 217 text, start, start + count, dir, null); 218 TextLine tl = TextLine.obtain(); 219 try { 220 tl.set(paint, text, start, start + count, 221 mp.getParagraphDir(), 222 mp.getDirections(0, count), 223 false /* tabstop is not supported */, 224 null, 225 -1, -1, // ellipsis is not supported. 226 false /* fallback line spacing is not used */ 227 ); 228 tl.shape(consumer); 229 } finally { 230 TextLine.recycle(tl); 231 } 232 } 233 234 } 235