diff options
| author | TreeHugger Robot <treehugger-gerrit@google.com> | 2020-10-15 23:11:03 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-10-15 23:11:03 +0000 |
| commit | 7347b89fec525b31437252117bfcd37f13d26d8a (patch) | |
| tree | 0fc67ef55ac22159253d7d7105f67a7666cde242 /core/java/android | |
| parent | cde91282539bbb063ba4682826170fccc23d34b9 (diff) | |
| parent | c489d627c9babcf33ec60ba89f1ba4b982281586 (diff) | |
Merge "Update TextShaper APIs to address API council feedback"
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/text/StyledTextShaper.java | 67 | ||||
| -rw-r--r-- | core/java/android/text/TextLine.java | 50 | ||||
| -rw-r--r-- | core/java/android/text/TextShaper.java | 229 |
3 files changed, 254 insertions, 92 deletions
diff --git a/core/java/android/text/StyledTextShaper.java b/core/java/android/text/StyledTextShaper.java deleted file mode 100644 index bf906143bc56..000000000000 --- a/core/java/android/text/StyledTextShaper.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.NonNull; -import android.graphics.Paint; -import android.graphics.text.PositionedGlyphs; -import android.graphics.text.TextShaper; - -import java.util.List; - -/** - * Provides text shaping for multi-styled text. - * - * @see TextShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint) - * @see TextShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint) - * @see StyledTextShaper#shapeText(CharSequence, int, int, TextDirectionHeuristic, TextPaint) - */ -public class StyledTextShaper { - private StyledTextShaper() {} - - - /** - * 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 - * @return a shape result. - */ - public static @NonNull List<PositionedGlyphs> shapeText( - @NonNull CharSequence text, int start, int count, - @NonNull TextDirectionHeuristic dir, @NonNull TextPaint paint) { - 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. - ); - return tl.shape(); - } finally { - TextLine.recycle(tl); - } - } - -} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index b82683260985..4471056e23dd 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -24,7 +24,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.graphics.text.PositionedGlyphs; -import android.graphics.text.TextShaper; +import android.graphics.text.TextRunShaper; import android.os.Build; import android.text.Layout.Directions; import android.text.Layout.TabStops; @@ -37,7 +37,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; -import java.util.List; /** * Represents a line of styled text, for measuring in visual order and @@ -312,8 +311,7 @@ public class TextLine { /** * Shape the TextLine. */ - List<PositionedGlyphs> shape() { - List<PositionedGlyphs> glyphs = new ArrayList<>(); + void shape(TextShaper.GlyphsConsumer consumer) { float horizontal = 0; float x = 0; final int runCount = mDirections.getRunCount(); @@ -326,7 +324,7 @@ public class TextLine { int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { - horizontal += shapeRun(glyphs, segStart, j, runIsRtl, x + horizontal, + horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal, runIndex != (runCount - 1) || j != mLen); if (j != runLimit) { // charAt(j) == TAB_CHAR @@ -336,7 +334,6 @@ public class TextLine { } } } - return glyphs; } /** @@ -546,7 +543,7 @@ public class TextLine { /** * Shape a unidirectional (but possibly multi-styled) run of text. * - * @param glyphs the output positioned glyphs list + * @param consumer the consumer of the shape result * @param start the line-relative start * @param limit the line-relative limit * @param runIsRtl true if the run is right-to-left @@ -555,16 +552,17 @@ public class TextLine { * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ - private float shapeRun(List<PositionedGlyphs> glyphs, int start, + private float shapeRun(TextShaper.GlyphsConsumer consumer, int start, int limit, boolean runIsRtl, float x, boolean needWidth) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { float w = -measureRun(start, limit, limit, runIsRtl, null); - handleRun(start, limit, limit, runIsRtl, null, glyphs, x + w, 0, 0, 0, null, false); + handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, false); return w; } - return handleRun(start, limit, limit, runIsRtl, null, glyphs, x, 0, 0, 0, null, needWidth); + return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, + needWidth); } @@ -899,7 +897,7 @@ public class TextLine { * @param end the end of the text * @param runIsRtl true if the run is right-to-left * @param c the canvas, can be null if rendering is not needed - * @param glyphs the output positioned glyph list, can be null if not necessary + * @param consumer the output positioned glyph list, can be null if not necessary * @param x the edge of the run closest to the leading margin * @param top the top of the line * @param y the baseline @@ -913,7 +911,7 @@ public class TextLine { */ private float handleText(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, - Canvas c, List<PositionedGlyphs> glyphs, float x, int top, int y, int bottom, + Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations) { @@ -946,8 +944,8 @@ public class TextLine { rightX = x + totalWidth; } - if (glyphs != null) { - shapeTextRun(glyphs, wp, start, end, contextStart, contextEnd, runIsRtl, leftX); + if (consumer != null) { + shapeTextRun(consumer, wp, start, end, contextStart, contextEnd, runIsRtl, leftX); } if (c != null) { @@ -1135,7 +1133,7 @@ public class TextLine { * @param limit the limit of the run * @param runIsRtl true if the run is right-to-left * @param c the canvas, can be null - * @param glyphs the output positioned glyphs, can be null + * @param consumer the output positioned glyphs, can be null * @param x the end of the run closest to the leading margin * @param top the top of the line * @param y the baseline @@ -1147,7 +1145,7 @@ public class TextLine { */ private float handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, - List<PositionedGlyphs> glyphs, float x, int top, int y, + TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth) { if (measureLimit < start || measureLimit > limit) { @@ -1180,7 +1178,7 @@ public class TextLine { wp.set(mPaint); wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit())); wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); - return handleText(wp, start, limit, start, limit, runIsRtl, c, glyphs, x, top, + return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top, y, bottom, fmi, needWidth, measureLimit, null); } @@ -1262,7 +1260,7 @@ public class TextLine { activePaint.setEndHyphenEdit( adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, - glyphs, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit, + consumer, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations); activeStart = j; @@ -1288,7 +1286,7 @@ public class TextLine { adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); activePaint.setEndHyphenEdit( adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); - x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, glyphs, x, + x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations); } @@ -1327,7 +1325,7 @@ public class TextLine { /** * Shape a text run with the set-up paint. * - * @param glyphs the output positioned glyphs list + * @param consumer the output positioned glyphs list * @param paint the paint used to render the text * @param start the start of the run * @param end the end of the run @@ -1336,30 +1334,32 @@ public class TextLine { * @param runIsRtl true if the run is right-to-left * @param x the x position of the left edge of the run */ - private void shapeTextRun(List<PositionedGlyphs> glyphs, TextPaint paint, + private void shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) { int count = end - start; int contextCount = contextEnd - contextStart; + PositionedGlyphs glyphs; if (mCharsValid) { - glyphs.add(TextShaper.shapeTextRun( + glyphs = TextRunShaper.shapeTextRun( mChars, start, count, contextStart, contextCount, x, 0f, runIsRtl, paint - )); + ); } else { - glyphs.add(TextShaper.shapeTextRun( + glyphs = TextRunShaper.shapeTextRun( mText, mStart + start, count, mStart + contextStart, contextCount, x, 0f, runIsRtl, paint - )); + ); } + consumer.accept(start, count, glyphs, paint); } 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. + * <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() {} + + /** + * 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); + } + } + +} |
