1 /* 2 * Copyright (C) 2008 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 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assert.fail; 24 import static org.mockito.Matchers.anyInt; 25 import static org.mockito.Mockito.any; 26 import static org.mockito.Mockito.mock; 27 import static org.mockito.Mockito.when; 28 29 import android.content.Context; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Paint; 33 import android.graphics.Paint.FontMetricsInt; 34 import android.graphics.Typeface; 35 import android.os.LocaleList; 36 import android.text.Editable; 37 import android.text.Layout; 38 import android.text.Layout.Alignment; 39 import android.text.PrecomputedText; 40 import android.text.SpannableString; 41 import android.text.SpannableStringBuilder; 42 import android.text.Spanned; 43 import android.text.SpannedString; 44 import android.text.StaticLayout; 45 import android.text.TextDirectionHeuristic; 46 import android.text.TextDirectionHeuristics; 47 import android.text.TextPaint; 48 import android.text.TextUtils; 49 import android.text.TextUtils.TruncateAt; 50 import android.text.method.cts.EditorState; 51 import android.text.style.LineBackgroundSpan; 52 import android.text.style.LineHeightSpan; 53 import android.text.style.ReplacementSpan; 54 import android.text.style.StyleSpan; 55 import android.text.style.TextAppearanceSpan; 56 57 import androidx.test.InstrumentationRegistry; 58 import androidx.test.filters.SmallTest; 59 import androidx.test.runner.AndroidJUnit4; 60 61 import org.junit.Before; 62 import org.junit.Test; 63 import org.junit.runner.RunWith; 64 import org.mockito.ArgumentCaptor; 65 66 import java.text.Normalizer; 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.Locale; 70 71 @SmallTest 72 @RunWith(AndroidJUnit4.class) 73 public class StaticLayoutTest { 74 private static final float SPACE_MULTI = 1.0f; 75 private static final float SPACE_ADD = 0.0f; 76 private static final int DEFAULT_OUTER_WIDTH = 150; 77 78 private static final int LAST_LINE = 5; 79 private static final int LINE_COUNT = 6; 80 private static final int LARGER_THAN_LINE_COUNT = 50; 81 82 private static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing " 83 + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad " 84 + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " 85 + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse " 86 + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " 87 + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 88 89 /* the first line must have one tab. the others not. totally 6 lines 90 */ 91 private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar" 92 + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong"; 93 94 private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence"; 95 96 private static final int VERTICAL_BELOW_TEXT = 1000; 97 98 private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER; 99 100 private static final int ELLIPSIZE_WIDTH = 8; 101 102 private StaticLayout mDefaultLayout; 103 private TextPaint mDefaultPaint; 104 105 private static class TestingTextPaint extends TextPaint { 106 // need to have a subclass to ensure measurement happens in Java and not C++ 107 } 108 109 @Before 110 public void setup() { 111 mDefaultPaint = new TextPaint(); 112 mDefaultLayout = createDefaultStaticLayout(); 113 } 114 115 private StaticLayout createDefaultStaticLayout() { 116 return new StaticLayout(LAYOUT_TEXT, mDefaultPaint, 117 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 118 } 119 120 private StaticLayout createEllipsizeStaticLayout() { 121 return new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint, 122 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true, 123 TextUtils.TruncateAt.MIDDLE, ELLIPSIZE_WIDTH); 124 } 125 126 private StaticLayout createEllipsizeStaticLayout(CharSequence text, 127 TextUtils.TruncateAt ellipsize) { 128 return new StaticLayout(text, 0, text.length(), 129 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, 130 SPACE_MULTI, SPACE_ADD, true /* include pad */, 131 ellipsize, 132 ELLIPSIZE_WIDTH); 133 } 134 135 /** 136 * Constructor test 137 */ 138 @Test 139 public void testConstructor() { 140 new StaticLayout(LAYOUT_TEXT, mDefaultPaint, DEFAULT_OUTER_WIDTH, 141 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 142 143 new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint, 144 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 145 146 new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint, 147 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, false, null, 0); 148 } 149 150 @Test(expected=NullPointerException.class) 151 public void testConstructorNull() { 152 new StaticLayout(null, null, -1, null, 0, 0, true); 153 } 154 155 @Test 156 public void testBuilder() { 157 { 158 // Obtain. 159 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 160 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 161 StaticLayout layout = builder.build(); 162 // Check values passed to obtain(). 163 assertEquals(LAYOUT_TEXT, layout.getText()); 164 assertEquals(mDefaultPaint, layout.getPaint()); 165 assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth()); 166 // Check default values. 167 assertEquals(Alignment.ALIGN_NORMAL, layout.getAlignment()); 168 assertEquals(0.0f, layout.getSpacingAdd(), 0.0f); 169 assertEquals(1.0f, layout.getSpacingMultiplier(), 0.0f); 170 assertEquals(DEFAULT_OUTER_WIDTH, layout.getEllipsizedWidth()); 171 } 172 { 173 // Obtain with null objects. 174 StaticLayout.Builder builder = StaticLayout.Builder.obtain(null, 0, 0, null, 0); 175 try { 176 StaticLayout layout = builder.build(); 177 fail("should throw NullPointerException here"); 178 } catch (NullPointerException e) { 179 } 180 } 181 { 182 // setText. 183 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 184 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 185 builder.setText(LAYOUT_TEXT_SINGLE_LINE); 186 StaticLayout layout = builder.build(); 187 assertEquals(LAYOUT_TEXT_SINGLE_LINE, layout.getText()); 188 } 189 { 190 // setAlignment. 191 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 192 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 193 builder.setAlignment(DEFAULT_ALIGN); 194 StaticLayout layout = builder.build(); 195 assertEquals(DEFAULT_ALIGN, layout.getAlignment()); 196 } 197 { 198 // setLineSpacing. 199 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 200 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 201 builder.setLineSpacing(1.0f, 2.0f); 202 StaticLayout layout = builder.build(); 203 assertEquals(1.0f, layout.getSpacingAdd(), 0.0f); 204 assertEquals(2.0f, layout.getSpacingMultiplier(), 0.0f); 205 } 206 { 207 // setEllipsizedWidth and setEllipsize. 208 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 209 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 210 builder.setEllipsize(TruncateAt.END); 211 builder.setEllipsizedWidth(ELLIPSIZE_WIDTH); 212 StaticLayout layout = builder.build(); 213 assertEquals(ELLIPSIZE_WIDTH, layout.getEllipsizedWidth()); 214 assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth()); 215 assertTrue(layout.getEllipsisCount(0) == 0); 216 assertTrue(layout.getEllipsisCount(5) > 0); 217 } 218 { 219 // setMaxLines. 220 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 221 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 222 builder.setMaxLines(1); 223 builder.setEllipsize(TruncateAt.END); 224 StaticLayout layout = builder.build(); 225 assertTrue(layout.getEllipsisCount(0) > 0); 226 assertEquals(1, layout.getLineCount()); 227 } 228 { 229 // Setter methods that cannot be directly tested. 230 // setBreakStrategy, setHyphenationFrequency, setIncludePad, and setIndents. 231 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 232 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 233 builder.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY); 234 builder.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); 235 builder.setIncludePad(true); 236 builder.setIndents(null, null); 237 StaticLayout layout = builder.build(); 238 assertNotNull(layout); 239 } 240 } 241 242 @Test 243 public void testSetLineSpacing_whereLineEndsWithNextLine() { 244 final float spacingAdd = 10f; 245 final float spacingMult = 3f; 246 247 // two lines of text, with line spacing, first line will have the spacing, but last line 248 // wont have the spacing 249 final String tmpText = "a\nb"; 250 StaticLayout.Builder builder = StaticLayout.Builder.obtain(tmpText, 0, tmpText.length(), 251 mDefaultPaint, DEFAULT_OUTER_WIDTH); 252 builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false); 253 final StaticLayout comparisonLayout = builder.build(); 254 255 assertEquals(2, comparisonLayout.getLineCount()); 256 final int heightWithLineSpacing = comparisonLayout.getLineBottom(0) 257 - comparisonLayout.getLineTop(0); 258 final int heightWithoutLineSpacing = comparisonLayout.getLineBottom(1) 259 - comparisonLayout.getLineTop(1); 260 assertTrue(heightWithLineSpacing > heightWithoutLineSpacing); 261 262 final String text = "a\n"; 263 // build the layout to be tested 264 builder = StaticLayout.Builder.obtain("a\n", 0, text.length(), mDefaultPaint, 265 DEFAULT_OUTER_WIDTH); 266 builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false); 267 final StaticLayout layout = builder.build(); 268 269 assertEquals(comparisonLayout.getLineCount(), layout.getLineCount()); 270 assertEquals(heightWithLineSpacing, layout.getLineBottom(0) - layout.getLineTop(0)); 271 assertEquals(heightWithoutLineSpacing, layout.getLineBottom(1) - layout.getLineTop(1)); 272 } 273 274 @Test 275 public void testBuilder_setJustificationMode() { 276 StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 277 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 278 builder.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD); 279 StaticLayout layout = builder.build(); 280 // Hard to expect the justification result. Just make sure the final layout is created 281 // without causing any exceptions. 282 assertNotNull(layout); 283 } 284 285 /* 286 * Get the line number corresponding to the specified vertical position. 287 * If you ask for a position above 0, you get 0. above 0 means pixel above the fire line 288 * if you ask for a position in the range of the height, return the pixel in line 289 * if you ask for a position below the bottom of the text, you get the last line. 290 * Test 4 values containing -1, 0, normal number and > count 291 */ 292 @Test 293 public void testGetLineForVertical() { 294 assertEquals(0, mDefaultLayout.getLineForVertical(-1)); 295 assertEquals(0, mDefaultLayout.getLineForVertical(0)); 296 assertTrue(mDefaultLayout.getLineForVertical(50) > 0); 297 assertEquals(LAST_LINE, mDefaultLayout.getLineForVertical(VERTICAL_BELOW_TEXT)); 298 } 299 300 /** 301 * Return the number of lines of text in this layout. 302 */ 303 @Test 304 public void testGetLineCount() { 305 assertEquals(LINE_COUNT, mDefaultLayout.getLineCount()); 306 } 307 308 /* 309 * Return the vertical position of the top of the specified line. 310 * If the specified line is one beyond the last line, returns the bottom of the last line. 311 * A line of text contains top and bottom in height. this method just get the top of a line 312 * Test 4 values containing -1, 0, normal number and > count 313 */ 314 @Test 315 public void testGetLineTop() { 316 assertTrue(mDefaultLayout.getLineTop(0) >= 0); 317 assertTrue(mDefaultLayout.getLineTop(1) > mDefaultLayout.getLineTop(0)); 318 } 319 320 @Test(expected=ArrayIndexOutOfBoundsException.class) 321 public void testGetLineTopBeforeFirst() { 322 mDefaultLayout.getLineTop(-1); 323 } 324 325 @Test(expected=ArrayIndexOutOfBoundsException.class) 326 public void testGetLineTopAfterLast() { 327 mDefaultLayout.getLineTop(LARGER_THAN_LINE_COUNT ); 328 } 329 330 /** 331 * Return the descent of the specified line. 332 * This method just like getLineTop, descent means the bottom pixel of the line 333 * Test 4 values containing -1, 0, normal number and > count 334 */ 335 @Test 336 public void testGetLineDescent() { 337 assertTrue(mDefaultLayout.getLineDescent(0) > 0); 338 assertTrue(mDefaultLayout.getLineDescent(1) > 0); 339 } 340 341 @Test(expected=ArrayIndexOutOfBoundsException.class) 342 public void testGetLineDescentBeforeFirst() { 343 mDefaultLayout.getLineDescent(-1); 344 } 345 346 @Test(expected=ArrayIndexOutOfBoundsException.class) 347 public void testGetLineDescentAfterLast() { 348 mDefaultLayout.getLineDescent(LARGER_THAN_LINE_COUNT ); 349 } 350 351 /** 352 * Returns the primary directionality of the paragraph containing the specified line. 353 * By default, each line should be same 354 */ 355 @Test 356 public void testGetParagraphDirection() { 357 assertEquals(mDefaultLayout.getParagraphDirection(0), 358 mDefaultLayout.getParagraphDirection(1)); 359 } 360 361 @Test(expected=ArrayIndexOutOfBoundsException.class) 362 public void testGetParagraphDirectionBeforeFirst() { 363 mDefaultLayout.getParagraphDirection(-1); 364 } 365 366 @Test(expected=ArrayIndexOutOfBoundsException.class) 367 public void testGetParagraphDirectionAfterLast() { 368 mDefaultLayout.getParagraphDirection(LARGER_THAN_LINE_COUNT ); 369 } 370 371 /** 372 * Return the text offset of the beginning of the specified line. 373 * If the specified line is one beyond the last line, returns the end of the last line. 374 * Test 4 values containing -1, 0, normal number and > count 375 * Each line's offset must >= 0 376 */ 377 @Test 378 public void testGetLineStart() { 379 assertTrue(mDefaultLayout.getLineStart(0) >= 0); 380 assertTrue(mDefaultLayout.getLineStart(1) >= 0); 381 } 382 383 @Test(expected=ArrayIndexOutOfBoundsException.class) 384 public void testGetLineStartBeforeFirst() { 385 mDefaultLayout.getLineStart(-1); 386 } 387 388 @Test(expected=ArrayIndexOutOfBoundsException.class) 389 public void testGetLineStartAfterLast() { 390 mDefaultLayout.getLineStart(LARGER_THAN_LINE_COUNT ); 391 } 392 393 /* 394 * Returns whether the specified line contains one or more tabs. 395 */ 396 @Test 397 public void testGetContainsTab() { 398 assertTrue(mDefaultLayout.getLineContainsTab(0)); 399 assertFalse(mDefaultLayout.getLineContainsTab(1)); 400 } 401 402 @Test(expected=ArrayIndexOutOfBoundsException.class) 403 public void testGetContainsTabBeforeFirst() { 404 mDefaultLayout.getLineContainsTab(-1); 405 } 406 407 @Test(expected=ArrayIndexOutOfBoundsException.class) 408 public void testGetContainsTabAfterLast() { 409 mDefaultLayout.getLineContainsTab(LARGER_THAN_LINE_COUNT ); 410 } 411 412 /** 413 * Returns an array of directionalities for the specified line. 414 * The array alternates counts of characters in left-to-right 415 * and right-to-left segments of the line. 416 * We can not check the return value, for Directions's field is package private 417 * So only check it not null 418 */ 419 @Test 420 public void testGetLineDirections(){ 421 assertNotNull(mDefaultLayout.getLineDirections(0)); 422 assertNotNull(mDefaultLayout.getLineDirections(1)); 423 } 424 425 @Test(expected = ArrayIndexOutOfBoundsException.class) 426 public void testGetLineDirectionsBeforeFirst() { 427 mDefaultLayout.getLineDirections(-1); 428 } 429 430 @Test(expected = ArrayIndexOutOfBoundsException.class) 431 public void testGetLineDirectionsAfterLast() { 432 mDefaultLayout.getLineDirections(LARGER_THAN_LINE_COUNT); 433 } 434 435 /** 436 * Returns the (negative) number of extra pixels of ascent padding 437 * in the top line of the Layout. 438 */ 439 @Test 440 public void testGetTopPadding() { 441 assertTrue(mDefaultLayout.getTopPadding() < 0); 442 } 443 444 /** 445 * Returns the number of extra pixels of descent padding in the bottom line of the Layout. 446 */ 447 @Test 448 public void testGetBottomPadding() { 449 assertTrue(mDefaultLayout.getBottomPadding() > 0); 450 } 451 452 /* 453 * Returns the number of characters to be ellipsized away, or 0 if no ellipsis is to take place. 454 * So each line must >= 0 455 */ 456 @Test 457 public void testGetEllipsisCount() { 458 // Multilines (6 lines) and TruncateAt.START so no ellipsis at all 459 mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, 460 TextUtils.TruncateAt.MIDDLE); 461 462 assertTrue(mDefaultLayout.getEllipsisCount(0) == 0); 463 assertTrue(mDefaultLayout.getEllipsisCount(1) == 0); 464 assertTrue(mDefaultLayout.getEllipsisCount(2) == 0); 465 assertTrue(mDefaultLayout.getEllipsisCount(3) == 0); 466 assertTrue(mDefaultLayout.getEllipsisCount(4) == 0); 467 assertTrue(mDefaultLayout.getEllipsisCount(5) == 0); 468 469 try { 470 mDefaultLayout.getEllipsisCount(-1); 471 fail("should throw ArrayIndexOutOfBoundsException"); 472 } catch (ArrayIndexOutOfBoundsException e) { 473 } 474 475 try { 476 mDefaultLayout.getEllipsisCount(LARGER_THAN_LINE_COUNT); 477 fail("should throw ArrayIndexOutOfBoundsException"); 478 } catch (ArrayIndexOutOfBoundsException e) { 479 } 480 481 // Multilines (6 lines) and TruncateAt.MIDDLE so no ellipsis at all 482 mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, 483 TextUtils.TruncateAt.MIDDLE); 484 485 assertTrue(mDefaultLayout.getEllipsisCount(0) == 0); 486 assertTrue(mDefaultLayout.getEllipsisCount(1) == 0); 487 assertTrue(mDefaultLayout.getEllipsisCount(2) == 0); 488 assertTrue(mDefaultLayout.getEllipsisCount(3) == 0); 489 assertTrue(mDefaultLayout.getEllipsisCount(4) == 0); 490 assertTrue(mDefaultLayout.getEllipsisCount(5) == 0); 491 492 // Multilines (6 lines) and TruncateAt.END so ellipsis only on the last line 493 mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, 494 TextUtils.TruncateAt.END); 495 496 assertTrue(mDefaultLayout.getEllipsisCount(0) == 0); 497 assertTrue(mDefaultLayout.getEllipsisCount(1) == 0); 498 assertTrue(mDefaultLayout.getEllipsisCount(2) == 0); 499 assertTrue(mDefaultLayout.getEllipsisCount(3) == 0); 500 assertTrue(mDefaultLayout.getEllipsisCount(4) == 0); 501 assertTrue(mDefaultLayout.getEllipsisCount(5) > 0); 502 503 // Multilines (6 lines) and TruncateAt.MARQUEE so ellipsis only on the last line 504 mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, 505 TextUtils.TruncateAt.END); 506 507 assertTrue(mDefaultLayout.getEllipsisCount(0) == 0); 508 assertTrue(mDefaultLayout.getEllipsisCount(1) == 0); 509 assertTrue(mDefaultLayout.getEllipsisCount(2) == 0); 510 assertTrue(mDefaultLayout.getEllipsisCount(3) == 0); 511 assertTrue(mDefaultLayout.getEllipsisCount(4) == 0); 512 assertTrue(mDefaultLayout.getEllipsisCount(5) > 0); 513 } 514 515 /* 516 * Return the offset of the first character to be ellipsized away 517 * relative to the start of the line. 518 * (So 0 if the beginning of the line is ellipsized, not getLineStart().) 519 */ 520 @Test 521 public void testGetEllipsisStart() { 522 mDefaultLayout = createEllipsizeStaticLayout(); 523 assertTrue(mDefaultLayout.getEllipsisStart(0) >= 0); 524 assertTrue(mDefaultLayout.getEllipsisStart(1) >= 0); 525 526 try { 527 mDefaultLayout.getEllipsisStart(-1); 528 fail("should throw ArrayIndexOutOfBoundsException"); 529 } catch (ArrayIndexOutOfBoundsException e) { 530 } 531 532 try { 533 mDefaultLayout.getEllipsisStart(LARGER_THAN_LINE_COUNT); 534 fail("should throw ArrayIndexOutOfBoundsException"); 535 } catch (ArrayIndexOutOfBoundsException e) { 536 } 537 } 538 539 /* 540 * Return the width to which this Layout is ellipsizing 541 * or getWidth() if it is not doing anything special. 542 * The constructor's Argument TextUtils.TruncateAt defines which EllipsizedWidth to use 543 * ellipsizedWidth if argument is not null 544 * outerWidth if argument is null 545 */ 546 @Test 547 public void testGetEllipsizedWidth() { 548 int ellipsizedWidth = 60; 549 int outerWidth = 100; 550 StaticLayout layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), 551 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI, 552 SPACE_ADD, false, TextUtils.TruncateAt.END, ellipsizedWidth); 553 assertEquals(ellipsizedWidth, layout.getEllipsizedWidth()); 554 555 layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), 556 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, 557 false, null, ellipsizedWidth); 558 assertEquals(outerWidth, layout.getEllipsizedWidth()); 559 } 560 561 /** 562 * scenario description: 563 * 1. set the text. 564 * 2. change the text 565 * 3. Check the text won't change to the StaticLayout 566 */ 567 @Test 568 public void testImmutableStaticLayout() { 569 Editable editable = Editable.Factory.getInstance().newEditable("123\t\n555"); 570 StaticLayout layout = new StaticLayout(editable, mDefaultPaint, 571 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 572 573 assertEquals(2, layout.getLineCount()); 574 assertTrue(mDefaultLayout.getLineContainsTab(0)); 575 576 // change the text 577 editable.delete(0, editable.length() - 1); 578 579 assertEquals(2, layout.getLineCount()); 580 assertTrue(layout.getLineContainsTab(0)); 581 582 } 583 584 // String wrapper for testing not well known implementation of CharSequence. 585 private class FakeCharSequence implements CharSequence { 586 private String mStr; 587 588 public FakeCharSequence(String str) { 589 mStr = str; 590 } 591 592 @Override 593 public char charAt(int index) { 594 return mStr.charAt(index); 595 } 596 597 @Override 598 public int length() { 599 return mStr.length(); 600 } 601 602 @Override 603 public CharSequence subSequence(int start, int end) { 604 return mStr.subSequence(start, end); 605 } 606 607 @Override 608 public String toString() { 609 return mStr; 610 } 611 }; 612 613 private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) { 614 List<CharSequence> result = new ArrayList<>(); 615 616 List<String> normalizedStrings = new ArrayList<>(); 617 for (Normalizer.Form form: forms) { 618 normalizedStrings.add(Normalizer.normalize(testString, form)); 619 } 620 621 for (String str: normalizedStrings) { 622 result.add(str); 623 result.add(new SpannedString(str)); 624 result.add(new SpannableString(str)); 625 result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation. 626 result.add(new FakeCharSequence(str)); // as a not well known implementation. 627 } 628 return result; 629 } 630 631 private String buildTestMessage(CharSequence seq) { 632 String normalized; 633 if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) { 634 normalized = "NFC"; 635 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) { 636 normalized = "NFD"; 637 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) { 638 normalized = "NFKC"; 639 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) { 640 normalized = "NFKD"; 641 } else { 642 throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD"); 643 } 644 645 StringBuilder builder = new StringBuilder(); 646 for (int i = 0; i < seq.length(); ++i) { 647 builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i)))); 648 } 649 650 return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" + 651 ", class: " + seq.getClass().getName() + 652 ", Normalization: " + normalized; 653 } 654 655 @Test 656 public void testGetOffset_ASCII() { 657 String testStrings[] = { "abcde", "ab\ncd", "ab\tcd", "ab\n\nc", "ab\n\tc" }; 658 659 for (String testString: testStrings) { 660 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 661 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 662 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 663 664 String testLabel = buildTestMessage(seq); 665 666 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 667 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 668 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 669 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 670 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4)); 671 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 672 673 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 674 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 675 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2)); 676 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 677 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4)); 678 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5)); 679 } 680 } 681 682 String testString = "ab\r\nde"; 683 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 684 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 685 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 686 687 String testLabel = buildTestMessage(seq); 688 689 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 690 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 691 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 692 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 693 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4)); 694 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 695 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6)); 696 697 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 698 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 699 assertEquals(testLabel, 4, layout.getOffsetToRightOf(2)); 700 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 701 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4)); 702 assertEquals(testLabel, 6, layout.getOffsetToRightOf(5)); 703 assertEquals(testLabel, 6, layout.getOffsetToRightOf(6)); 704 } 705 } 706 707 @Test 708 public void testGetOffset_UNICODE() { 709 String testStrings[] = new String[] { 710 // Cyrillic alphabets. 711 "\u0410\u0411\u0412\u0413\u0414", 712 // Japanese Hiragana Characters. 713 "\u3042\u3044\u3046\u3048\u304A", 714 }; 715 716 for (String testString: testStrings) { 717 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 718 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 719 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 720 721 String testLabel = buildTestMessage(seq); 722 723 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 724 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 725 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 726 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 727 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4)); 728 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 729 730 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 731 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 732 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2)); 733 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 734 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4)); 735 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5)); 736 } 737 } 738 } 739 740 @Test 741 public void testGetOffset_UNICODE_Normalization() { 742 // "A" with acute, circumflex, tilde, diaeresis, ring above. 743 String testString = "\u00C1\u00C2\u00C3\u00C4\u00C5"; 744 Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC }; 745 for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) { 746 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 747 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 748 749 String testLabel = buildTestMessage(seq); 750 751 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 752 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 753 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 754 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 755 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4)); 756 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 757 758 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 759 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 760 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2)); 761 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 762 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4)); 763 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5)); 764 } 765 766 Normalizer.Form[] twoUnicodeForms = { Normalizer.Form.NFD, Normalizer.Form.NFKD }; 767 for (CharSequence seq: buildTestCharSequences(testString, twoUnicodeForms)) { 768 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 769 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 770 771 String testLabel = buildTestMessage(seq); 772 773 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 774 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 775 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2)); 776 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 777 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4)); 778 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 779 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6)); 780 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7)); 781 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8)); 782 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9)); 783 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10)); 784 785 assertEquals(testLabel, 2, layout.getOffsetToRightOf(0)); 786 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 787 assertEquals(testLabel, 4, layout.getOffsetToRightOf(2)); 788 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 789 assertEquals(testLabel, 6, layout.getOffsetToRightOf(4)); 790 assertEquals(testLabel, 6, layout.getOffsetToRightOf(5)); 791 assertEquals(testLabel, 8, layout.getOffsetToRightOf(6)); 792 assertEquals(testLabel, 8, layout.getOffsetToRightOf(7)); 793 assertEquals(testLabel, 10, layout.getOffsetToRightOf(8)); 794 assertEquals(testLabel, 10, layout.getOffsetToRightOf(9)); 795 assertEquals(testLabel, 10, layout.getOffsetToRightOf(10)); 796 } 797 } 798 799 @Test 800 public void testGetOffset_UNICODE_SurrogatePairs() { 801 // Emoticons for surrogate pairs tests. 802 String testString = 803 "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04"; 804 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 805 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 806 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 807 808 String testLabel = buildTestMessage(seq); 809 810 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 811 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 812 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2)); 813 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 814 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4)); 815 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5)); 816 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6)); 817 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7)); 818 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8)); 819 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9)); 820 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10)); 821 822 assertEquals(testLabel, 2, layout.getOffsetToRightOf(0)); 823 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 824 assertEquals(testLabel, 4, layout.getOffsetToRightOf(2)); 825 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 826 assertEquals(testLabel, 6, layout.getOffsetToRightOf(4)); 827 assertEquals(testLabel, 6, layout.getOffsetToRightOf(5)); 828 assertEquals(testLabel, 8, layout.getOffsetToRightOf(6)); 829 assertEquals(testLabel, 8, layout.getOffsetToRightOf(7)); 830 assertEquals(testLabel, 10, layout.getOffsetToRightOf(8)); 831 assertEquals(testLabel, 10, layout.getOffsetToRightOf(9)); 832 assertEquals(testLabel, 10, layout.getOffsetToRightOf(10)); 833 } 834 } 835 836 @Test 837 public void testGetOffset_UNICODE_Thai() { 838 // Thai Characters. The expected cursorable boundary is 839 // | \u0E02 | \u0E2D | \u0E1A | \u0E04\u0E38 | \u0E13 | 840 String testString = "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13"; 841 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 842 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 843 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 844 845 String testLabel = buildTestMessage(seq); 846 847 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 848 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 849 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 850 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3)); 851 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4)); 852 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5)); 853 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6)); 854 855 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 856 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 857 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2)); 858 assertEquals(testLabel, 5, layout.getOffsetToRightOf(3)); 859 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4)); 860 assertEquals(testLabel, 6, layout.getOffsetToRightOf(5)); 861 assertEquals(testLabel, 6, layout.getOffsetToRightOf(6)); 862 } 863 } 864 865 @Test 866 public void testGetOffset_UNICODE_Arabic() { 867 // Arabic Characters. The expected cursorable boundary is 868 // | \u0623 \u064F | \u0633 \u0652 | \u0631 \u064E | \u0629 \u064C |"; 869 String testString = "\u0623\u064F\u0633\u0652\u0631\u064E\u0629\u064C"; 870 871 Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC }; 872 for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) { 873 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 874 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 875 876 String testLabel = buildTestMessage(seq); 877 878 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(0)); 879 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1)); 880 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2)); 881 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3)); 882 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(4)); 883 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5)); 884 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(6)); 885 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(7)); 886 assertEquals(testLabel, 8, layout.getOffsetToLeftOf(8)); 887 888 assertEquals(testLabel, 0, layout.getOffsetToRightOf(0)); 889 assertEquals(testLabel, 0, layout.getOffsetToRightOf(1)); 890 assertEquals(testLabel, 0, layout.getOffsetToRightOf(2)); 891 assertEquals(testLabel, 2, layout.getOffsetToRightOf(3)); 892 assertEquals(testLabel, 2, layout.getOffsetToRightOf(4)); 893 assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); 894 assertEquals(testLabel, 4, layout.getOffsetToRightOf(6)); 895 assertEquals(testLabel, 6, layout.getOffsetToRightOf(7)); 896 assertEquals(testLabel, 6, layout.getOffsetToRightOf(8)); 897 } 898 } 899 900 @Test 901 public void testGetOffset_UNICODE_Bidi() { 902 // String having RTL characters and LTR characters 903 904 // LTR Context 905 // The first and last two characters are LTR characters. 906 String testString = "\u0061\u0062\u05DE\u05E1\u05E2\u0063\u0064"; 907 // Logical order: [L1] [L2] [R1] [R2] [R3] [L3] [L4] 908 // 0 1 2 3 4 5 6 7 909 // Display order: [L1] [L2] [R3] [R2] [R1] [L3] [L4] 910 // 0 1 2 4 3 5 6 7 911 // [L?] means ?th LTR character and [R?] means ?th RTL character. 912 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 913 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 914 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 915 916 String testLabel = buildTestMessage(seq); 917 918 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0)); 919 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1)); 920 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2)); 921 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3)); 922 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4)); 923 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5)); 924 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6)); 925 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7)); 926 927 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0)); 928 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1)); 929 assertEquals(testLabel, 4, layout.getOffsetToRightOf(2)); 930 assertEquals(testLabel, 5, layout.getOffsetToRightOf(3)); 931 assertEquals(testLabel, 3, layout.getOffsetToRightOf(4)); 932 assertEquals(testLabel, 6, layout.getOffsetToRightOf(5)); 933 assertEquals(testLabel, 7, layout.getOffsetToRightOf(6)); 934 assertEquals(testLabel, 7, layout.getOffsetToRightOf(7)); 935 } 936 937 // RTL Context 938 // The first and last two characters are RTL characters. 939 String testString2 = "\u05DE\u05E1\u0063\u0064\u0065\u05DE\u05E1"; 940 // Logical order: [R1] [R2] [L1] [L2] [L3] [R3] [R4] 941 // 0 1 2 3 4 5 6 7 942 // Display order: [R4] [R3] [L1] [L2] [L3] [R2] [R1] 943 // 7 6 5 3 4 2 1 0 944 // [L?] means ?th LTR character and [R?] means ?th RTL character. 945 for (CharSequence seq: buildTestCharSequences(testString2, Normalizer.Form.values())) { 946 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 947 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 948 949 String testLabel = buildTestMessage(seq); 950 951 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0)); 952 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1)); 953 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2)); 954 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(3)); 955 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4)); 956 assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5)); 957 assertEquals(testLabel, 7, layout.getOffsetToLeftOf(6)); 958 assertEquals(testLabel, 7, layout.getOffsetToLeftOf(7)); 959 960 assertEquals(testLabel, 0, layout.getOffsetToRightOf(0)); 961 assertEquals(testLabel, 0, layout.getOffsetToRightOf(1)); 962 assertEquals(testLabel, 1, layout.getOffsetToRightOf(2)); 963 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3)); 964 assertEquals(testLabel, 2, layout.getOffsetToRightOf(4)); 965 assertEquals(testLabel, 3, layout.getOffsetToRightOf(5)); 966 assertEquals(testLabel, 5, layout.getOffsetToRightOf(6)); 967 assertEquals(testLabel, 6, layout.getOffsetToRightOf(7)); 968 } 969 } 970 971 private void moveCursorToRightCursorableOffset(EditorState state) { 972 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 973 StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(), 974 mDefaultPaint, DEFAULT_OUTER_WIDTH).build(); 975 final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart); 976 state.mSelectionStart = state.mSelectionEnd = newOffset; 977 } 978 979 private void moveCursorToLeftCursorableOffset(EditorState state) { 980 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 981 StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(), 982 mDefaultPaint, DEFAULT_OUTER_WIDTH).build(); 983 final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart); 984 state.mSelectionStart = state.mSelectionEnd = newOffset; 985 } 986 987 @Test 988 public void testGetOffset_Emoji() { 989 EditorState state = new EditorState(); 990 991 // Emojis 992 // U+00A9 is COPYRIGHT SIGN. 993 state.setByString("| U+00A9 U+00A9 U+00A9"); 994 moveCursorToRightCursorableOffset(state); 995 state.assertEquals("U+00A9 | U+00A9 U+00A9"); 996 moveCursorToRightCursorableOffset(state); 997 state.assertEquals("U+00A9 U+00A9 | U+00A9"); 998 moveCursorToRightCursorableOffset(state); 999 state.assertEquals("U+00A9 U+00A9 U+00A9 |"); 1000 moveCursorToRightCursorableOffset(state); 1001 state.assertEquals("U+00A9 U+00A9 U+00A9 |"); 1002 moveCursorToLeftCursorableOffset(state); 1003 state.assertEquals("U+00A9 U+00A9 | U+00A9"); 1004 moveCursorToLeftCursorableOffset(state); 1005 state.assertEquals("U+00A9 | U+00A9 U+00A9"); 1006 moveCursorToLeftCursorableOffset(state); 1007 state.assertEquals("| U+00A9 U+00A9 U+00A9"); 1008 moveCursorToLeftCursorableOffset(state); 1009 state.assertEquals("| U+00A9 U+00A9 U+00A9"); 1010 1011 // Surrogate pairs 1012 // U+1F468 is MAN. 1013 state.setByString("| U+1F468 U+1F468 U+1F468"); 1014 moveCursorToRightCursorableOffset(state); 1015 state.assertEquals("U+1F468 | U+1F468 U+1F468"); 1016 moveCursorToRightCursorableOffset(state); 1017 state.assertEquals("U+1F468 U+1F468 | U+1F468"); 1018 moveCursorToRightCursorableOffset(state); 1019 state.assertEquals("U+1F468 U+1F468 U+1F468 |"); 1020 moveCursorToRightCursorableOffset(state); 1021 state.assertEquals("U+1F468 U+1F468 U+1F468 |"); 1022 moveCursorToLeftCursorableOffset(state); 1023 state.assertEquals("U+1F468 U+1F468 | U+1F468"); 1024 moveCursorToLeftCursorableOffset(state); 1025 state.assertEquals("U+1F468 | U+1F468 U+1F468"); 1026 moveCursorToLeftCursorableOffset(state); 1027 state.assertEquals("| U+1F468 U+1F468 U+1F468"); 1028 moveCursorToLeftCursorableOffset(state); 1029 state.assertEquals("| U+1F468 U+1F468 U+1F468"); 1030 1031 // Keycaps 1032 // U+20E3 is COMBINING ENCLOSING KEYCAP. 1033 state.setByString("| '1' U+20E3 '1' U+20E3 '1' U+20E3"); 1034 moveCursorToRightCursorableOffset(state); 1035 state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3"); 1036 moveCursorToRightCursorableOffset(state); 1037 state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3"); 1038 moveCursorToRightCursorableOffset(state); 1039 state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |"); 1040 moveCursorToRightCursorableOffset(state); 1041 state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |"); 1042 moveCursorToLeftCursorableOffset(state); 1043 state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3"); 1044 moveCursorToLeftCursorableOffset(state); 1045 state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3"); 1046 moveCursorToLeftCursorableOffset(state); 1047 state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3"); 1048 moveCursorToLeftCursorableOffset(state); 1049 state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3"); 1050 1051 // Variation selectors 1052 // U+00A9 is COPYRIGHT SIGN, U+FE0E is VARIATION SELECTOR-15. U+FE0F is VARIATION 1053 // SELECTOR-16. 1054 state.setByString("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E"); 1055 moveCursorToRightCursorableOffset(state); 1056 state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E"); 1057 moveCursorToRightCursorableOffset(state); 1058 state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E"); 1059 moveCursorToRightCursorableOffset(state); 1060 state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |"); 1061 moveCursorToRightCursorableOffset(state); 1062 state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |"); 1063 moveCursorToLeftCursorableOffset(state); 1064 state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E"); 1065 moveCursorToLeftCursorableOffset(state); 1066 state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E"); 1067 moveCursorToLeftCursorableOffset(state); 1068 state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E"); 1069 moveCursorToLeftCursorableOffset(state); 1070 state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E"); 1071 1072 // Keycap + variation selector 1073 state.setByString("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3"); 1074 moveCursorToRightCursorableOffset(state); 1075 state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3"); 1076 moveCursorToRightCursorableOffset(state); 1077 state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3"); 1078 moveCursorToRightCursorableOffset(state); 1079 state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |"); 1080 moveCursorToRightCursorableOffset(state); 1081 state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |"); 1082 moveCursorToLeftCursorableOffset(state); 1083 state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3"); 1084 moveCursorToLeftCursorableOffset(state); 1085 state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3"); 1086 moveCursorToLeftCursorableOffset(state); 1087 state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3"); 1088 moveCursorToLeftCursorableOffset(state); 1089 state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3"); 1090 1091 // Flags 1092 // U+1F1E6 U+1F1E8 is Ascension Island flag. 1093 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8"); 1094 moveCursorToRightCursorableOffset(state); 1095 state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8"); 1096 moveCursorToRightCursorableOffset(state); 1097 state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8"); 1098 moveCursorToRightCursorableOffset(state); 1099 state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |"); 1100 moveCursorToRightCursorableOffset(state); 1101 state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |"); 1102 moveCursorToLeftCursorableOffset(state); 1103 state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8"); 1104 moveCursorToLeftCursorableOffset(state); 1105 state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8"); 1106 moveCursorToLeftCursorableOffset(state); 1107 state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8"); 1108 moveCursorToLeftCursorableOffset(state); 1109 state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8"); 1110 } 1111 1112 @Test 1113 public void testGetOffsetForHorizontal_Multilines() { 1114 // Emoticons for surrogate pairs tests. 1115 String testString = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04"; 1116 final float width = mDefaultPaint.measureText(testString, 0, 6); 1117 StaticLayout layout = new StaticLayout(testString, mDefaultPaint, (int)width, 1118 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 1119 // We expect the line break to be after the third emoticon, but we allow flexibility of the 1120 // line break algorithm as long as the break is within the string. These other cases might 1121 // happen if for example the font has kerning between emoticons. 1122 final int lineBreakOffset = layout.getOffsetForHorizontal(1, 0.0f); 1123 assertEquals(0, layout.getLineForOffset(lineBreakOffset - 1)); 1124 1125 assertEquals(0, layout.getOffsetForHorizontal(0, 0.0f)); 1126 assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width)); 1127 assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width * 2)); 1128 1129 final int lineCount = layout.getLineCount(); 1130 assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width)); 1131 assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width * 2)); 1132 } 1133 1134 @Test 1135 public void testIsRtlCharAt() { 1136 { 1137 String testString = "ab(\u0623\u0624)c\u0625"; 1138 StaticLayout layout = new StaticLayout(testString, mDefaultPaint, 1139 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 1140 1141 assertFalse(layout.isRtlCharAt(0)); 1142 assertFalse(layout.isRtlCharAt(1)); 1143 assertFalse(layout.isRtlCharAt(2)); 1144 assertTrue(layout.isRtlCharAt(3)); 1145 assertTrue(layout.isRtlCharAt(4)); 1146 assertFalse(layout.isRtlCharAt(5)); 1147 assertFalse(layout.isRtlCharAt(6)); 1148 assertTrue(layout.isRtlCharAt(7)); 1149 } 1150 { 1151 String testString = "\u0623\u0624(ab)\u0625c"; 1152 StaticLayout layout = new StaticLayout(testString, mDefaultPaint, 1153 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 1154 1155 assertTrue(layout.isRtlCharAt(0)); 1156 assertTrue(layout.isRtlCharAt(1)); 1157 assertTrue(layout.isRtlCharAt(2)); 1158 assertFalse(layout.isRtlCharAt(3)); 1159 assertFalse(layout.isRtlCharAt(4)); 1160 assertTrue(layout.isRtlCharAt(5)); 1161 assertTrue(layout.isRtlCharAt(6)); 1162 assertFalse(layout.isRtlCharAt(7)); 1163 assertFalse(layout.isRtlCharAt(8)); 1164 } 1165 } 1166 1167 @Test 1168 public void testGetHorizontal() { 1169 String testString = "abc\u0623\u0624\u0625def"; 1170 StaticLayout layout = new StaticLayout(testString, mDefaultPaint, 1171 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 1172 1173 assertEquals(layout.getPrimaryHorizontal(0), layout.getSecondaryHorizontal(0), 0.0f); 1174 assertTrue(layout.getPrimaryHorizontal(0) < layout.getPrimaryHorizontal(3)); 1175 assertTrue(layout.getPrimaryHorizontal(3) < layout.getSecondaryHorizontal(3)); 1176 assertTrue(layout.getPrimaryHorizontal(4) < layout.getSecondaryHorizontal(3)); 1177 assertEquals(layout.getPrimaryHorizontal(4), layout.getSecondaryHorizontal(4), 0.0f); 1178 assertEquals(layout.getPrimaryHorizontal(3), layout.getSecondaryHorizontal(6), 0.0f); 1179 assertEquals(layout.getPrimaryHorizontal(6), layout.getSecondaryHorizontal(3), 0.0f); 1180 assertEquals(layout.getPrimaryHorizontal(7), layout.getSecondaryHorizontal(7), 0.0f); 1181 } 1182 1183 @Test 1184 public void testVeryLargeString() { 1185 final int MAX_COUNT = 1 << 20; 1186 final int WORD_SIZE = 32; 1187 char[] longText = new char[MAX_COUNT]; 1188 for (int n = 0; n < MAX_COUNT; n++) { 1189 longText[n] = (n % WORD_SIZE) == 0 ? ' ' : 'm'; 1190 } 1191 String longTextString = new String(longText); 1192 TextPaint paint = new TestingTextPaint(); 1193 StaticLayout layout = new StaticLayout(longTextString, paint, DEFAULT_OUTER_WIDTH, 1194 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 1195 assertNotNull(layout); 1196 } 1197 1198 @Test 1199 public void testNoCrashWhenWordStyleOverlap() { 1200 // test case where word boundary overlaps multiple style spans 1201 SpannableStringBuilder text = new SpannableStringBuilder("word boundaries, overlap style"); 1202 // span covers "boundaries" 1203 text.setSpan(new StyleSpan(Typeface.BOLD), 1204 "word ".length(), "word boundaries".length(), 1205 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 1206 mDefaultPaint.setTextLocale(Locale.US); 1207 StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(), 1208 mDefaultPaint, DEFAULT_OUTER_WIDTH) 1209 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) // enable hyphenation 1210 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) 1211 .build(); 1212 assertNotNull(layout); 1213 } 1214 1215 @Test 1216 public void testRespectingIndentsOnEllipsizedText() { 1217 // test case where word boundary overlaps multiple style spans 1218 final String text = "words with indents"; 1219 1220 // +1 to ensure that we won't wrap in the normal case 1221 int textWidth = (int) (mDefaultPaint.measureText(text) + 1); 1222 StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(), 1223 mDefaultPaint, textWidth) 1224 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) // enable hyphenation 1225 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) 1226 .setEllipsize(TruncateAt.END) 1227 .setEllipsizedWidth(textWidth) 1228 .setMaxLines(1) 1229 .setIndents(null, new int[] {20}) 1230 .build(); 1231 assertTrue(layout.getEllipsisStart(0) != 0); 1232 } 1233 1234 @Test(expected = IndexOutOfBoundsException.class) 1235 public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withSpannable() { 1236 final String text = "1\n2\n3"; 1237 final SpannableString spannable = new SpannableString(text); 1238 spannable.setSpan(new Object(), 0, text.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1239 final Layout layout = StaticLayout.Builder.obtain(spannable, 0, spannable.length(), 1240 mDefaultPaint, Integer.MAX_VALUE - 1).setMaxLines(2) 1241 .setEllipsize(TruncateAt.END).build(); 1242 layout.getPrimaryHorizontal(layout.getText().length()); 1243 } 1244 1245 @Test(expected = IndexOutOfBoundsException.class) 1246 public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withString() { 1247 final String text = "1\n2\n3"; 1248 final Layout layout = StaticLayout.Builder.obtain(text, 0, text.length(), 1249 mDefaultPaint, Integer.MAX_VALUE - 1).setMaxLines(2) 1250 .setEllipsize(TruncateAt.END).build(); 1251 layout.getPrimaryHorizontal(layout.getText().length()); 1252 } 1253 1254 @Test 1255 public void testNegativeWidth() { 1256 StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5) 1257 .setIndents(new int[] { 10 }, new int[] { 10 }) 1258 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY).build(); 1259 StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5) 1260 .setIndents(new int[] { 10 }, new int[] { 10 }) 1261 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build(); 1262 StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5) 1263 .setIndents(new int[] { 10 }, new int[] { 10 }) 1264 .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); 1265 } 1266 1267 @Test 1268 public void testGetLineMax() { 1269 final float wholeWidth = mDefaultPaint.measureText(LOREM_IPSUM); 1270 final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph. 1271 final String multiParaTestString = 1272 LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM; 1273 final Layout layout = StaticLayout.Builder.obtain(multiParaTestString, 0, 1274 multiParaTestString.length(), mDefaultPaint, lineWidth) 1275 .build(); 1276 for (int i = 0; i < layout.getLineCount(); i++) { 1277 assertTrue(layout.getLineMax(i) <= lineWidth); 1278 } 1279 } 1280 1281 @Test 1282 public void testIndent() { 1283 final float wholeWidth = mDefaultPaint.measureText(LOREM_IPSUM); 1284 final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph. 1285 final int indentWidth = (int) (lineWidth * 0.3f); // Make 30% indent. 1286 final String multiParaTestString = 1287 LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM; 1288 final Layout layout = StaticLayout.Builder.obtain(multiParaTestString, 0, 1289 multiParaTestString.length(), mDefaultPaint, lineWidth) 1290 .setIndents(new int[] { indentWidth }, null) 1291 .build(); 1292 for (int i = 0; i < layout.getLineCount(); i++) { 1293 assertTrue(layout.getLineMax(i) <= lineWidth - indentWidth); 1294 } 1295 } 1296 1297 private static Bitmap drawToBitmap(Layout l) { 1298 final Bitmap bmp = Bitmap.createBitmap(l.getWidth(), l.getHeight(), Bitmap.Config.RGB_565); 1299 final Canvas c = new Canvas(bmp); 1300 1301 c.save(); 1302 c.translate(0, 0); 1303 l.draw(c); 1304 c.restore(); 1305 return bmp; 1306 } 1307 1308 private static String textPaintToString(TextPaint p) { 1309 return "{" 1310 + "mTextSize=" + p.getTextSize() + ", " 1311 + "mTextSkewX=" + p.getTextSkewX() + ", " 1312 + "mTextScaleX=" + p.getTextScaleX() + ", " 1313 + "mLetterSpacing=" + p.getLetterSpacing() + ", " 1314 + "mFlags=" + p.getFlags() + ", " 1315 + "mTextLocales=" + p.getTextLocales() + ", " 1316 + "mFontVariationSettings=" + p.getFontVariationSettings() + ", " 1317 + "mTypeface=" + p.getTypeface() + ", " 1318 + "mFontFeatureSettings=" + p.getFontFeatureSettings() 1319 + "}"; 1320 } 1321 1322 private static String directionToString(TextDirectionHeuristic dir) { 1323 if (dir == TextDirectionHeuristics.LTR) { 1324 return "LTR"; 1325 } else if (dir == TextDirectionHeuristics.RTL) { 1326 return "RTL"; 1327 } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { 1328 return "FIRSTSTRONG_LTR"; 1329 } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { 1330 return "FIRSTSTRONG_RTL"; 1331 } else if (dir == TextDirectionHeuristics.ANYRTL_LTR) { 1332 return "ANYRTL_LTR"; 1333 } else { 1334 throw new RuntimeException("Unknown Direction"); 1335 } 1336 } 1337 1338 static class LayoutParam { 1339 final int mStrategy; 1340 final int mFrequency; 1341 final TextPaint mPaint; 1342 final TextDirectionHeuristic mDir; 1343 1344 LayoutParam(int strategy, int frequency, TextPaint paint, TextDirectionHeuristic dir) { 1345 mStrategy = strategy; 1346 mFrequency = frequency; 1347 mPaint = new TextPaint(paint); 1348 mDir = dir; 1349 } 1350 1351 @Override 1352 public String toString() { 1353 return "{" 1354 + "mStrategy=" + mStrategy + ", " 1355 + "mFrequency=" + mFrequency + ", " 1356 + "mPaint=" + textPaintToString(mPaint) + ", " 1357 + "mDir=" + directionToString(mDir) 1358 + "}"; 1359 1360 } 1361 1362 Layout getLayout(CharSequence text, int width) { 1363 return StaticLayout.Builder.obtain(text, 0, text.length(), mPaint, width) 1364 .setBreakStrategy(mStrategy).setHyphenationFrequency(mFrequency) 1365 .setTextDirection(mDir).build(); 1366 } 1367 1368 PrecomputedText getPrecomputedText(CharSequence text) { 1369 PrecomputedText.Params param = new PrecomputedText.Params.Builder(mPaint) 1370 .setBreakStrategy(mStrategy) 1371 .setHyphenationFrequency(mFrequency) 1372 .setTextDirection(mDir).build(); 1373 return PrecomputedText.create(text, param); 1374 } 1375 }; 1376 1377 void assertSameStaticLayout(CharSequence text, LayoutParam measuredTextParam, 1378 LayoutParam staticLayoutParam) { 1379 String msg = "StaticLayout for " + staticLayoutParam + " with PrecomputedText" 1380 + " created with " + measuredTextParam + " must output the same BMP."; 1381 1382 final float wholeWidth = mDefaultPaint.measureText(text.toString()); 1383 final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph. 1384 1385 // Static layout parameter should be used for the final output. 1386 final Layout expectedLayout = staticLayoutParam.getLayout(text, lineWidth); 1387 1388 final PrecomputedText mt = measuredTextParam.getPrecomputedText(text); 1389 final Layout resultLayout = StaticLayout.Builder.obtain(mt, 0, mt.length(), 1390 staticLayoutParam.mPaint, lineWidth) 1391 .setBreakStrategy(staticLayoutParam.mStrategy) 1392 .setHyphenationFrequency(staticLayoutParam.mFrequency) 1393 .setTextDirection(staticLayoutParam.mDir).build(); 1394 1395 assertEquals(msg, expectedLayout.getHeight(), resultLayout.getHeight(), 0.0f); 1396 1397 final Bitmap expectedBMP = drawToBitmap(expectedLayout); 1398 final Bitmap resultBMP = drawToBitmap(resultLayout); 1399 1400 assertTrue(msg, resultBMP.sameAs(expectedBMP)); 1401 } 1402 1403 @Test 1404 public void testPrecomputedText() { 1405 int[] breaks = { 1406 Layout.BREAK_STRATEGY_SIMPLE, 1407 Layout.BREAK_STRATEGY_HIGH_QUALITY, 1408 Layout.BREAK_STRATEGY_BALANCED, 1409 }; 1410 1411 int[] frequencies = { 1412 Layout.HYPHENATION_FREQUENCY_NORMAL, 1413 Layout.HYPHENATION_FREQUENCY_FULL, 1414 Layout.HYPHENATION_FREQUENCY_NONE, 1415 }; 1416 1417 TextDirectionHeuristic[] dirs = { 1418 TextDirectionHeuristics.LTR, 1419 TextDirectionHeuristics.RTL, 1420 TextDirectionHeuristics.FIRSTSTRONG_LTR, 1421 TextDirectionHeuristics.FIRSTSTRONG_RTL, 1422 TextDirectionHeuristics.ANYRTL_LTR, 1423 }; 1424 1425 float[] textSizes = { 1426 8.0f, 16.0f, 32.0f 1427 }; 1428 1429 LocaleList[] locales = { 1430 LocaleList.forLanguageTags("en-US"), 1431 LocaleList.forLanguageTags("ja-JP"), 1432 LocaleList.forLanguageTags("en-US,ja-JP"), 1433 }; 1434 1435 TextPaint paint = new TextPaint(); 1436 1437 // If the PrecomputedText is created with the same argument of the StaticLayout, generate 1438 // the same bitmap. 1439 for (int b : breaks) { 1440 for (int f : frequencies) { 1441 for (TextDirectionHeuristic dir : dirs) { 1442 for (float textSize : textSizes) { 1443 for (LocaleList locale : locales) { 1444 paint.setTextSize(textSize); 1445 paint.setTextLocales(locale); 1446 1447 assertSameStaticLayout(LOREM_IPSUM, 1448 new LayoutParam(b, f, paint, dir), 1449 new LayoutParam(b, f, paint, dir)); 1450 } 1451 } 1452 } 1453 } 1454 } 1455 1456 // If the parameters are different, the output of the static layout must be 1457 // same bitmap. 1458 for (int bi = 0; bi < breaks.length; bi++) { 1459 for (int fi = 0; fi < frequencies.length; fi++) { 1460 for (int diri = 0; diri < dirs.length; diri++) { 1461 for (int sizei = 0; sizei < textSizes.length; sizei++) { 1462 for (int localei = 0; localei < locales.length; localei++) { 1463 TextPaint p1 = new TextPaint(); 1464 TextPaint p2 = new TextPaint(); 1465 1466 p1.setTextSize(textSizes[sizei]); 1467 p2.setTextSize(textSizes[(sizei + 1) % textSizes.length]); 1468 1469 p1.setTextLocales(locales[localei]); 1470 p2.setTextLocales(locales[(localei + 1) % locales.length]); 1471 1472 int b1 = breaks[bi]; 1473 int b2 = breaks[(bi + 1) % breaks.length]; 1474 1475 int f1 = frequencies[fi]; 1476 int f2 = frequencies[(fi + 1) % frequencies.length]; 1477 1478 TextDirectionHeuristic dir1 = dirs[diri]; 1479 TextDirectionHeuristic dir2 = dirs[(diri + 1) % dirs.length]; 1480 1481 assertSameStaticLayout(LOREM_IPSUM, 1482 new LayoutParam(b1, f1, p1, dir1), 1483 new LayoutParam(b2, f2, p2, dir2)); 1484 } 1485 } 1486 } 1487 } 1488 } 1489 } 1490 1491 1492 @Test 1493 public void testReplacementFontMetricsTest() { 1494 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 1495 1496 Typeface tf = new Typeface.Builder(context.getAssets(), "fonts/samplefont.ttf").build(); 1497 assertNotNull(tf); 1498 TextPaint paint = new TextPaint(); 1499 paint.setTypeface(tf); 1500 1501 ReplacementSpan firstReplacement = mock(ReplacementSpan.class); 1502 ArgumentCaptor<FontMetricsInt> fm1Captor = ArgumentCaptor.forClass(FontMetricsInt.class); 1503 when(firstReplacement.getSize( 1504 any(Paint.class), any(CharSequence.class), anyInt(), anyInt(), 1505 fm1Captor.capture())).thenReturn(0); 1506 TextAppearanceSpan firstStyleSpan = new TextAppearanceSpan( 1507 null /* family */, Typeface.NORMAL /* style */, 100 /* text size, 1em = 100px */, 1508 null /* text color */, null /* link color */); 1509 1510 ReplacementSpan secondReplacement = mock(ReplacementSpan.class); 1511 ArgumentCaptor<FontMetricsInt> fm2Captor = ArgumentCaptor.forClass(FontMetricsInt.class); 1512 when(secondReplacement.getSize( 1513 any(Paint.class), any(CharSequence.class), any(Integer.class), any(Integer.class), 1514 fm2Captor.capture())).thenReturn(0); 1515 TextAppearanceSpan secondStyleSpan = new TextAppearanceSpan( 1516 null /* family */, Typeface.NORMAL /* style */, 200 /* text size, 1em = 200px */, 1517 null /* text color */, null /* link color */); 1518 1519 SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World\nHello, Android"); 1520 ssb.setSpan(firstStyleSpan, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1521 ssb.setSpan(firstReplacement, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1522 ssb.setSpan(secondStyleSpan, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1523 ssb.setSpan(secondReplacement, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1524 1525 StaticLayout.Builder.obtain(ssb, 0, ssb.length(), paint, Integer.MAX_VALUE).build(); 1526 1527 FontMetricsInt firstMetrics = fm1Captor.getValue(); 1528 FontMetricsInt secondMetrics = fm2Captor.getValue(); 1529 1530 // The samplefont.ttf has 0.8em ascent and 0.2em descent. 1531 assertEquals(-100, firstMetrics.ascent); 1532 assertEquals(20, firstMetrics.descent); 1533 1534 assertEquals(-200, secondMetrics.ascent); 1535 assertEquals(40, secondMetrics.descent); 1536 } 1537 1538 @Test 1539 public void testChangeFontMetricsLineHeightBySpanTest() { 1540 final TextPaint paint = new TextPaint(); 1541 paint.setTextSize(50); 1542 final SpannableString spanStr0 = new SpannableString(LOREM_IPSUM); 1543 // Make sure the final layout contain multiple lines. 1544 final int width = (int) paint.measureText(spanStr0.toString()) / 5; 1545 final int expectedHeight0 = 25; 1546 1547 spanStr0.setSpan(new LineHeightSpan.Standard(expectedHeight0), 0, spanStr0.length(), 1548 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1549 StaticLayout layout0 = StaticLayout.Builder.obtain(spanStr0, 0, spanStr0.length(), 1550 paint, width).build(); 1551 1552 // We need at least 3 lines for testing. 1553 assertTrue(layout0.getLineCount() > 2); 1554 // Omit the first and last line, because their line hight might be different due to padding. 1555 for (int i = 1; i < layout0.getLineCount() - 1; ++i) { 1556 assertEquals(expectedHeight0, layout0.getLineBottom(i) - layout0.getLineTop(i)); 1557 } 1558 1559 final SpannableString spanStr1 = new SpannableString(LOREM_IPSUM); 1560 int expectedHeight1 = 100; 1561 1562 spanStr1.setSpan(new LineHeightSpan.Standard(expectedHeight1), 0, spanStr1.length(), 1563 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1564 StaticLayout layout1 = StaticLayout.Builder.obtain(spanStr1, 0, spanStr1.length(), 1565 paint, width).build(); 1566 1567 for (int i = 1; i < layout1.getLineCount() - 1; ++i) { 1568 assertEquals(expectedHeight1, layout1.getLineBottom(i) - layout1.getLineTop(i)); 1569 } 1570 } 1571 1572 @Test 1573 public void testChangeFontMetricsLineHeightBySpanMultipleTimesTest() { 1574 final TextPaint paint = new TextPaint(); 1575 paint.setTextSize(50); 1576 final SpannableString spanStr = new SpannableString(LOREM_IPSUM); 1577 final int width = (int) paint.measureText(spanStr.toString()) / 5; 1578 final int expectedHeight = 100; 1579 1580 spanStr.setSpan(new LineHeightSpan.Standard(25), 0, spanStr.length(), 1581 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1582 // Only the last span is effective. 1583 spanStr.setSpan(new LineHeightSpan.Standard(expectedHeight), 0, spanStr.length(), 1584 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1585 StaticLayout layout = StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(), 1586 paint, width).build(); 1587 1588 assertTrue(layout.getLineCount() > 2); 1589 for (int i = 1; i < layout.getLineCount() - 1; ++i) { 1590 assertEquals(expectedHeight, layout.getLineBottom(i) - layout.getLineTop(i)); 1591 } 1592 } 1593 1594 private class FakeLineBackgroundSpan implements LineBackgroundSpan { 1595 // Whenever drawBackground() is called, the start and end of 1596 // the line will be stored into mHistory as an array in the 1597 // format of [start, end]. 1598 private final List<int[]> mHistory; 1599 1600 FakeLineBackgroundSpan() { 1601 mHistory = new ArrayList<int[]>(); 1602 } 1603 1604 @Override 1605 public void drawBackground(Canvas c, Paint p, 1606 int left, int right, 1607 int top, int baseline, int bottom, 1608 CharSequence text, int start, int end, 1609 int lnum) { 1610 mHistory.add(new int[] {start, end}); 1611 } 1612 1613 List<int[]> getHistory() { 1614 return mHistory; 1615 } 1616 } 1617 1618 private void testLineBackgroundSpanInRange(String text, int start, int end) { 1619 final SpannableString spanStr = new SpannableString(text); 1620 final FakeLineBackgroundSpan span = new FakeLineBackgroundSpan(); 1621 spanStr.setSpan(span, start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); 1622 1623 final TextPaint paint = new TextPaint(); 1624 paint.setTextSize(50); 1625 final int width = (int) paint.measureText(spanStr.toString()) / 5; 1626 final StaticLayout layout = StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(), 1627 paint, width).build(); 1628 1629 // One line is too simple, need more to test. 1630 assertTrue(layout.getLineCount() > 1); 1631 drawToBitmap(layout); 1632 List<int[]> history = span.getHistory(); 1633 1634 if (history.size() == 0) { 1635 // drawBackground() of FakeLineBackgroundSpan was never called. 1636 // This only happens when the length of the span is zero. 1637 assertTrue(start >= end); 1638 return; 1639 } 1640 1641 // Check if drawBackground() is corrected called for each affected line. 1642 int lastLineEnd = history.get(0)[0]; 1643 for (int[] lineRange: history) { 1644 // The range of line must intersect with the span. 1645 assertTrue(lineRange[0] < end && lineRange[1] > start); 1646 // Check: 1647 // 1. drawBackground() is called in the correct sequence. 1648 // 2. drawBackground() is called only once for each affected line. 1649 assertEquals(lastLineEnd, lineRange[0]); 1650 lastLineEnd = lineRange[1]; 1651 } 1652 1653 int[] firstLineRange = history.get(0); 1654 int[] lastLineRange = history.get(history.size() - 1); 1655 1656 // Check if affected lines match the span coverage. 1657 assertTrue(firstLineRange[0] <= start && end <= lastLineRange[1]); 1658 } 1659 1660 @Test 1661 public void testDrawWithLineBackgroundSpanCoverWholeText() { 1662 testLineBackgroundSpanInRange(LOREM_IPSUM, 0, LOREM_IPSUM.length()); 1663 } 1664 1665 @Test 1666 public void testDrawWithLineBackgroundSpanCoverNothing() { 1667 int i = 0; 1668 // Zero length Spans. 1669 testLineBackgroundSpanInRange(LOREM_IPSUM, i, i); 1670 i = LOREM_IPSUM.length() / 2; 1671 testLineBackgroundSpanInRange(LOREM_IPSUM, i, i); 1672 } 1673 1674 @Test 1675 public void testDrawWithLineBackgroundSpanCoverPart() { 1676 int start = 0; 1677 int end = LOREM_IPSUM.length() / 2; 1678 testLineBackgroundSpanInRange(LOREM_IPSUM, start, end); 1679 1680 start = LOREM_IPSUM.length() / 2; 1681 end = LOREM_IPSUM.length(); 1682 testLineBackgroundSpanInRange(LOREM_IPSUM, start, end); 1683 } 1684 } 1685