From c489d627c9babcf33ec60ba89f1ba4b982281586 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Wed, 7 Oct 2020 18:22:34 -0700 Subject: Update TextShaper APIs to address API council feedback This CL contains followings: - Rename TextShaper to TextRunShaper, StyledTextShaper to TextShaper - Renamed getTotalAdvance to getAdvance - Rename getStyle to getGlyphStyle - Rename getOriginX/Y to getOffsetX/Y - Rename getPositionX/Y to getGlyphX/Y - Fixed some documentation errors. - Remvoed GlyphStyle. Added GlyphConsumer instead. Bug: 170255480 Test: atest TextShaperRunTest GlyphStyleTest TextShaperTest Change-Id: I0ffd7a3374e9cd1e04872240c2d0da26bc530244 --- core/java/android/text/TextShaper.java | 229 +++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 core/java/android/text/TextShaper.java (limited to 'core/java/android/text/TextShaper.java') diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java new file mode 100644 index 000000000000..dd2570401b3e --- /dev/null +++ b/core/java/android/text/TextShaper.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.graphics.Paint; +import android.graphics.text.PositionedGlyphs; +import android.graphics.text.TextRunShaper; + +/** + * 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)
+ * }
+ * 
+ * 
+ * @see TextRunShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint) + * @see TextRunShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint) + * @see TextShaper#shapeText(CharSequence, int, int, TextDirectionHeuristic, TextPaint, + * GlyphsConsumer) + */ +public class TextShaper { + private TextShaper() {} + + /** + * An consumer interface for accepting text shape result. + */ + public interface GlyphsConsumer { + /** + * Accept text shape result. + * + * The implementation must not keep reference of paint since it will be mutated for the + * subsequent styles. Also, for saving heap size, keep only necessary members in the + * {@link TextPaint} instead of copying {@link TextPaint} object. + * + * @param start The start index of the shaped text. + * @param count The length of the shaped text. + * @param glyphs The shape result. + * @param paint The paint to be used for drawing. + */ + void accept( + @IntRange(from = 0) int start, + @IntRange(from = 0) int count, + @NonNull PositionedGlyphs glyphs, + @NonNull TextPaint paint); + } + + /** + * Shape multi-styled text. + * + * @param text a styled text. + * @param start a start index of shaping target in the text. + * @param count a length of shaping target in the text. + * @param dir a text direction. + * @param paint a paint + * @param consumer a consumer of the shape result. + */ + public static void shapeText( + @NonNull CharSequence text, @IntRange(from = 0) int start, + @IntRange(from = 0) int count, @NonNull TextDirectionHeuristic dir, + @NonNull TextPaint paint, @NonNull GlyphsConsumer consumer) { + MeasuredParagraph mp = MeasuredParagraph.buildForBidi( + text, start, start + count, dir, null); + TextLine tl = TextLine.obtain(); + try { + tl.set(paint, text, start, start + count, + mp.getParagraphDir(), + mp.getDirections(start, start + count), + false /* tabstop is not supported */, + null, + -1, -1 // ellipsis is not supported. + ); + tl.shape(consumer); + } finally { + TextLine.recycle(tl); + } + } + +} -- cgit v1.2.3