235 lines
9.8 KiB
Java
235 lines
9.8 KiB
Java
![]() |
/*
|
||
|
* 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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|