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