| /* |
| * 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. |
| * <pre> |
| * <code> |
| * // 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) |
| * } |
| * </code> |
| * </pre> |
| * @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() {} |
| |
| /** |
| * A 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. |
| * |
| * 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. |
| * |
| * @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(0, count), |
| false /* tabstop is not supported */, |
| null, |
| -1, -1, // ellipsis is not supported. |
| false /* fallback line spacing is not used */ |
| ); |
| tl.shape(consumer); |
| } finally { |
| TextLine.recycle(tl); |
| } |
| } |
| |
| } |