Reorganize MeasuredText API

This CL changes the MeasuredText API:
- Rename MeasuredText to PrecomputedText.
- PrecomputedText is no longer a Spanned.
- Introduce PrecomputedText.Param which holds all text layout parameters.
- Add API to get PrecomputedText.Param from TextView.
- Remove MeasuredText.Builder and add PrecomputedText.create method instead.
- Remove setRange from MeasuredText since it is not for normal use case.
  (It can not be used for TextView)

Here is a performance scores: (median, walleye-userdebug, N=20)

StaticLayout creation time (w/o patch -> w/ patch)
 PrecomputedText Balanced Hyphenation  :    743,615 ->    737,145: (-0.9%)
 PrecomputedText Balanced NoHyphenation:    551,544 ->    542,715: (-1.6%)
 PrecomputedText Greedy Hyphenation    :    500,343 ->    499,601: (-0.1%)
 PrecomputedText Greedy NoHyphenation  :    497,987 ->    492,587: (-1.1%)
 RandomText Balanced Hyphenation       : 19,100,592 -> 19,135,289: (+0.2%)
 RandomText Balanced NoHyphenation     :  8,015,088 ->  7,954,260: (-0.8%)
 RandomText Greedy Hyphenation         :  7,950,915 ->  7,877,424: (-0.9%)
 RandomText Greedy NoHyphenation       :  7,939,337 ->  7,863,471: (-1.0%)

PrecomputedText creation time (w/o patch -> w/ patch)
 NoStyled Hyphenation                  : 18,935,638 -> 18,925,422: (-0.1%)
 NoStyled Hyphenation WidthOnly        : 18,469,726 -> 18,978,413: (+2.8%)
 NoStyled NoHyphenation                :  7,940,792 ->  7,919,127: (-0.3%)
 NoStyled NoHyphenation WidthOnly      :  7,463,230 ->  7,922,643: (+6.2%)
 Styled Hyphenation                    : 14,822,501 -> 14,809,017: (-0.1%)
 Styled Hyphenation WidthOnly          : 13,891,770 -> 14,656,617: (+5.5%)
 Styled NoHyphenation                  : 14,511,134 -> 14,301,503: (-1.4%)
 Styled NoHyphenation WidthOnly        : 13,495,345 -> 14,264,314: (+5.7%)

StaticLayout draw time (w/o patch -> w/ patch)
 PrecomputedText NoStyled              :    663,974 ->    661,610: (-0.4%)
 PrecomputedText NoStyled WithoutCache :    648,294 ->    648,766: (+0.1%)
 PrecomputedText Styled                :    879,322 ->    852,770: (-3.0%)
 PrecomputedText Styled WithoutCache   :  1,084,570 ->  1,110,147: (+2.4%)
 RandomText NoStyled                   :    565,682 ->    555,435: (-1.8%)
 RandomText NoStyled WithoutCache      :  9,070,533 ->  9,064,825: (-0.1%)
 RandomText Styled                     :  2,955,202 ->  2,962,008: (+0.2%)
 RandomText Styled WithoutCache        : 12,242,325 -> 12,228,573: (-0.1%)

Bug: 67504091
Bug: 73091756
Test: bit FrameworksCoreTests:android.text.
Test: atest CtsWidgetTestCases:EditTextTest \
    CtsWidgetTestCases:TextViewFadingEdgeTest \
    FrameworksCoreTests:TextViewFallbackLineSpacingTest \
    FrameworksCoreTests:TextViewTest FrameworksCoreTests:TypefaceTest \
    CtsGraphicsTestCases:TypefaceTest CtsWidgetTestCases:TextViewTest \
    CtsTextTestCases
Change-Id: I7db9e2ca4db68a16648cfb8fcf63555f501304c2
This commit is contained in:
Seigo Nonaka
2018-02-01 21:39:24 -08:00
parent d3905e6544
commit 7fd36d19e3
16 changed files with 816 additions and 639 deletions

View File

@@ -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");
}

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -75,6 +75,23 @@ public class StaticLayout extends Layout {
public final static class Builder {
private Builder() {}
/**
* 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.
*
@@ -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,43 +686,20 @@ 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 (measured == null) {
final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir,
b.mBreakStrategy, b.mHyphenationFrequency);
measured = PrecomputedText.createWidthOnly(source, param, bufStart, bufEnd);
}
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();
}
try {
for (int paraIndex = 0; paraIndex < measured.getParagraphCount(); paraIndex++) {

View File

@@ -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);
}
}
}

View File

@@ -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,

View File

@@ -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;
@@ -4084,6 +4085,80 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return mHyphenationFrequency;
}
/**
* 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
@@ -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() {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);
}