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&lt;Pair&lt;PositionedGlyphs, DrawStyle&gt;&gt;()
46  *     private val end = mutableListOf&lt;Pair&lt;PositionedGlyphs, DrawStyle&gt;&gt;()
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