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