diff options
16 files changed, 816 insertions, 639 deletions
diff --git a/apct-tests/perftests/core/src/android/text/MeasuredTextMemoryUsageTest.java b/apct-tests/perftests/core/src/android/text/PrecomputedTextMemoryUsageTest.java index fc6302ea9394..73e17242ae78 100644 --- a/apct-tests/perftests/core/src/android/text/MeasuredTextMemoryUsageTest.java +++ b/apct-tests/perftests/core/src/android/text/PrecomputedTextMemoryUsageTest.java @@ -45,7 +45,7 @@ import java.util.Random; @LargeTest @RunWith(AndroidJUnit4.class) -public class MeasuredTextMemoryUsageTest { +public class PrecomputedTextMemoryUsageTest { private static final int WORD_LENGTH = 9; // Random word has 9 characters. private static final boolean NO_STYLE_TEXT = false; @@ -53,7 +53,7 @@ public class MeasuredTextMemoryUsageTest { private static int TRIAL_COUNT = 100; - public MeasuredTextMemoryUsageTest() {} + public PrecomputedTextMemoryUsageTest() {} private TextPerfUtils mTextUtil = new TextPerfUtils(); @@ -77,13 +77,16 @@ public class MeasuredTextMemoryUsageTest { @Test public void testMemoryUsage_NoHyphenation() { int[] memories = new int[TRIAL_COUNT]; - // Report median of randomly generated MeasuredText. - for (int i = 0; i < TRIAL_COUNT; ++i) { - memories[i] = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build().getMemoryUsage(); + .build(); + + // Report median of randomly generated PrecomputedText. + for (int i = 0; i < TRIAL_COUNT; ++i) { + memories[i] = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), param) + .getMemoryUsage(); } reportMemoryUsage(median(memories), "MemoryUsage_NoHyphenation"); } @@ -91,13 +94,16 @@ public class MeasuredTextMemoryUsageTest { @Test public void testMemoryUsage_Hyphenation() { int[] memories = new int[TRIAL_COUNT]; - // Report median of randomly generated MeasuredText. - for (int i = 0; i < TRIAL_COUNT; ++i) { - memories[i] = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build().getMemoryUsage(); + .build(); + + // Report median of randomly generated PrecomputedText. + for (int i = 0; i < TRIAL_COUNT; ++i) { + memories[i] = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), param) + .getMemoryUsage(); } reportMemoryUsage(median(memories), "MemoryUsage_Hyphenation"); } @@ -105,13 +111,16 @@ public class MeasuredTextMemoryUsageTest { @Test public void testMemoryUsage_NoHyphenation_WidthOnly() { int[] memories = new int[TRIAL_COUNT]; - // Report median of randomly generated MeasuredText. - for (int i = 0; i < TRIAL_COUNT; ++i) { - memories[i] = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(false /* width only */).getMemoryUsage(); + .build(); + + // Report median of randomly generated PrecomputedText. + for (int i = 0; i < TRIAL_COUNT; ++i) { + CharSequence cs = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + memories[i] = PrecomputedText.createWidthOnly(cs, param, 0, cs.length()) + .getMemoryUsage(); } reportMemoryUsage(median(memories), "MemoryUsage_NoHyphenation_WidthOnly"); } @@ -119,13 +128,16 @@ public class MeasuredTextMemoryUsageTest { @Test public void testMemoryUsage_Hyphenatation_WidthOnly() { int[] memories = new int[TRIAL_COUNT]; - // Report median of randomly generated MeasuredText. - for (int i = 0; i < TRIAL_COUNT; ++i) { - memories[i] = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(false /* width only */).getMemoryUsage(); + .build(); + + // Report median of randomly generated PrecomputedText. + for (int i = 0; i < TRIAL_COUNT; ++i) { + CharSequence cs = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + memories[i] = PrecomputedText.createWidthOnly(cs, param, 0, cs.length()) + .getMemoryUsage(); } reportMemoryUsage(median(memories), "MemoryUsage_Hyphenation_WidthOnly"); } diff --git a/apct-tests/perftests/core/src/android/text/MeasuredTextPerfTest.java b/apct-tests/perftests/core/src/android/text/PrecomputedTextPerfTest.java index 98f2bd5e5736..1cd0ae13069b 100644 --- a/apct-tests/perftests/core/src/android/text/MeasuredTextPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/PrecomputedTextPerfTest.java @@ -42,7 +42,7 @@ import java.util.Random; @LargeTest @RunWith(AndroidJUnit4.class) -public class MeasuredTextPerfTest { +public class PrecomputedTextPerfTest { private static final int WORD_LENGTH = 9; // Random word has 9 characters. private static final int WORDS_IN_LINE = 8; // Roughly, 8 words in a line. private static final boolean NO_STYLE_TEXT = false; @@ -51,7 +51,7 @@ public class MeasuredTextPerfTest { private static TextPaint PAINT = new TextPaint(); private static final int TEXT_WIDTH = WORDS_IN_LINE * WORD_LENGTH * (int) PAINT.getTextSize(); - public MeasuredTextPerfTest() {} + public PrecomputedTextPerfTest() {} @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -66,120 +66,136 @@ public class MeasuredTextPerfTest { @Test public void testCreate_NoStyled_Hyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(true /* do full layout */); + PrecomputedText.create(text, param); } } @Test public void testCreate_NoStyled_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(true /* do full layout */); + PrecomputedText.create(text, param); } } @Test public void testCreate_NoStyled_Hyphenation_WidthOnly() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(false /* width only */); + PrecomputedText.create(text, param); } } @Test public void testCreate_NoStyled_NoHyphenation_WidthOnly() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(false /* width only */); + PrecomputedText.create(text, param); } } @Test public void testCreate_Styled_Hyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(true /* do full layout */); + PrecomputedText.create(text, param); } } @Test public void testCreate_Styled_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(true /* do full layout */); + PrecomputedText.create(text, param); } } @Test public void testCreate_Styled_Hyphenation_WidthOnly() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(false /* width only */); + PrecomputedText.create(text, param); } } @Test public void testCreate_Styled_NoHyphenation_WidthOnly() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final PrecomputedText.Params param = new PrecomputedText.Params.Builder(PAINT) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .build(); + while (state.keepRunning()) { state.pauseTiming(); final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT); state.resumeTiming(); - new MeasuredText.Builder(text, PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(false /* width only */); + PrecomputedText.create(text, param); } } } diff --git a/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java index 231aaf2ca074..8823af1df350 100644 --- a/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java @@ -63,6 +63,18 @@ public class StaticLayoutPerfTest { mTextUtil.resetRandom(0 /* seed */); } + private PrecomputedText makeMeasured(CharSequence text, TextPaint paint) { + PrecomputedText.Params param = new PrecomputedText.Params.Builder(paint).build(); + return PrecomputedText.create(text, param); + } + + private PrecomputedText makeMeasured(CharSequence text, TextPaint paint, int strategy, + int frequency) { + PrecomputedText.Params param = new PrecomputedText.Params.Builder(paint) + .setHyphenationFrequency(frequency).setBreakStrategy(strategy).build(); + return PrecomputedText.create(text, param); + } + @Test public void testCreate_FixedText_NoStyle_Greedy_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); @@ -151,18 +163,16 @@ public class StaticLayoutPerfTest { } @Test - public void testCreate_MeasuredText_NoStyled_Greedy_NoHyphenation() { + public void testCreate_PrecomputedText_NoStyled_Greedy_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, + Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE); state.resumeTiming(); - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + StaticLayout.Builder.obtain(text, 0, text.getText().length(), PAINT, TEXT_WIDTH) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) .build(); @@ -170,18 +180,16 @@ public class StaticLayoutPerfTest { } @Test - public void testCreate_MeasuredText_NoStyled_Greedy_Hyphenation() { + public void testCreate_PrecomputedText_NoStyled_Greedy_Hyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, + Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NORMAL); state.resumeTiming(); - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + StaticLayout.Builder.obtain(text, 0, text.getText().length(), PAINT, TEXT_WIDTH) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) .build(); @@ -189,18 +197,16 @@ public class StaticLayoutPerfTest { } @Test - public void testCreate_MeasuredText_NoStyled_Balanced_NoHyphenation() { + public void testCreate_PrecomputedText_NoStyled_Balanced_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, + Layout.BREAK_STRATEGY_BALANCED, Layout.HYPHENATION_FREQUENCY_NONE); state.resumeTiming(); - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + StaticLayout.Builder.obtain(text, 0, text.getText().length(), PAINT, TEXT_WIDTH) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .build(); @@ -208,18 +214,16 @@ public class StaticLayoutPerfTest { } @Test - public void testCreate_MeasuredText_NoStyled_Balanced_Hyphenation() { + public void testCreate_PrecomputedText_NoStyled_Balanced_Hyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, + Layout.BREAK_STRATEGY_BALANCED, Layout.HYPHENATION_FREQUENCY_NORMAL); state.resumeTiming(); - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + StaticLayout.Builder.obtain(text, 0, text.getText().length(), PAINT, TEXT_WIDTH) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .build(); @@ -227,18 +231,16 @@ public class StaticLayoutPerfTest { } @Test - public void testCreate_MeasuredText_Styled_Greedy_NoHyphenation() { + public void testCreate_PrecomputedText_Styled_Greedy_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT) - .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) - .build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, + Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE); state.resumeTiming(); - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + StaticLayout.Builder.obtain(text, 0, text.getText().length(), PAINT, TEXT_WIDTH) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) .build(); @@ -328,15 +330,16 @@ public class StaticLayoutPerfTest { } @Test - public void testDraw_MeasuredText_Styled() { + public void testDraw_PrecomputedText_Styled() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final RenderNode node = RenderNode.create("benchmark", null); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT); final StaticLayout layout = - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build(); + StaticLayout.Builder.obtain( + text, 0, text.getText().length(), PAINT, TEXT_WIDTH).build(); final DisplayListCanvas c = node.start(1200, 200); state.resumeTiming(); @@ -345,15 +348,16 @@ public class StaticLayoutPerfTest { } @Test - public void testDraw_MeasuredText_NoStyled() { + public void testDraw_PrecomputedText_NoStyled() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final RenderNode node = RenderNode.create("benchmark", null); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT); final StaticLayout layout = - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build(); + StaticLayout.Builder.obtain( + text, 0, text.getText().length(), PAINT, TEXT_WIDTH).build(); final DisplayListCanvas c = node.start(1200, 200); state.resumeTiming(); @@ -362,15 +366,16 @@ public class StaticLayoutPerfTest { } @Test - public void testDraw_MeasuredText_Styled_WithoutCache() { + public void testDraw_PrecomputedText_Styled_WithoutCache() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final RenderNode node = RenderNode.create("benchmark", null); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT); final StaticLayout layout = - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build(); + StaticLayout.Builder.obtain( + text, 0, text.getText().length(), PAINT, TEXT_WIDTH).build(); final DisplayListCanvas c = node.start(1200, 200); Canvas.freeTextLayoutCaches(); state.resumeTiming(); @@ -380,15 +385,16 @@ public class StaticLayoutPerfTest { } @Test - public void testDraw_MeasuredText_NoStyled_WithoutCache() { + public void testDraw_PrecomputedText_NoStyled_WithoutCache() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final RenderNode node = RenderNode.create("benchmark", null); while (state.keepRunning()) { state.pauseTiming(); - final MeasuredText text = new MeasuredText.Builder( - mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build(); + final PrecomputedText text = makeMeasured( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT); final StaticLayout layout = - StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build(); + StaticLayout.Builder.obtain( + text, 0, text.getText().length(), PAINT, TEXT_WIDTH).build(); final DisplayListCanvas c = node.start(1200, 200); Canvas.freeTextLayoutCaches(); state.resumeTiming(); diff --git a/api/current.txt b/api/current.txt index 54f0e15926a0..543d71c53bbd 100644 --- a/api/current.txt +++ b/api/current.txt @@ -13832,6 +13832,7 @@ package android.graphics { method public int breakText(java.lang.String, boolean, float, float[]); method public void clearShadowLayer(); method public float descent(); + method public boolean equalsForTextMeasurement(android.graphics.Paint); method public int getAlpha(); method public int getColor(); method public android.graphics.ColorFilter getColorFilter(); @@ -43182,36 +43183,6 @@ package android.text { method public boolean isAllowed(char); } - public class MeasuredText implements android.text.Spanned { - method public char charAt(int); - method public int getBreakStrategy(); - method public int getEnd(); - method public int getHyphenationFrequency(); - method public android.text.TextPaint getPaint(); - method public int getParagraphCount(); - method public int getParagraphEnd(int); - method public int getParagraphStart(int); - method public int getSpanEnd(java.lang.Object); - method public int getSpanFlags(java.lang.Object); - method public int getSpanStart(java.lang.Object); - method public <T> T[] getSpans(int, int, java.lang.Class<T>); - method public int getStart(); - method public java.lang.CharSequence getText(); - method public android.text.TextDirectionHeuristic getTextDir(); - method public int length(); - method public int nextSpanTransition(int, int, java.lang.Class); - method public java.lang.CharSequence subSequence(int, int); - } - - public static final class MeasuredText.Builder { - ctor public MeasuredText.Builder(java.lang.CharSequence, android.text.TextPaint); - method public android.text.MeasuredText build(); - method public android.text.MeasuredText.Builder setBreakStrategy(int); - method public android.text.MeasuredText.Builder setHyphenationFrequency(int); - method public android.text.MeasuredText.Builder setRange(int, int); - method public android.text.MeasuredText.Builder setTextDirection(android.text.TextDirectionHeuristic); - } - public abstract interface NoCopySpan { } @@ -43223,6 +43194,31 @@ package android.text { method public abstract int getSpanTypeId(); } + public class PrecomputedText { + method public static android.text.PrecomputedText create(java.lang.CharSequence, android.text.PrecomputedText.Params); + method public int getParagraphCount(); + method public int getParagraphEnd(int); + method public int getParagraphStart(int); + method public android.text.PrecomputedText.Params getParams(); + method public java.lang.CharSequence getText(); + } + + public static class PrecomputedText.Params { + method public int getBreakStrategy(); + method public int getHyphenationFrequency(); + method public android.text.TextDirectionHeuristic getTextDirection(); + method public android.text.TextPaint getTextPaint(); + method public boolean sameTextMetrics(android.text.PrecomputedText.Params); + } + + public static class PrecomputedText.Params.Builder { + ctor public PrecomputedText.Params.Builder(android.text.TextPaint); + method public android.text.PrecomputedText.Params build(); + method public android.text.PrecomputedText.Params.Builder setBreakStrategy(int); + method public android.text.PrecomputedText.Params.Builder setHyphenationFrequency(int); + method public android.text.PrecomputedText.Params.Builder setTextDirection(android.text.TextDirectionHeuristic); + } + public class Selection { method public static boolean extendDown(android.text.Spannable, android.text.Layout); method public static boolean extendLeft(android.text.Spannable, android.text.Layout); @@ -43354,6 +43350,7 @@ package android.text { public static final class StaticLayout.Builder { method public android.text.StaticLayout build(); + method public static android.text.StaticLayout.Builder obtain(android.text.PrecomputedText, int, int, android.text.TextPaint, int); method public static android.text.StaticLayout.Builder obtain(java.lang.CharSequence, int, int, android.text.TextPaint, int); method public android.text.StaticLayout.Builder setAlignment(android.text.Layout.Alignment); method public android.text.StaticLayout.Builder setBreakStrategy(int); @@ -53604,6 +53601,7 @@ package android.widget { method public final android.content.res.ColorStateList getTextColors(); method public java.util.Locale getTextLocale(); method public android.os.LocaleList getTextLocales(); + method public android.text.PrecomputedText.Params getTextMetricsParams(); method public float getTextScaleX(); method public float getTextSize(); method public int getTotalPaddingBottom(); @@ -53709,6 +53707,8 @@ package android.widget { method public final void setMovementMethod(android.text.method.MovementMethod); method public void setOnEditorActionListener(android.widget.TextView.OnEditorActionListener); method public void setPaintFlags(int); + method public void setPrecomputedTextAndParams(android.text.PrecomputedText); + method public void setPrecomputedTextOrThrow(android.text.PrecomputedText); method public void setPrivateImeOptions(java.lang.String); method public void setRawInputType(int); method public void setScroller(android.widget.Scroller); @@ -53733,6 +53733,7 @@ package android.widget { method public final void setTextKeepState(java.lang.CharSequence, android.widget.TextView.BufferType); method public void setTextLocale(java.util.Locale); method public void setTextLocales(android.os.LocaleList); + method public void setTextMetricsParams(android.text.PrecomputedText.Params); method public void setTextScaleX(float); method public void setTextSize(float); method public void setTextSize(int, float); diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 6fa5312be5cc..dbe415773899 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -322,6 +322,12 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback */ public static Metrics isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics) { + return isBoring(text, null /* precomputed */, paint, textDir, metrics); + } + + /** @hide */ + public static Metrics isBoring(CharSequence text, PrecomputedText precomputed, TextPaint paint, + TextDirectionHeuristic textDir, Metrics metrics) { final int textLength = text.length(); if (hasAnyInterestingChars(text, textLength)) { return null; // There are some interesting characters. Not boring. @@ -344,18 +350,17 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback fm.reset(); } - TextLine line = TextLine.obtain(); - line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, - Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; + if (precomputed != null) { // Reaching here means there is only one paragraph. - MeasuredParagraph mp = mt.getMeasuredParagraph(0); + MeasuredParagraph mp = precomputed.getMeasuredParagraph(0); fm.width = (int) Math.ceil(mp.getWidth(0, mp.getTextLength())); } else { + TextLine line = TextLine.obtain(); + line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); fm.width = (int) Math.ceil(line.metrics(fm)); + TextLine.recycle(line); } - TextLine.recycle(line); return fm; } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 18431cacbfaf..10444f045d6f 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -363,7 +363,7 @@ public class DynamicLayout extends Layout { @JustificationMode int justificationMode, @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth) { - super(createEllipsizer(ellipsize, display), + super(createEllipsizer(ellipsize, display), null /* precomputed */, paint, width, align, textDir, spacingmult, spacingadd); final Builder b = Builder.obtain(base, paint, width) diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index aa97b2aba749..d5d35904031c 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -245,6 +245,13 @@ public abstract class Layout { protected Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd) { + this(text, null /* precomputed */, paint, width, align, textDir, spacingMult, spacingAdd); + } + + /** @hide */ + protected Layout(CharSequence text, PrecomputedText precomputed, TextPaint paint, + int width, Alignment align, TextDirectionHeuristic textDir, + float spacingMult, float spacingAdd) { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); @@ -259,6 +266,7 @@ public abstract class Layout { } mText = text; + mPrecomputed = precomputed; mPaint = paint; mWidth = width; mAlignment = align; @@ -562,7 +570,7 @@ public abstract class Layout { // XXX: assumes there's nothing additional to be done canvas.drawText(buf, start, end, x, lbaseline, paint); } else { - tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops); + tl.set(paint, buf, mPrecomputed, start, end, dir, directions, hasTab, tabStops); if (justify) { tl.justify(right - left - indentWidth); } @@ -2264,6 +2272,7 @@ public abstract class Layout { } private CharSequence mText; + private PrecomputedText mPrecomputed; private TextPaint mPaint; private TextPaint mWorkPaint = new TextPaint(); private int mWidth; diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java deleted file mode 100644 index bb7a9e0b7906..000000000000 --- a/core/java/android/text/MeasuredText.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright (C) 2017 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.util.IntArray; - -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.Preconditions; - -import java.util.ArrayList; - -/** - * A text which has already been measured. - */ -public class MeasuredText implements Spanned { - private static final char LINE_FEED = '\n'; - - // The original text. - private final @NonNull CharSequence mText; - - // The inclusive start offset of the measuring target. - private final @IntRange(from = 0) int mStart; - - // The exclusive end offset of the measuring target. - private final @IntRange(from = 0) int mEnd; - - // The TextPaint used for measurement. - private final @NonNull TextPaint mPaint; - - // The requested text direction. - private final @NonNull TextDirectionHeuristic mTextDir; - - // The measured paragraph texts. - private final @NonNull MeasuredParagraph[] mMeasuredParagraphs; - - // The sorted paragraph end offsets. - private final @NonNull int[] mParagraphBreakPoints; - - // The break strategy for this measured text. - private final @Layout.BreakStrategy int mBreakStrategy; - - // The hyphenation frequency for this measured text. - private final @Layout.HyphenationFrequency int mHyphenationFrequency; - - /** - * A Builder for MeasuredText - */ - public static final class Builder { - // Mandatory parameters. - private final @NonNull CharSequence mText; - private final @NonNull TextPaint mPaint; - - // Members to be updated by setters. - private @IntRange(from = 0) int mStart; - private @IntRange(from = 0) int mEnd; - private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; - private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; - private @Layout.HyphenationFrequency int mHyphenationFrequency = - Layout.HYPHENATION_FREQUENCY_NORMAL; - - - /** - * Builder constructor - * - * @param text The text to be measured. - * @param paint The paint to be used for drawing. - */ - public Builder(@NonNull CharSequence text, @NonNull TextPaint paint) { - Preconditions.checkNotNull(text); - Preconditions.checkNotNull(paint); - - mText = text; - mPaint = paint; - mStart = 0; - mEnd = text.length(); - } - - /** - * Set the range of measuring target. - * - * @param start The measuring target start offset in the text. - * @param end The measuring target end offset in the text. - */ - public @NonNull Builder setRange(@IntRange(from = 0) int start, - @IntRange(from = 0) int end) { - Preconditions.checkArgumentInRange(start, 0, mText.length(), "start"); - Preconditions.checkArgumentInRange(end, 0, mText.length(), "end"); - Preconditions.checkArgument(start <= end, "The range is reversed."); - - mStart = start; - mEnd = end; - return this; - } - - /** - * Set the text direction heuristic - * - * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. - * - * @param textDir The text direction heuristic for resolving bidi behavior. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { - Preconditions.checkNotNull(textDir); - mTextDir = textDir; - return this; - } - - /** - * Set the break strategy - * - * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. - * - * @param breakStrategy The break strategy. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setBreakStrategy(@Layout.BreakStrategy int breakStrategy) { - mBreakStrategy = breakStrategy; - return this; - } - - /** - * Set the hyphenation frequency - * - * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. - * - * @param hyphenationFrequency The hyphenation frequency. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setHyphenationFrequency( - @Layout.HyphenationFrequency int hyphenationFrequency) { - mHyphenationFrequency = hyphenationFrequency; - return this; - } - - /** - * Build the measured text - * - * @return the measured text. - */ - public @NonNull MeasuredText build() { - return build(true /* build full layout result */); - } - - /** @hide */ - public @NonNull MeasuredText build(boolean computeLayout) { - final boolean needHyphenation = mBreakStrategy != Layout.BREAK_STRATEGY_SIMPLE - && mHyphenationFrequency != Layout.HYPHENATION_FREQUENCY_NONE; - - final IntArray paragraphEnds = new IntArray(); - final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>(); - - int paraEnd = 0; - for (int paraStart = mStart; paraStart < mEnd; paraStart = paraEnd) { - paraEnd = TextUtils.indexOf(mText, LINE_FEED, paraStart, mEnd); - if (paraEnd < 0) { - // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph - // end. - paraEnd = mEnd; - } else { - paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. - } - - paragraphEnds.add(paraEnd); - measuredTexts.add(MeasuredParagraph.buildForStaticLayout( - mPaint, mText, paraStart, paraEnd, mTextDir, needHyphenation, - computeLayout, null /* no recycle */)); - } - - return new MeasuredText(mText, mStart, mEnd, mPaint, mTextDir, mBreakStrategy, - mHyphenationFrequency, measuredTexts.toArray( - new MeasuredParagraph[measuredTexts.size()]), - paragraphEnds.toArray()); - } - }; - - // Use MeasuredText.Builder instead. - private MeasuredText(@NonNull CharSequence text, - @IntRange(from = 0) int start, - @IntRange(from = 0) int end, - @NonNull TextPaint paint, - @NonNull TextDirectionHeuristic textDir, - @Layout.BreakStrategy int breakStrategy, - @Layout.HyphenationFrequency int frequency, - @NonNull MeasuredParagraph[] measuredTexts, - @NonNull int[] paragraphBreakPoints) { - mText = text; - mStart = start; - mEnd = end; - // Copy the paint so that we can keep the reference of typeface in native layout result. - mPaint = new TextPaint(paint); - mMeasuredParagraphs = measuredTexts; - mParagraphBreakPoints = paragraphBreakPoints; - mTextDir = textDir; - mBreakStrategy = breakStrategy; - mHyphenationFrequency = frequency; - } - - /** - * Return the underlying text. - */ - public @NonNull CharSequence getText() { - return mText; - } - - /** - * Returns the inclusive start offset of measured region. - */ - public @IntRange(from = 0) int getStart() { - return mStart; - } - - /** - * Returns the exclusive end offset of measured region. - */ - public @IntRange(from = 0) int getEnd() { - return mEnd; - } - - /** - * Returns the text direction associated with char sequence. - */ - public @NonNull TextDirectionHeuristic getTextDir() { - return mTextDir; - } - - /** - * Returns the paint used to measure this text. - */ - public @NonNull TextPaint getPaint() { - return mPaint; - } - - /** - * Returns the length of the paragraph of this text. - */ - public @IntRange(from = 0) int getParagraphCount() { - return mParagraphBreakPoints.length; - } - - /** - * Returns the paragraph start offset of the text. - */ - public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { - Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); - return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1]; - } - - /** - * Returns the paragraph end offset of the text. - */ - public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { - Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); - return mParagraphBreakPoints[paraIndex]; - } - - /** @hide */ - public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { - return mMeasuredParagraphs[paraIndex]; - } - - /** - * Returns the break strategy for this text. - */ - public @Layout.BreakStrategy int getBreakStrategy() { - return mBreakStrategy; - } - - /** - * Returns the hyphenation frequency for this text. - */ - public @Layout.HyphenationFrequency int getHyphenationFrequency() { - return mHyphenationFrequency; - } - - /** - * Returns true if the given TextPaint gives the same result of text layout for this text. - * @hide - */ - public boolean canUseMeasuredResult(@NonNull TextPaint paint) { - return mPaint.getTextSize() == paint.getTextSize() - && mPaint.getTextSkewX() == paint.getTextSkewX() - && mPaint.getTextScaleX() == paint.getTextScaleX() - && mPaint.getLetterSpacing() == paint.getLetterSpacing() - && mPaint.getWordSpacing() == paint.getWordSpacing() - && mPaint.getFlags() == paint.getFlags() // Maybe not all flag affects text layout. - && mPaint.getTextLocales() == paint.getTextLocales() // need to be equals? - && mPaint.getFontVariationSettings() == paint.getFontVariationSettings() - && mPaint.getTypeface() == paint.getTypeface() - && TextUtils.equals(mPaint.getFontFeatureSettings(), paint.getFontFeatureSettings()); - } - - /** @hide */ - public int findParaIndex(@IntRange(from = 0) int pos) { - // TODO: Maybe good to remove paragraph concept from MeasuredText and add substring layout - // support to StaticLayout. - for (int i = 0; i < mParagraphBreakPoints.length; ++i) { - if (pos < mParagraphBreakPoints[i]) { - return i; - } - } - throw new IndexOutOfBoundsException( - "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1] - + ", gave " + pos); - } - - /** @hide */ - public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { - final int paraIndex = findParaIndex(start); - final int paraStart = getParagraphStart(paraIndex); - final int paraEnd = getParagraphEnd(paraIndex); - if (start < paraStart || paraEnd < end) { - throw new RuntimeException("Cannot measured across the paragraph:" - + "para: (" + paraStart + ", " + paraEnd + "), " - + "request: (" + start + ", " + end + ")"); - } - return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); - } - - /** - * Returns the size of native MeasuredText memory usage - * - * Note that this may not be aculate. Must be used only for testing purposes. - * @hide - */ - public int getMemoryUsage() { - int r = 0; - for (int i = 0; i < getParagraphCount(); ++i) { - r += getMeasuredParagraph(i).getMemoryUsage(); - } - return r; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // Spanned overrides - // - // Just proxy for underlying mText if appropriate. - - @Override - public <T> T[] getSpans(int start, int end, Class<T> type) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpans(start, end, type); - } else { - return ArrayUtils.emptyArray(type); - } - } - - @Override - public int getSpanStart(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanStart(tag); - } else { - return -1; - } - } - - @Override - public int getSpanEnd(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanEnd(tag); - } else { - return -1; - } - } - - @Override - public int getSpanFlags(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanFlags(tag); - } else { - return 0; - } - } - - @Override - public int nextSpanTransition(int start, int limit, Class type) { - if (mText instanceof Spanned) { - return ((Spanned) mText).nextSpanTransition(start, limit, type); - } else { - return mText.length(); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CharSequence overrides. - // - // Just proxy for underlying mText. - - @Override - public int length() { - return mText.length(); - } - - @Override - public char charAt(int index) { - // TODO: Should this be index + mStart ? - return mText.charAt(index); - } - - @Override - public CharSequence subSequence(int start, int end) { - // TODO: return MeasuredText. - // TODO: Should this be index + mStart, end + mStart ? - return mText.subSequence(start, end); - } - - @Override - public String toString() { - return mText.toString(); - } -} diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java new file mode 100644 index 000000000000..39fc2bd52db4 --- /dev/null +++ b/core/java/android/text/PrecomputedText.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2017 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.util.IntArray; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; + +/** + * A text which has the character metrics data + * + * This text holds a part of the text layout result. You can accelerate + * {@link android.widget.TextView} or {@link StaticLayout} by using this text. + * + * <pre> + * Example of background measurement. + * <code> + * void asyncSetText(final TextView textView, final String expensiveLongString, Handler handler) { + * final PrecomputedText.Params params = textView.getTextParams(); + * handler.post(() -> { + * final PrecomputedText precomputedText + * = PrecomputedText.create(expensiveLongString, params); + * textView.post(() -> { + * textView.setPrecomputedTextOrThrow(precomputedText); + * }); + * }); + * } + * </code> + * </pre> + * + * Note that the {@link PrecomputedText} created from different parameters of the target + * {@link android.widget.TextView} will be rejected internally and compute the text layout again + * with the current {@link android.widget.TextView} parameters. + */ +public class PrecomputedText { + private static final char LINE_FEED = '\n'; + + /** + * The information required for building {@link PrecomputedText}. + * + * Contains information required for precomputing text measurement metadata, so it can be done + * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout + * constraints are not known. + */ + public static class Params { + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private final @NonNull TextDirectionHeuristic mTextDir; + + // The break strategy for this measured text. + private final @Layout.BreakStrategy int mBreakStrategy; + + // The hyphenation frequency for this measured text. + private final @Layout.HyphenationFrequency int mHyphenationFrequency; + + /** + * A builder for creating {@link Params}. + */ + public static class Builder { + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; + + // The break strategy for this measured text. + private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; + + // The hyphenation frequency for this measured text. + private @Layout.HyphenationFrequency int mHyphenationFrequency = + Layout.HYPHENATION_FREQUENCY_NORMAL; + + /** + * Builder constructor + * + * @param paint The paint to be used for drawing + */ + public Builder(@NonNull TextPaint paint) { + mPaint = paint; + } + + /** + * Set the line break strategy + * + * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. + * + * @param strategy The break strategy + * @return this builder, useful for chaining + */ + public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) { + mBreakStrategy = strategy; + return this; + } + + /** + * Set the hyphenation frequency + * + * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. + * + * @param frequency The hyphenation frequency + * @return this builder, useful for chaining + */ + public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) { + mHyphenationFrequency = frequency; + return this; + } + + /** + * Set the text direction heuristic + * + * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. + * + * @param textDir The text direction heuristic for resolving bidi behavior + * @return this builder, useful for chaining + */ + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { + mTextDir = textDir; + return this; + } + + /** + * Build the {@link Params} + * + * @return the layout parameter + */ + public @NonNull Params build() { + return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency); + } + } + + // Use Builder instead. + /** @hide */ + public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, + @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { + mPaint = paint; + mTextDir = textDir; + mBreakStrategy = strategy; + mHyphenationFrequency = frequency; + } + + /** + * Returns the {@link TextPaint} for this text + * + * @return A {@link TextPaint} + */ + public @NonNull TextPaint getTextPaint() { + return mPaint; + } + + /** + * Returns the {@link TextDirectionHeuristic} for this text + * + * @return A {@link TextDirectionHeuristic} + */ + public @NonNull TextDirectionHeuristic getTextDirection() { + return mTextDir; + } + + /** + * Returns the break strategy for this text + * + * @return A line break strategy + */ + public @Layout.BreakStrategy int getBreakStrategy() { + return mBreakStrategy; + } + + /** + * Returns the hyphenation frequency for this text + * + * @return A hyphenation frequency + */ + public @Layout.HyphenationFrequency int getHyphenationFrequency() { + return mHyphenationFrequency; + } + + private boolean sameTextMetricsInternal(@NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, + @Layout.HyphenationFrequency int frequency) { + return mTextDir == textDir + && mBreakStrategy == strategy + && mHyphenationFrequency == frequency + && mPaint.equalsForTextMeasurement(paint); + } + + /** + * Check if the same text layout. + * + * @return true if this and the given param result in the same text layout + */ + public boolean sameTextMetrics(@NonNull Params param) { + return sameTextMetricsInternal(param.mPaint, param.mTextDir, param.mBreakStrategy, + param.mHyphenationFrequency); + } + }; + + // The original text. + private final @NonNull CharSequence mText; + + // The inclusive start offset of the measuring target. + private final @IntRange(from = 0) int mStart; + + // The exclusive end offset of the measuring target. + private final @IntRange(from = 0) int mEnd; + + private final @NonNull Params mParams; + + // The measured paragraph texts. + private final @NonNull MeasuredParagraph[] mMeasuredParagraphs; + + // The sorted paragraph end offsets. + private final @NonNull int[] mParagraphBreakPoints; + + /** + * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph + * positioning information. + * <p> + * This can be expensive, so computing this on a background thread before your text will be + * presented can save work on the UI thread. + * </p> + * + * @param text The text to be measured + * @param param Parameters that define how text will be precomputed + * @return A {@link PrecomputedText} + */ + public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params param) { + return createInternal(text, param, 0, text.length(), true /* compute full Layout */); + } + + + /** @hide */ + public static PrecomputedText createWidthOnly(@NonNull CharSequence text, @NonNull Params param, + @IntRange(from = 0) int start, @IntRange(from = 0) int end) { + return createInternal(text, param, start, end, false /* compute width only */); + } + + private static PrecomputedText createInternal(@NonNull CharSequence text, @NonNull Params param, + @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(param); + final boolean needHyphenation = param.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE + && param.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; + + final IntArray paragraphEnds = new IntArray(); + final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>(); + + int paraEnd = 0; + for (int paraStart = start; paraStart < end; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); + if (paraEnd < 0) { + // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph + // end. + paraEnd = end; + } else { + paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. + } + + paragraphEnds.add(paraEnd); + measuredTexts.add(MeasuredParagraph.buildForStaticLayout( + param.getTextPaint(), text, paraStart, paraEnd, param.getTextDirection(), + needHyphenation, computeLayout, null /* no recycle */)); + } + + return new PrecomputedText(text, start, end, param, + measuredTexts.toArray(new MeasuredParagraph[measuredTexts.size()]), + paragraphEnds.toArray()); + } + + // Use PrecomputedText.create instead. + private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, + @IntRange(from = 0) int end, @NonNull Params param, + @NonNull MeasuredParagraph[] measuredTexts, @NonNull int[] paragraphBreakPoints) { + mText = TextUtils.stringOrSpannedString(text); + mStart = start; + mEnd = end; + mParams = param; + mMeasuredParagraphs = measuredTexts; + mParagraphBreakPoints = paragraphBreakPoints; + } + + /** + * Return the underlying text. + */ + public @NonNull CharSequence getText() { + return mText; + } + + /** + * Returns the inclusive start offset of measured region. + * @hide + */ + public @IntRange(from = 0) int getStart() { + return mStart; + } + + /** + * Returns the exclusive end offset of measured region. + * @hide + */ + public @IntRange(from = 0) int getEnd() { + return mEnd; + } + + /** + * Returns the layout parameters used to measure this text. + */ + public @NonNull Params getParams() { + return mParams; + } + + /** + * Returns the length of the paragraph of this text. + */ + public @IntRange(from = 0) int getParagraphCount() { + return mParagraphBreakPoints.length; + } + + /** + * Returns the paragraph start offset of the text. + */ + public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1]; + } + + /** + * Returns the paragraph end offset of the text. + */ + public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return mParagraphBreakPoints[paraIndex]; + } + + /** @hide */ + public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { + return mMeasuredParagraphs[paraIndex]; + } + + /** + * Returns true if the given TextPaint gives the same result of text layout for this text. + * @hide + */ + public boolean canUseMeasuredResult(@IntRange(from = 0) int start, @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, + @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { + final TextPaint mtPaint = mParams.getTextPaint(); + return mStart == start + && mEnd == end + && mParams.sameTextMetricsInternal(paint, textDir, strategy, frequency); + } + + /** @hide */ + public int findParaIndex(@IntRange(from = 0) int pos) { + // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring + // layout support to StaticLayout. + for (int i = 0; i < mParagraphBreakPoints.length; ++i) { + if (pos < mParagraphBreakPoints[i]) { + return i; + } + } + throw new IndexOutOfBoundsException( + "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1] + + ", gave " + pos); + } + + /** @hide */ + public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { + final int paraIndex = findParaIndex(start); + final int paraStart = getParagraphStart(paraIndex); + final int paraEnd = getParagraphEnd(paraIndex); + if (start < paraStart || paraEnd < end) { + throw new RuntimeException("Cannot measured across the paragraph:" + + "para: (" + paraStart + ", " + paraEnd + "), " + + "request: (" + start + ", " + end + ")"); + } + return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); + } + + /** + * Returns the size of native PrecomputedText memory usage + * + * Note that this is not guaranteed to be accurate. Must be used only for testing purposes. + * @hide + */ + public int getMemoryUsage() { + int r = 0; + for (int i = 0; i < getParagraphCount(); ++i) { + r += getMeasuredParagraph(i).getMemoryUsage(); + } + return r; + } +} diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index e62f4216f33a..5869802e74e6 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -78,6 +78,23 @@ public class StaticLayout extends Layout { /** * Obtain a builder for constructing StaticLayout objects. * + * @param source The precomputed text. + * @param start The index of the start of the text + * @param end The index + 1 of the end of the text + * @param paint The base paint used for layout + * @param width The width in pixels + * @return a builder object used for constructing the StaticLayout + */ + @NonNull + public static Builder obtain(@NonNull PrecomputedText source, @IntRange(from = 0) int start, + @IntRange(from = 0) int end, @NonNull TextPaint paint, + @IntRange(from = 0) int width) { + return obtain(source.getText(), source, start, end, paint, width); + } + + /** + * Obtain a builder for constructing StaticLayout objects. + * * @param source The text to be laid out, optionally with spans * @param start The index of the start of the text * @param end The index + 1 of the end of the text @@ -89,6 +106,12 @@ public class StaticLayout extends Layout { public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @IntRange(from = 0) int width) { + return obtain(source, null, start, end, paint, width); + } + + private static Builder obtain(@NonNull CharSequence source, @Nullable PrecomputedText text, + @IntRange(from = 0) int start, @IntRange(from = 0) int end, + @NonNull TextPaint paint, @IntRange(from = 0) int width) { Builder b = sPool.acquire(); if (b == null) { b = new Builder(); @@ -96,6 +119,7 @@ public class StaticLayout extends Layout { // set default initial values b.mText = source; + b.mPrecomputed = text; b.mStart = start; b.mEnd = end; b.mPaint = paint; @@ -428,6 +452,7 @@ public class StaticLayout extends Layout { } private CharSequence mText; + private PrecomputedText mPrecomputed; private int mStart; private int mEnd; private TextPaint mPaint; @@ -490,7 +515,7 @@ public class StaticLayout extends Layout { float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { - this(source, bufstart, bufend, paint, outerwidth, align, + this(source, null /* precomputed */, bufstart, bufend, paint, outerwidth, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); } @@ -500,7 +525,16 @@ public class StaticLayout extends Layout { * @deprecated Use {@link Builder} instead. */ @Deprecated - public StaticLayout(CharSequence source, int bufstart, int bufend, + public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, + int outerwidth, Alignment align, TextDirectionHeuristic textDir, + float spacingmult, float spacingadd, boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { + this(source, null /* precomputed */, bufstart, bufend, paint, outerwidth, align, textDir, + spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, maxLines); + } + + /** @hide */ + public StaticLayout(CharSequence source, PrecomputedText precomputed, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, @@ -511,6 +545,7 @@ public class StaticLayout extends Layout { : (source instanceof Spanned) ? new SpannedEllipsizer(source) : new Ellipsizer(source), + (ellipsize == null) ? precomputed : null, paint, outerwidth, align, textDir, spacingmult, spacingadd); Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth) @@ -651,44 +686,21 @@ public class StaticLayout extends Layout { b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, indents, mLeftPaddings, mRightPaddings); - MeasuredText measured = null; - final Spanned spanned; - final boolean canUseMeasuredText; - if (source instanceof MeasuredText) { - measured = (MeasuredText) source; - - if (bufStart != measured.getStart() || bufEnd != measured.getEnd()) { - // The buffer position has changed. Re-measure here. - canUseMeasuredText = false; - } else if (b.mBreakStrategy != measured.getBreakStrategy() - || b.mHyphenationFrequency != measured.getHyphenationFrequency()) { - // The computed hyphenation pieces may not be able to used. Re-measure it. - canUseMeasuredText = false; - } else { - // We can use measured information. - canUseMeasuredText = true; + PrecomputedText measured = null; + final Spanned spanned = (b.mText instanceof Spanned) ? (Spanned) b.mText : null; + if (b.mPrecomputed != null) { + if (b.mPrecomputed.canUseMeasuredResult(bufStart, bufEnd, textDir, paint, + b.mBreakStrategy, b.mHyphenationFrequency)) { + measured = b.mPrecomputed; } - } else { - canUseMeasuredText = false; } - - if (!canUseMeasuredText) { - measured = new MeasuredText.Builder(source, paint) - .setRange(bufStart, bufEnd) - .setTextDirection(textDir) - .setBreakStrategy(b.mBreakStrategy) - .setHyphenationFrequency(b.mHyphenationFrequency) - .build(false /* full layout is not necessary for line breaking */); - spanned = (source instanceof Spanned) ? (Spanned) source : null; - } else { - final CharSequence original = measured.getText(); - spanned = (original instanceof Spanned) ? (Spanned) original : null; - // Overwrite with the one when measured. - // TODO: Give an option for developer not to overwrite and measure again here? - textDir = measured.getTextDir(); - paint = measured.getPaint(); + if (measured == null) { + final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir, + b.mBreakStrategy, b.mHyphenationFrequency); + measured = PrecomputedText.createWidthOnly(source, param, bufStart, bufEnd); } + try { for (int paraIndex = 0; paraIndex < measured.getParagraphCount(); paraIndex++) { final int paraStart = measured.getParagraphStart(paraIndex); diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 55367dcce47e..be5bb4d1809a 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -16,6 +16,7 @@ package android.text; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Canvas; @@ -60,7 +61,7 @@ public class TextLine { private char[] mChars; private boolean mCharsValid; private Spanned mSpanned; - private MeasuredText mMeasured; + private PrecomputedText mComputed; // Additional width of whitespace for justification. This value is per whitespace, thus // the line width will increase by mAddedWidth x (number of stretchable whitespaces). @@ -119,7 +120,7 @@ public class TextLine { tl.mSpanned = null; tl.mTabs = null; tl.mChars = null; - tl.mMeasured = null; + tl.mComputed = null; tl.mMetricAffectingSpanSpanSet.recycle(); tl.mCharacterStyleSpanSet.recycle(); @@ -149,10 +150,31 @@ public class TextLine { * @param tabStops the tabStops. Can be null. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) - public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, - Directions directions, boolean hasTabs, TabStops tabStops) { + public void set(TextPaint paint, CharSequence text, int start, + int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops) { + set(paint, text, null, start, limit, dir, directions, hasTabs, tabStops); + } + + /** + * Initializes a TextLine and prepares it for use. + * + * @param paint the base paint for the line + * @param text the text, can be Styled + * @param precomputed the precomputed text + * @param start the start of the line relative to the text + * @param limit the limit of the line relative to the text + * @param dir the paragraph direction of this line + * @param directions the directions information of this line + * @param hasTabs true if the line might contain tabs + * @param tabStops the tabStops. + */ + public void set(@NonNull TextPaint paint, @NonNull CharSequence text, + @Nullable PrecomputedText precomputed, @IntRange(from = 0) int start, + @IntRange(from = 0) int limit, int dir, @NonNull Directions directions, boolean hasTabs, + @Nullable TabStops tabStops) { mPaint = paint; mText = text; + mComputed = precomputed; mStart = start; mLen = limit - start; mDir = dir; @@ -170,14 +192,6 @@ public class TextLine { hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0; } - mMeasured = null; - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; - if (mt.canUseMeasuredResult(paint)) { - mMeasured = mt; - } - } - mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; if (mCharsValid) { @@ -746,12 +760,12 @@ public class TextLine { return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset); } else { final int delta = mStart; - if (mMeasured == null) { + if (mComputed == null) { // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text. return wp.getRunAdvance(mText, delta + start, delta + end, delta + contextStart, delta + contextEnd, runIsRtl, delta + offset); } else { - return mMeasured.getWidth(start + delta, end + delta); + return mComputed.getWidth(start + delta, end + delta); } } } diff --git a/core/java/android/view/RecordingCanvas.java b/core/java/android/view/RecordingCanvas.java index fbb862be54ef..fc7d828de12e 100644 --- a/core/java/android/view/RecordingCanvas.java +++ b/core/java/android/view/RecordingCanvas.java @@ -34,7 +34,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.TemporaryBuffer; import android.text.GraphicsOperations; -import android.text.MeasuredText; +import android.text.PrecomputedText; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; @@ -507,8 +507,8 @@ public class RecordingCanvas extends Canvas { TextUtils.getChars(text, contextStart, contextEnd, buf, 0); long measuredTextPtr = 0; int measuredTextOffset = 0; - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; + if (text instanceof PrecomputedText) { + PrecomputedText mt = (PrecomputedText) text; int paraIndex = mt.findParaIndex(start); if (end <= mt.getParagraphEnd(paraIndex)) { // Only support if the target is in the same paragraph. @@ -641,7 +641,7 @@ public class RecordingCanvas extends Canvas { @FastNative private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count, int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint, - long nativeMeasuredText, int measuredTextOffset); + long nativePrecomputedText, int measuredTextOffset); @FastNative private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 5710db3ce8e0..f6e771a9605e 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -80,8 +80,8 @@ import android.text.GraphicsOperations; import android.text.InputFilter; import android.text.InputType; import android.text.Layout; -import android.text.MeasuredText; import android.text.ParcelableSpan; +import android.text.PrecomputedText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -637,6 +637,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private CharSequence mText; private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; + private PrecomputedText mPrecomputed; private CharSequence mHint; private Layout mHintLayout; @@ -4085,6 +4086,80 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText} + * + * @return A current {@link PrecomputedText.Params} + */ + public @NonNull PrecomputedText.Params getTextMetricsParams() { + return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(), + mBreakStrategy, mHyphenationFrequency); + } + + /** + * Apply the text layout parameter. + */ + public void setTextMetricsParams(@NonNull PrecomputedText.Params params) { + mTextPaint.set(params.getTextPaint()); + mTextDir = params.getTextDirection(); + mBreakStrategy = params.getBreakStrategy(); + mHyphenationFrequency = params.getHyphenationFrequency(); + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Sets the precomputed text. + * + * If the parameters for the precomputed text is different from current text view parameters, + * apply the parameteres to the text view too. + * + * @param text A precomputed text. + */ + public void setPrecomputedTextAndParams(@NonNull PrecomputedText text) { + Preconditions.checkNotNull(text); + final PrecomputedText.Params params = text.getParams(); + if (!params.sameTextMetrics(getTextMetricsParams())) { + setTextMetricsParams(params); + } + setText(text.getText()); + if (mTransformed != text.getText()) { + // setText modified given text for some reasons, selection, transformation, etc. + // Can't use computed result. + return; + } else { + mPrecomputed = text; + } + } + + /** + * Sets the precomputed text. + * + * If the parameters for the precomputed text is different from current text view parameters, + * throws {@link IllegalArgumentException}. + * + * @param text A precomputed text. + */ + public void setPrecomputedTextOrThrow(@NonNull PrecomputedText text) { + Preconditions.checkNotNull(text); + final PrecomputedText.Params params = text.getParams(); + if (!params.sameTextMetrics(getTextMetricsParams())) { + throw new IllegalArgumentException( + "The precomputed configuration is different from this TextView."); + } + setText(text.getText()); + if (mTransformed != text.getText()) { + // setText modified given text for some reasons, selection, transformation, etc. + // Can't use computed result. + // TODO: Do we throw an exception here too? + } else { + mPrecomputed = text; + } + } + + /** * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the * last line is too short for justification, the last line will be displayed with the * alignment set by {@link android.view.View#setTextAlignment}. @@ -5519,6 +5594,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) { + mPrecomputed = null; mTextSetFromXmlOrResourceId = false; if (text == null) { text = ""; @@ -5577,7 +5653,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) { + } else if (!(text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -8244,7 +8320,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener result = builder.build(); } else { if (boring == UNKNOWN_BORING) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); + boring = BoringLayout.isBoring(mTransformed, mPrecomputed, mTextPaint, mTextDir, + mBoring); if (boring != null) { mBoring = boring; } @@ -8282,9 +8359,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } if (result == null) { - StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed, - 0, mTransformed.length(), mTextPaint, wantWidth) - .setAlignment(alignment) + StaticLayout.Builder builder; + if (mPrecomputed != null) { + builder = StaticLayout.Builder.obtain(mPrecomputed, 0, + mPrecomputed.getText().length(), mTextPaint, wantWidth); + } else { + builder = StaticLayout.Builder.obtain(mTransformed, 0, mTransformed.length(), + mTextPaint, wantWidth); + } + builder.setAlignment(alignment) .setTextDirection(mTextDir) .setLineSpacing(mSpacingAdd, mSpacingMult) .setIncludePad(mIncludePad) @@ -8411,7 +8494,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (des < 0) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); + boring = BoringLayout.isBoring(mTransformed, mPrecomputed, mTextPaint, mTextDir, + mBoring); if (boring != null) { mBoring = boring; } @@ -11696,6 +11780,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns the current {@link TextDirectionHeuristic} + * + * @return A {@link TextDirectionHeuristic}. * @hide */ protected TextDirectionHeuristic getTextDirectionHeuristic() { diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp index 482d028a67f3..2c05d0b976fc 100644 --- a/core/jni/android/graphics/Paint.cpp +++ b/core/jni/android/graphics/Paint.cpp @@ -1008,6 +1008,23 @@ namespace PaintGlue { return paint->getLooper() && paint->getLooper()->asABlurShadow(nullptr); } + static jboolean equalsForTextMeasurement(jlong lPaint, jlong rPaint) { + if (lPaint == rPaint) { + return true; + } + Paint* leftPaint = reinterpret_cast<Paint*>(lPaint); + Paint* rightPaint = reinterpret_cast<Paint*>(rPaint); + + const Typeface* leftTypeface = Typeface::resolveDefault(leftPaint->getAndroidTypeface()); + const Typeface* rightTypeface = Typeface::resolveDefault(rightPaint->getAndroidTypeface()); + minikin::MinikinPaint leftMinikinPaint + = MinikinUtils::prepareMinikinPaint(leftPaint, leftTypeface); + minikin::MinikinPaint rightMinikinPaint + = MinikinUtils::prepareMinikinPaint(rightPaint, rightTypeface); + + return leftMinikinPaint == rightMinikinPaint; + } + }; // namespace PaintGlue static const JNINativeMethod methods[] = { @@ -1107,7 +1124,8 @@ static const JNINativeMethod methods[] = { {"nGetStrikeThruPosition","(J)F", (void*) PaintGlue::getStrikeThruPosition}, {"nGetStrikeThruThickness","(J)F", (void*) PaintGlue::getStrikeThruThickness}, {"nSetShadowLayer", "(JFFFI)V", (void*)PaintGlue::setShadowLayer}, - {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer} + {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer}, + {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement}, }; int register_android_graphics_Paint(JNIEnv* env) { diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java index eacb727099ed..07df0454362c 100644 --- a/graphics/java/android/graphics/BaseCanvas.java +++ b/graphics/java/android/graphics/BaseCanvas.java @@ -22,7 +22,7 @@ import android.annotation.Nullable; import android.annotation.Size; import android.graphics.Canvas.VertexMode; import android.text.GraphicsOperations; -import android.text.MeasuredText; +import android.text.PrecomputedText; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; @@ -487,8 +487,8 @@ public abstract class BaseCanvas { TextUtils.getChars(text, contextStart, contextEnd, buf, 0); long measuredTextPtr = 0; int measuredTextOffset = 0; - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; + if (text instanceof PrecomputedText) { + PrecomputedText mt = (PrecomputedText) text; int paraIndex = mt.findParaIndex(start); if (end <= mt.getParagraphEnd(paraIndex)) { // Only suppor the same paragraph. @@ -647,7 +647,7 @@ public abstract class BaseCanvas { private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count, int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint, - long nativeMeasuredText, int measuredTextOffset); + long nativePrecomputedText, int measuredTextOffset); private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint); diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index ed147e9e2ec7..42dac38affba 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2835,6 +2835,16 @@ public class Paint { return result; } + /** + * Returns true of the passed {@link Paint} will have the same effect on text measurement + * + * @param other A {@link Paint} object. + * @return true if the other {@link Paint} has the same effect on text measurement. + */ + public boolean equalsForTextMeasurement(@NonNull Paint other) { + return nEqualsForTextMeasurement(mNativePaint, other.mNativePaint); + } + // regular JNI private static native long nGetNativeFinalizer(); private static native long nInit(); @@ -3002,4 +3012,6 @@ public class Paint { private static native float nGetStrikeThruThickness(long paintPtr); @CriticalNative private static native void nSetTextSize(long paintPtr, float textSize); + @CriticalNative + private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr); } |