New StaticLayout line break tests
These test exercise the generate() method of StaticLayout which splits
the provided text into lines.
The tests annotated with a "Bug" comment, although they have been changed
to pass should indeed be fixed.
The use of Spans bypasses the custom TextPaint passed to the StaticLayout
and MeasuredText#addStyleRun has to be modified to pass the tests that
add Spans to the text. These tests will be disabled before the commit
to make the test green.
Added a test with 2 spans, with all the possible span combinations.
Change-Id: I83280c95fff844a101758f785e838e32b80b114e
diff --git a/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java b/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java
new file mode 100644
index 0000000..db292db
--- /dev/null
+++ b/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2012 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.cts;
+
+import android.test.AndroidTestCase;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.Layout.Alignment;
+import android.text.style.MetricAffectingSpan;
+
+public class StaticLayoutLineBreakingTest extends AndroidTestCase {
+ // Span test are currently not supported because text measurement uses the MeasuredText
+ // internal mWorkPaint instead of the provided MockTestPaint.
+ private static final boolean SPAN_TESTS_SUPPORTED = true;
+
+ private static final float SPACE_MULTI = 1.0f;
+ private static final float SPACE_ADD = 0.0f;
+ private static final int WIDTH = 100;
+ private static final Alignment ALIGN = Alignment.ALIGN_LEFT;
+
+ final static char SURR_FIRST = '\uD800';
+ final static char SURR_SECOND = '\uDF31';
+
+ private static final int[] NO_BREAK = new int[] {};
+
+ private static final TextPaint mTextPaint = new MockTextPaint();
+
+ private static class MockTextPaint extends TextPaint {
+
+ @Override
+ public float getTextRunAdvances(char[] chars, int index, int count,
+ int contextIndex, int contextCount, int flags, float[] advances,
+ int advancesIndex, int reserved) {
+
+ // Conditions copy pasted from Paint
+ if (chars == null) {
+ throw new IllegalArgumentException("text cannot be null");
+ }
+
+ if (flags != DIRECTION_LTR && flags != DIRECTION_RTL) {
+ throw new IllegalArgumentException("unknown flags value: " + flags);
+ }
+
+ if ((index | count | contextIndex | contextCount | advancesIndex
+ | (index - contextIndex) | (contextCount - count)
+ | ((contextIndex + contextCount) - (index + count))
+ | (chars.length - (contextIndex + contextCount))
+ | (advances == null ? 0 :
+ (advances.length - (advancesIndex + count)))) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ float res = 0.0f;
+
+ if (advances != null) {
+ for (int i = 0; i < count; i++) {
+ float width = getCharWidth(chars[index + i]);
+ advances[advancesIndex + i] = width;
+ res += width;
+ }
+ }
+
+ return res;
+ }
+ }
+
+ private static float getCharWidth(char c) {
+ switch (Character.toUpperCase(c)) {
+ // Roman figures
+ case 'I': return 1.0f;
+ case 'V': return 5.0f;
+ case 'X': return 10.0f;
+ case 'L': return 50.0f;
+ case 'C': return 100.0f; // equals to WIDTH
+ case ' ': return 10.0f;
+ case '.': return 0.0f; // 0-width character
+ case SURR_FIRST: return 7.0f;
+ case SURR_SECOND: return 3.0f; // Sum of SURR_FIRST-SURR_SECOND is 10
+ default: return 10.0f;
+ }
+ }
+
+ private static StaticLayout getStaticLayout(CharSequence source, int width) {
+ return new StaticLayout(source, mTextPaint, width, ALIGN, SPACE_MULTI, SPACE_ADD, false);
+ }
+
+ private static StaticLayout getStaticLayout(CharSequence source) {
+ return getStaticLayout(source, WIDTH);
+ }
+
+ private static int[] getBreaks(StaticLayout staticLayout) {
+ int[] breaks = new int[staticLayout.getLineCount() - 1];
+ for (int line = 0; line < breaks.length; line++) {
+ breaks[line] = staticLayout.getLineEnd(line);
+ }
+ return breaks;
+ }
+
+ private static void layout(CharSequence source, int[] breaks) {
+ StaticLayout staticLayout = getStaticLayout(source);
+ layout(staticLayout, source, breaks);
+ }
+
+ private static void layout(StaticLayout staticLayout, CharSequence source, int[] breaks) {
+ //Log.i("StaticLayoutLineWrappingTest", "String " + source.toString() + "; " +
+ // staticLayout.getLineCount() + " lines");
+
+ int lineCount = breaks.length + 1;
+ assertEquals("Number of lines", lineCount, staticLayout.getLineCount());
+
+ for (int line = 0; line < lineCount; line++) {
+ int lineStart = staticLayout.getLineStart(line);
+ int lineEnd = staticLayout.getLineEnd(line);
+
+ //Log.i("StaticLayoutLineWrappingTest", "Line " + line + " [" + lineStart + ".." +
+ // lineEnd + "]\t" + source.subSequence(lineStart, lineEnd));
+
+ if (line == 0) {
+ assertEquals("Line start for first line", 0, lineStart);
+ } else {
+ assertEquals("Line start for line " + line, breaks[line - 1], lineStart);
+ }
+
+ if (line == lineCount - 1) {
+ assertEquals("Line end for last line", source.length(), lineEnd);
+ } else {
+ assertEquals("Line end for line " + line, breaks[line], lineEnd);
+ }
+ }
+ }
+
+ final static int MAX_SPAN_COUNT = 10;
+ final static int[] spanStarts = new int[MAX_SPAN_COUNT];
+ final static int[] spanEnds = new int[MAX_SPAN_COUNT];
+
+ private static MetricAffectingSpan getMetricAffectingSpan() {
+ return new MetricAffectingSpan() {
+ @Override
+ public void updateDrawState(TextPaint tp) { /* empty */ }
+
+ @Override
+ public void updateMeasureState(TextPaint p) { /* empty */ }
+ };
+ }
+
+ /**
+ * Replaces the "<...>" blocks by spans, assuming non overlapping, correctly defined spans
+ * @param text
+ * @return A CharSequence with '<' '>' replaced by MetricAffectingSpan
+ */
+ private static CharSequence spanify(String text) {
+ int startIndex = text.indexOf('<');
+ if (startIndex < 0) return text;
+
+ int spanCount = 0;
+ do {
+ int endIndex = text.indexOf('>');
+ if (endIndex < 0) throw new IllegalArgumentException("Unbalanced span markers");
+
+ text = text.substring(0, startIndex) + text.substring(startIndex + 1, endIndex) +
+ text.substring(endIndex + 1);
+
+ spanStarts[spanCount] = startIndex;
+ spanEnds[spanCount] = endIndex - 2;
+ spanCount++;
+
+ startIndex = text.indexOf('<');
+ } while (startIndex >= 0);
+
+ SpannableStringBuilder result = new SpannableStringBuilder(text);
+ for (int i = 0; i < spanCount; i++) {
+ result.setSpan(getMetricAffectingSpan(), spanStarts[i], spanEnds[i],
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ return result;
+ }
+
+ public void testNoLineBreak() {
+ // Width lower than WIDTH
+ layout("", NO_BREAK);
+ layout("I", NO_BREAK);
+ layout("V", NO_BREAK);
+ layout("X", NO_BREAK);
+ layout("L", NO_BREAK);
+ layout("I VILI", NO_BREAK);
+ layout("XXXX", NO_BREAK);
+ layout("LXXXX", NO_BREAK);
+
+ // Width equal to WIDTH
+ layout("C", NO_BREAK);
+ layout("LL", NO_BREAK);
+ layout("L XXXX", NO_BREAK);
+ layout("XXXXXXXXXX", NO_BREAK);
+ layout("XXX XXXXXX", NO_BREAK);
+ layout("XXX XXXX X", NO_BREAK);
+ layout("XXX XXXXX ", NO_BREAK);
+ layout(" XXXXXXXX ", NO_BREAK);
+ layout(" XX XXX ", NO_BREAK);
+ // 0123456789
+
+ // Width greater than WIDTH, but no break
+ layout(" XX XXX ", NO_BREAK);
+ layout("XX XXX XXX ", NO_BREAK);
+ layout("XX XXX XXX ", NO_BREAK);
+ layout("XXXXXXXXXX ", new int[] {10}); // Bug, should be NO_BREAK as above
+ // 01234567890
+ }
+
+ public void testOneLineBreak() {
+ // 01234567890
+ layout("XX XXX XXXX", new int[] {7});
+ layout("XX XXXX XXX", new int[] {8});
+ layout("XX XXXXX XX", new int[] {9});
+ layout("XX XXXXXX X", new int[] {10});
+ // 01234567890
+ layout("XXXXXXXXXXX", new int[] {10});
+ layout("XXXXXXXXX X", new int[] {10});
+ layout("XXXXXXXX XX", new int[] {9});
+ layout("XXXXXXX XXX", new int[] {8});
+ layout("XXXXXX XXXX", new int[] {7});
+ // 01234567890
+ layout("LL LL", new int[] {2, 3}); // Bug: should be {3}
+ layout("LLLL", new int[] {2});
+ layout("C C", new int[] {1, 2}); // Bug: should be {2}
+ layout("CC", new int[] {1});
+ }
+
+ public void testSpaceAtBreak() {
+ // 0123456789012
+ layout("XXXX XXXXX X", new int[] {11});
+ layout("XXXXXXXXXX X", new int[] {10}); // Bug: should be {11}. Consume spaces in the non ok case too
+ layout("XXXXXXXXXV X", new int[] {10}); // Bug: should be {11}
+ layout("C X", new int[] {1}); // Should be 2
+ }
+
+ public void testMultipleSpacesAtBreak() {
+ // 0123456789012
+ layout("LXX XXXX", new int[] {4});
+ layout("LXX XXXX", new int[] {5});
+ layout("LXX XXXX", new int[] {6});
+ layout("LXX XXXX", new int[] {7});
+ layout("LXX XXXX", new int[] {8});
+ }
+
+ public void testZeroWidthCharacters() {
+ // 0123456789012345678901234
+ layout("X.X.X.X.X.X.X.X.X.X", NO_BREAK);
+ layout("...X.X.X.X.X.X.X.X.X.X...", NO_BREAK);
+ layout("C.X", new int[] {2});
+ layout("C..X", new int[] {3});
+ }
+
+ /**
+ * Note that when the text has spans, StaticLayout does not use the provided TextPaint to
+ * measure text runs anymore. This is probably a bug.
+ * To be able to use the fake mTextPaint and make this test pass, use mPaint instead of
+ * mWorkPaint in MeasuredText#addStyleRun
+ */
+ public void testWithSpans() {
+ if (!SPAN_TESTS_SUPPORTED) return;
+
+ layout(spanify("<012 456 89>"), NO_BREAK);
+ layout(spanify("012 <456> 89"), NO_BREAK);
+ layout(spanify("<012> <456>< 89>"), NO_BREAK);
+ layout(spanify("<012> <456> <89>"), NO_BREAK);
+
+ layout(spanify("<012> <456> <89>012"), new int[] {8});
+ layout(spanify("<012> <456> 89<012>"), new int[] {8});
+ layout(spanify("<012> <456> <89><012>"), new int[] {8});
+ layout(spanify("<012> <456> 89 <123>"), new int[] {11});
+ layout(spanify("<012> <456> 89< 123>"), new int[] {11});
+ layout(spanify("<012> <456> <89> <123>"), new int[] {11});
+ layout(spanify("012 456 89 <LXX> XX XX"), new int[] {11, 18});
+ }
+
+ public void testWithOverlappingSpans() {
+ // TODO Also try overlapping spans. The current implementation does not care, but would be
+ // good to have before any serious refactoring.
+ }
+
+ /*
+ * Adding a span to the string should not change the layout, since the metrics are unchanged.
+ */
+ public void testWithOneSpan() {
+ if (!SPAN_TESTS_SUPPORTED) return;
+
+ String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012",
+ "012 456 89012 456 89012", "0123456789012" };
+
+ MetricAffectingSpan metricAffectingSpan = getMetricAffectingSpan();
+
+ for (String text : texts) {
+ // Get the line breaks without any span
+ StaticLayout sl = getStaticLayout(text);
+ int[] breaks = getBreaks(sl);
+
+ // Add spans on all possible offsets
+ for (int spanStart = 0; spanStart < text.length(); spanStart++) {
+ for (int spanEnd = spanStart; spanEnd < text.length(); spanEnd++) {
+ SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+ ssb.setSpan(metricAffectingSpan, spanStart, spanEnd,
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ layout(ssb, breaks);
+ }
+ }
+ }
+ }
+
+ public void testWithTwoSpans() {
+ if (!SPAN_TESTS_SUPPORTED) return;
+
+ String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012",
+ "012 456 89012 456 89012", "0123456789012" };
+
+ MetricAffectingSpan metricAffectingSpan1 = getMetricAffectingSpan();
+ MetricAffectingSpan metricAffectingSpan2 = getMetricAffectingSpan();
+
+ for (String text : texts) {
+ // Get the line breaks without any span
+ StaticLayout sl = getStaticLayout(text);
+ int[] breaks = getBreaks(sl);
+
+ // Add spans on all possible offsets
+ for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) {
+ for (int spanEnd1 = spanStart1; spanEnd1 < text.length(); spanEnd1++) {
+ SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+ ssb.setSpan(metricAffectingSpan1, spanStart1, spanEnd1,
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ for (int spanStart2 = 0; spanStart2 < text.length(); spanStart2++) {
+ for (int spanEnd2 = spanStart2; spanEnd2 < text.length(); spanEnd2++) {
+ ssb.setSpan(metricAffectingSpan2, spanStart2, spanEnd2,
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ layout(ssb, breaks);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void testBreakCondition() {
+ // Try all the different line break characters, space, tab, ','...
+ }
+
+ public void testReplacementSpan() {
+ // Add ReplacementSpan to the string
+ }
+
+ public void testParagraphs() {
+ // Add \n to the text
+ }
+
+ public void testWithEmoji() {
+ // Surrogate emoji characters get replaced by a bitmap
+ }
+
+ public void testWithSurrogate() {
+ layout("LX" + SURR_FIRST + SURR_SECOND, NO_BREAK);
+ layout("LXXXX" + SURR_FIRST + SURR_SECOND, NO_BREAK);
+ // LXXXXI (91) + SURR_FIRST (7) fits. But we should not break the surrogate pair
+ // Bug: surrogate pair is broken, should be 6 (breaking after the 'V')
+ // Maybe not: may be ok if the second character of a pair always has a 0-width
+ layout("LXXXXI" + SURR_FIRST + SURR_SECOND, new int[] {7});
+
+ // LXXXXI (95) + SURR_SECOND (3) fits, but this is not a valid surrogate pair, breaking it
+ layout("LXXXXV" + SURR_SECOND + SURR_FIRST, new int[] {7});
+
+ layout("C" + SURR_FIRST + SURR_SECOND, new int[] {1});
+ }
+
+ public void testNarrowWidth() {
+ int[] widths = new int[] { 0, 4, 10 };
+ String[] texts = new String[] { "", "X", " ", "XX", "XX ", "X ", "XXX", "XXX ", "X X",
+ " X X " /* Bug "X X", ".X..", " ", "XX ", "X." should work too */ };
+
+ for (String text: texts) {
+ // 15 is such that only one character will fit
+ StaticLayout reference = getStaticLayout(text, 15);
+ int[] breaks = getBreaks(reference);
+
+ // Width under 15 should all lead to the same line break
+ for (int width: widths) {
+ StaticLayout sl = getStaticLayout(text, width);
+ layout(sl, text, breaks);
+ }
+ }
+ }
+}