1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.text.cts; 18 19 import static org.junit.Assert.assertEquals; 20 21 import android.content.Context;; 22 import android.graphics.Typeface; 23 import android.support.test.InstrumentationRegistry; 24 import android.support.test.filters.SmallTest; 25 import android.support.test.runner.AndroidJUnit4; 26 import android.text.Layout; 27 import android.text.Layout.Alignment; 28 import android.text.SpannableStringBuilder; 29 import android.text.Spanned; 30 import android.text.StaticLayout; 31 import android.text.TextDirectionHeuristics; 32 import android.text.TextPaint; 33 import android.text.style.MetricAffectingSpan; 34 import android.util.Log; 35 36 import org.junit.Test; 37 import org.junit.runner.RunWith; 38 39 @SmallTest 40 @RunWith(AndroidJUnit4.class) 41 public class StaticLayoutLineBreakingTest { 42 // Span test are currently not supported because text measurement uses the MeasuredText 43 // internal mWorkPaint instead of the provided MockTestPaint. 44 private static final boolean SPAN_TESTS_SUPPORTED = false; 45 private static final boolean DEBUG = false; 46 47 private static final float SPACE_MULTI = 1.0f; 48 private static final float SPACE_ADD = 0.0f; 49 private static final int WIDTH = 100; 50 private static final Alignment ALIGN = Alignment.ALIGN_NORMAL; 51 52 private static final char SURR_FIRST = '\uD800'; 53 private static final char SURR_SECOND = '\uDF31'; 54 55 private static final int[] NO_BREAK = new int[] {}; 56 57 private static final TextPaint sTextPaint = new TextPaint(); 58 59 static { 60 // The test font has following coverage and width. 61 // U+0020: 10em 62 // U+002E (.): 10em 63 // U+0043 (C): 100em 64 // U+0049 (I): 1em 65 // U+004C (L): 50em 66 // U+0056 (V): 5em 67 // U+0058 (X): 10em 68 // U+005F (_): 0em 69 // U+FFFD (invalid surrogate will be replaced to this): 7em 70 // U+10331 (\uD800\uDF31): 10em 71 Context context = InstrumentationRegistry.getTargetContext(); 72 sTextPaint.setTypeface(Typeface.createFromAsset(context.getAssets(), 73 "fonts/StaticLayoutLineBreakingTestFont.ttf")); 74 sTextPaint.setTextSize(1.0f); // Make 1em == 1px. 75 } 76 77 private static StaticLayout getStaticLayout(CharSequence source, int width, 78 int breakStrategy) { 79 return StaticLayout.Builder.obtain(source, 0, source.length(), sTextPaint, width) 80 .setAlignment(ALIGN) 81 .setLineSpacing(SPACE_ADD, SPACE_MULTI) 82 .setIncludePad(false) 83 .setBreakStrategy(breakStrategy) 84 .build(); 85 } 86 87 private static int[] getBreaks(CharSequence source) { 88 return getBreaks(source, WIDTH, Layout.BREAK_STRATEGY_SIMPLE); 89 } 90 91 private static int[] getBreaks(CharSequence source, int width, int breakStrategy) { 92 final StaticLayout staticLayout = getStaticLayout(source, width, breakStrategy); 93 94 final int[] breaks = new int[staticLayout.getLineCount() - 1]; 95 for (int line = 0; line < breaks.length; line++) { 96 breaks[line] = staticLayout.getLineEnd(line); 97 } 98 return breaks; 99 } 100 101 private static void debugLayout(CharSequence source, StaticLayout staticLayout) { 102 if (DEBUG) { 103 int count = staticLayout.getLineCount(); 104 Log.i("SLLBTest", "\"" + source.toString() + "\": " 105 + count + " lines"); 106 for (int line = 0; line < count; line++) { 107 int lineStart = staticLayout.getLineStart(line); 108 int lineEnd = staticLayout.getLineEnd(line); 109 Log.i("SLLBTest", "Line " + line + " [" + lineStart + ".." 110 + lineEnd + "]\t" + source.subSequence(lineStart, lineEnd)); 111 } 112 } 113 } 114 115 private static void layout(CharSequence source, int[] breaks) { 116 layout(source, breaks, WIDTH); 117 } 118 119 private static void layout(CharSequence source, int[] breaks, int width) { 120 final int[] breakStrategies = {Layout.BREAK_STRATEGY_SIMPLE, 121 Layout.BREAK_STRATEGY_HIGH_QUALITY}; 122 for (int breakStrategy : breakStrategies) { 123 final StaticLayout staticLayout = getStaticLayout(source, width, breakStrategy); 124 125 debugLayout(source, staticLayout); 126 127 final int lineCount = breaks.length + 1; 128 assertEquals("Number of lines", lineCount, staticLayout.getLineCount()); 129 130 for (int line = 0; line < lineCount; line++) { 131 final int lineStart = staticLayout.getLineStart(line); 132 final int lineEnd = staticLayout.getLineEnd(line); 133 134 if (line == 0) { 135 assertEquals("Line start for first line", 0, lineStart); 136 } else { 137 assertEquals("Line start for line " + line, breaks[line - 1], lineStart); 138 } 139 140 if (line == lineCount - 1) { 141 assertEquals("Line end for last line", source.length(), lineEnd); 142 } else { 143 assertEquals("Line end for line " + line, breaks[line], lineEnd); 144 } 145 } 146 } 147 } 148 149 private static void layoutMaxLines(CharSequence source, int[] breaks, int maxLines) { 150 final StaticLayout staticLayout = StaticLayout.Builder 151 .obtain(source, 0, source.length(), sTextPaint, WIDTH) 152 .setAlignment(ALIGN) 153 .setTextDirection(TextDirectionHeuristics.LTR) 154 .setLineSpacing(SPACE_ADD, SPACE_MULTI) 155 .setIncludePad(false) 156 .setMaxLines(maxLines) 157 .build(); 158 159 debugLayout(source, staticLayout); 160 161 final int lineCount = staticLayout.getLineCount(); 162 163 for (int line = 0; line < lineCount; line++) { 164 int lineStart = staticLayout.getLineStart(line); 165 int lineEnd = staticLayout.getLineEnd(line); 166 167 if (line == 0) { 168 assertEquals("Line start for first line", 0, lineStart); 169 } else { 170 assertEquals("Line start for line " + line, breaks[line - 1], lineStart); 171 } 172 173 if (line == lineCount - 1 && line != breaks.length - 1) { 174 assertEquals("Line end for last line", source.length(), lineEnd); 175 } else { 176 assertEquals("Line end for line " + line, breaks[line], lineEnd); 177 } 178 } 179 } 180 181 private static final int MAX_SPAN_COUNT = 10; 182 private static final int[] sSpanStarts = new int[MAX_SPAN_COUNT]; 183 private static final int[] sSpanEnds = new int[MAX_SPAN_COUNT]; 184 185 private static MetricAffectingSpan getMetricAffectingSpan() { 186 return new MetricAffectingSpan() { 187 @Override 188 public void updateDrawState(TextPaint tp) { /* empty */ } 189 190 @Override 191 public void updateMeasureState(TextPaint p) { /* empty */ } 192 }; 193 } 194 195 /** 196 * Replaces the "<...>" blocks by spans, assuming non overlapping, correctly defined spans 197 * @param text 198 * @return A CharSequence with '<' '>' replaced by MetricAffectingSpan 199 */ 200 private static CharSequence spanify(String text) { 201 int startIndex = text.indexOf('<'); 202 if (startIndex < 0) return text; 203 204 int spanCount = 0; 205 do { 206 int endIndex = text.indexOf('>'); 207 if (endIndex < 0) throw new IllegalArgumentException("Unbalanced span markers"); 208 209 text = text.substring(0, startIndex) + text.substring(startIndex + 1, endIndex) 210 + text.substring(endIndex + 1); 211 212 sSpanStarts[spanCount] = startIndex; 213 sSpanEnds[spanCount] = endIndex - 2; 214 spanCount++; 215 216 startIndex = text.indexOf('<'); 217 } while (startIndex >= 0); 218 219 SpannableStringBuilder result = new SpannableStringBuilder(text); 220 for (int i = 0; i < spanCount; i++) { 221 result.setSpan(getMetricAffectingSpan(), sSpanStarts[i], sSpanEnds[i], 222 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 223 } 224 return result; 225 } 226 227 @Test 228 public void testNoLineBreak() { 229 // Width lower than WIDTH 230 layout("", NO_BREAK); 231 layout("I", NO_BREAK); 232 layout("V", NO_BREAK); 233 layout("X", NO_BREAK); 234 layout("L", NO_BREAK); 235 layout("I VILI", NO_BREAK); 236 layout("XXXX", NO_BREAK); 237 layout("LXXXX", NO_BREAK); 238 239 // Width equal to WIDTH 240 layout("C", NO_BREAK); 241 layout("LL", NO_BREAK); 242 layout("L XXXX", NO_BREAK); 243 layout("XXXXXXXXXX", NO_BREAK); 244 layout("XXX XXXXXX", NO_BREAK); 245 layout("XXX XXXX X", NO_BREAK); 246 layout("XXX XXXXX ", NO_BREAK); 247 layout(" XXXXXXXX ", NO_BREAK); 248 layout(" XX XXX ", NO_BREAK); 249 // 0123456789 250 251 // Width greater than WIDTH, but no break 252 layout(" XX XXX ", NO_BREAK); 253 layout("XX XXX XXX ", NO_BREAK); 254 layout("XX XXX XXX ", NO_BREAK); 255 layout("XXXXXXXXXX ", NO_BREAK); 256 // 01234567890 257 } 258 259 @Test 260 public void testOneLineBreak() { 261 // 01234567890 262 layout("XX XXX XXXX", new int[] {7}); 263 layout("XX XXXX XXX", new int[] {8}); 264 layout("XX XXXXX XX", new int[] {9}); 265 layout("XX XXXXXX X", new int[] {10}); 266 // 01234567890 267 layout("XXXXXXXXXXX", new int[] {10}); 268 layout("XXXXXXXXX X", new int[] {10}); 269 layout("XXXXXXXX XX", new int[] {9}); 270 layout("XXXXXXX XXX", new int[] {8}); 271 layout("XXXXXX XXXX", new int[] {7}); 272 // 01234567890 273 layout("LL LL", new int[] {3}); 274 layout("LLLL", new int[] {2}); 275 layout("C C", new int[] {2}); 276 layout("CC", new int[] {1}); 277 } 278 279 @Test 280 public void testSpaceAtBreak() { 281 // 0123456789012 282 layout("XXXX XXXXX X", new int[] {11}); 283 layout("XXXXXXXXXX X", new int[] {11}); 284 layout("XXXXXXXXXV X", new int[] {11}); 285 layout("C X", new int[] {2}); 286 } 287 288 @Test 289 public void testMultipleSpacesAtBreak() { 290 // 0123456789012 291 layout("LXX XXXX", new int[] {4}); 292 layout("LXX XXXX", new int[] {5}); 293 layout("LXX XXXX", new int[] {6}); 294 layout("LXX XXXX", new int[] {7}); 295 layout("LXX XXXX", new int[] {8}); 296 } 297 298 @Test 299 public void testZeroWidthCharacters() { 300 // 0123456789012345678901234 301 layout("X_X_X_X_X_X_X_X_X_X", NO_BREAK); 302 layout("___X_X_X_X_X_X_X_X_X_X___", NO_BREAK); 303 layout("C_X", new int[] {2}); 304 layout("C__X", new int[] {3}); 305 } 306 307 /** 308 * Note that when the text has spans, StaticLayout does not use the provided TextPaint to 309 * measure text runs anymore. This is probably a bug. 310 * To be able to use the fake sTextPaint and make this test pass, use mPaint instead of 311 * mWorkPaint in MeasuredText#addStyleRun 312 */ 313 @Test 314 public void testWithSpans() { 315 if (!SPAN_TESTS_SUPPORTED) return; 316 317 layout(spanify("<012 456 89>"), NO_BREAK); 318 layout(spanify("012 <456> 89"), NO_BREAK); 319 layout(spanify("<012> <456>< 89>"), NO_BREAK); 320 layout(spanify("<012> <456> <89>"), NO_BREAK); 321 322 layout(spanify("<012> <456> <89>012"), new int[] {8}); 323 layout(spanify("<012> <456> 89<012>"), new int[] {8}); 324 layout(spanify("<012> <456> <89><012>"), new int[] {8}); 325 layout(spanify("<012> <456> 89 <123>"), new int[] {11}); 326 layout(spanify("<012> <456> 89< 123>"), new int[] {11}); 327 layout(spanify("<012> <456> <89> <123>"), new int[] {11}); 328 layout(spanify("012 456 89 <LXX> XX XX"), new int[] {11, 18}); 329 } 330 331 /* 332 * Adding a span to the string should not change the layout, since the metrics are unchanged. 333 */ 334 @Test 335 public void testWithOneSpan() { 336 if (!SPAN_TESTS_SUPPORTED) return; 337 338 String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", 339 "012 456 89012 456 89012", "0123456789012" }; 340 341 MetricAffectingSpan metricAffectingSpan = getMetricAffectingSpan(); 342 343 for (String text : texts) { 344 // Get the line breaks without any span 345 int[] breaks = getBreaks(text); 346 347 // Add spans on all possible offsets 348 for (int spanStart = 0; spanStart < text.length(); spanStart++) { 349 for (int spanEnd = spanStart; spanEnd < text.length(); spanEnd++) { 350 SpannableStringBuilder ssb = new SpannableStringBuilder(text); 351 ssb.setSpan(metricAffectingSpan, spanStart, spanEnd, 352 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 353 layout(ssb, breaks); 354 } 355 } 356 } 357 } 358 359 @Test 360 public void testWithTwoSpans() { 361 if (!SPAN_TESTS_SUPPORTED) return; 362 363 String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", 364 "012 456 89012 456 89012", "0123456789012" }; 365 366 MetricAffectingSpan metricAffectingSpan1 = getMetricAffectingSpan(); 367 MetricAffectingSpan metricAffectingSpan2 = getMetricAffectingSpan(); 368 369 for (String text : texts) { 370 // Get the line breaks without any span 371 int[] breaks = getBreaks(text); 372 373 // Add spans on all possible offsets 374 for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) { 375 for (int spanEnd1 = spanStart1; spanEnd1 < text.length(); spanEnd1++) { 376 SpannableStringBuilder ssb = new SpannableStringBuilder(text); 377 ssb.setSpan(metricAffectingSpan1, spanStart1, spanEnd1, 378 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 379 380 for (int spanStart2 = 0; spanStart2 < text.length(); spanStart2++) { 381 for (int spanEnd2 = spanStart2; spanEnd2 < text.length(); spanEnd2++) { 382 ssb.setSpan(metricAffectingSpan2, spanStart2, spanEnd2, 383 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 384 layout(ssb, breaks); 385 } 386 } 387 } 388 } 389 } 390 } 391 392 public static String replace(String string, char c, char r) { 393 return string.replaceAll(String.valueOf(c), String.valueOf(r)); 394 } 395 396 @Test 397 public void testWithSurrogate() { 398 layout("LX" + SURR_FIRST + SURR_SECOND, NO_BREAK); 399 layout("LXXXX" + SURR_FIRST + SURR_SECOND, NO_BREAK); 400 // LXXXXI (91) + SURR_FIRST + SURR_SECOND (10). Do not break in the middle point of 401 // surrogatge pair. 402 layout("LXXXXI" + SURR_FIRST + SURR_SECOND, new int[] {6}); 403 404 // LXXXXI (91) + SURR_SECOND (replaced with REPLACEMENT CHARACTER. width is 7px) fits. 405 // Break just after invalid trailing surrogate. 406 layout("LXXXXI" + SURR_SECOND + SURR_FIRST, new int[] {7}); 407 408 layout("C" + SURR_FIRST + SURR_SECOND, new int[] {1}); 409 } 410 411 @Test 412 public void testNarrowWidth() { 413 int[] widths = new int[] { 0, 4, 10 }; 414 String[] texts = new String[] { "", "X", " ", "XX", " X", "XXX" }; 415 416 for (String text: texts) { 417 // 15 is such that only one character will fit 418 int[] breaks = getBreaks(text, 15, Layout.BREAK_STRATEGY_SIMPLE); 419 420 // Width under 15 should all lead to the same line break 421 for (int width: widths) { 422 layout(text, breaks, width); 423 } 424 } 425 } 426 427 @Test 428 public void testNarrowWidthZeroWidth() { 429 int[] widths = new int[] { 1, 4 }; 430 for (int width: widths) { 431 layout("X.", new int[] {1}, width); 432 layout("X__", NO_BREAK, width); 433 layout("X__X", new int[] {3}, width); 434 layout("X__X_", new int[] {3}, width); 435 436 layout("_", NO_BREAK, width); 437 layout("__", NO_BREAK, width); 438 439 // TODO: The line breaking algorithms break the line too frequently in the presence of 440 // zero-width characters. The following cases document how line-breaking should behave 441 // in some cases, where the current implementation does not seem reasonable. (Breaking 442 // between a zero-width character that start the line and a character with positive 443 // width does not make sense.) Line-breaking should be fixed so that all the following 444 // tests end up on one line, with no breaks. 445 // layout("_X", NO_BREAK, width); 446 // layout("_X_", NO_BREAK, width); 447 // layout("__X__", NO_BREAK, width); 448 } 449 } 450 451 @Test 452 public void testMaxLines() { 453 layoutMaxLines("C", NO_BREAK, 1); 454 layoutMaxLines("C C", new int[] {2}, 1); 455 layoutMaxLines("C C", new int[] {2}, 2); 456 layoutMaxLines("CC", new int[] {1}, 1); 457 layoutMaxLines("CC", new int[] {1}, 2); 458 } 459 } 460