StaticLayoutTest.java revision ef7cfa17e78d05b8d931d839d25261c459a0fc90
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package android.text; 18 19import static android.text.Layout.Alignment.ALIGN_NORMAL; 20 21import static org.junit.Assert.assertEquals; 22import static org.junit.Assert.assertTrue; 23 24import android.graphics.Paint.FontMetricsInt; 25import android.os.LocaleList; 26import android.support.test.filters.SmallTest; 27import android.support.test.runner.AndroidJUnit4; 28import android.text.Layout.Alignment; 29import android.text.method.EditorState; 30import android.text.style.LocaleSpan; 31import android.util.Log; 32 33import org.junit.Before; 34import org.junit.Test; 35import org.junit.runner.RunWith; 36 37import java.text.Normalizer; 38import java.util.ArrayList; 39import java.util.List; 40import java.util.Locale; 41 42/** 43 * Tests StaticLayout vertical metrics behavior. 44 */ 45@SmallTest 46@RunWith(AndroidJUnit4.class) 47public class StaticLayoutTest { 48 private static final float SPACE_MULTI = 1.0f; 49 private static final float SPACE_ADD = 0.0f; 50 private static final int DEFAULT_OUTER_WIDTH = 150; 51 52 private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar" 53 + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong"; 54 private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence"; 55 56 private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER; 57 private static final int ELLIPSIZE_WIDTH = 8; 58 59 private StaticLayout mDefaultLayout; 60 private TextPaint mDefaultPaint; 61 62 @Before 63 public void setup() { 64 mDefaultPaint = new TextPaint(); 65 mDefaultLayout = createDefaultStaticLayout(); 66 } 67 68 private StaticLayout createDefaultStaticLayout() { 69 return new StaticLayout(LAYOUT_TEXT, mDefaultPaint, 70 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 71 } 72 73 @Test 74 public void testBuilder() { 75 { 76 // Obtain. 77 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 78 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 79 final StaticLayout layout = builder.build(); 80 // Check default value. 81 assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR, 82 layout.getTextDirectionHeuristic()); 83 } 84 { 85 // setTextDirection. 86 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 87 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 88 builder.setTextDirection(TextDirectionHeuristics.RTL); 89 final StaticLayout layout = builder.build(); 90 // Always returns TextDirectionHeuristics.FIRSTSTRONG_LTR. 91 assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR, 92 layout.getTextDirectionHeuristic()); 93 } 94 } 95 96 /** 97 * Basic test showing expected behavior and relationship between font 98 * metrics and line metrics. 99 */ 100 @Test 101 public void testGetters1() { 102 LayoutBuilder b = builder(); 103 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 104 105 // check default paint 106 Log.i("TG1:paint", fmi.toString()); 107 108 Layout l = b.build(); 109 assertVertMetrics(l, 0, 0, 110 fmi.ascent, fmi.descent); 111 112 // other quick metrics 113 assertEquals(0, l.getLineStart(0)); 114 assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0)); 115 assertEquals(false, l.getLineContainsTab(0)); 116 assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0)); 117 assertEquals(0, l.getEllipsisCount(0)); 118 assertEquals(0, l.getEllipsisStart(0)); 119 assertEquals(b.width, l.getEllipsizedWidth()); 120 } 121 122 /** 123 * Basic test showing effect of includePad = true with 1 line. 124 * Top and bottom padding are affected, as is the line descent and height. 125 */ 126 @Test 127 public void testGetters2() { 128 LayoutBuilder b = builder() 129 .setIncludePad(true); 130 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 131 132 Layout l = b.build(); 133 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 134 fmi.top, fmi.bottom); 135 } 136 137 /** 138 * Basic test showing effect of includePad = true wrapping to 2 lines. 139 * Ascent of top line and descent of bottom line are affected. 140 */ 141 @Test 142 public void testGetters3() { 143 LayoutBuilder b = builder() 144 .setIncludePad(true) 145 .setWidth(50); 146 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 147 148 Layout l = b.build(); 149 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 150 fmi.top, fmi.descent, 151 fmi.ascent, fmi.bottom); 152 } 153 154 /** 155 * Basic test showing effect of includePad = true wrapping to 3 lines. 156 * First line ascent is top, bottom line descent is bottom. 157 */ 158 @Test 159 public void testGetters4() { 160 LayoutBuilder b = builder() 161 .setText("This is a longer test") 162 .setIncludePad(true) 163 .setWidth(50); 164 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 165 166 Layout l = b.build(); 167 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 168 fmi.top, fmi.descent, 169 fmi.ascent, fmi.descent, 170 fmi.ascent, fmi.bottom); 171 } 172 173 /** 174 * Basic test showing effect of includePad = true wrapping to 3 lines and 175 * large text. See effect of leading. Currently, we don't expect there to 176 * even be non-zero leading. 177 */ 178 @Test 179 public void testGetters5() { 180 LayoutBuilder b = builder() 181 .setText("This is a longer test") 182 .setIncludePad(true) 183 .setWidth(150); 184 b.paint.setTextSize(36); 185 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 186 187 if (fmi.leading == 0) { // nothing to test 188 Log.i("TG5", "leading is 0, skipping test"); 189 return; 190 } 191 192 // So far, leading is not used, so this is the same as TG4. If we start 193 // using leading, this will fail. 194 Layout l = b.build(); 195 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 196 fmi.top, fmi.descent, 197 fmi.ascent, fmi.descent, 198 fmi.ascent, fmi.bottom); 199 } 200 201 /** 202 * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping 203 * to 3 lines. 204 */ 205 @Test 206 public void testGetters6() { 207 int spacingAdd = 2; // int so expressions return int 208 LayoutBuilder b = builder() 209 .setText("This is a longer test") 210 .setIncludePad(true) 211 .setWidth(50) 212 .setSpacingAdd(spacingAdd); 213 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 214 215 Layout l = b.build(); 216 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 217 fmi.top, fmi.descent + spacingAdd, 218 fmi.ascent, fmi.descent + spacingAdd, 219 fmi.ascent, fmi.bottom); 220 } 221 222 /** 223 * Basic test showing effect of includePad = true, spacingAdd = 2, 224 * spacingMult = 1.5, wrapping to 3 lines. 225 */ 226 @Test 227 public void testGetters7() { 228 LayoutBuilder b = builder() 229 .setText("This is a longer test") 230 .setIncludePad(true) 231 .setWidth(50) 232 .setSpacingAdd(2) 233 .setSpacingMult(1.5f); 234 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 235 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 236 237 Layout l = b.build(); 238 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 239 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 240 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 241 fmi.ascent, fmi.bottom); 242 } 243 244 /** 245 * Basic test showing effect of includePad = true, spacingAdd = 0, 246 * spacingMult = 0.8 when wrapping to 3 lines. 247 */ 248 @Test 249 public void testGetters8() { 250 LayoutBuilder b = builder() 251 .setText("This is a longer test") 252 .setIncludePad(true) 253 .setWidth(50) 254 .setSpacingAdd(2) 255 .setSpacingMult(.8f); 256 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 257 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 258 259 Layout l = b.build(); 260 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 261 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 262 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 263 fmi.ascent, fmi.bottom); 264 } 265 266 // ----- test utility classes and methods ----- 267 268 // Models the effect of the scale and add parameters. I think the current 269 // implementation misbehaves. 270 private static class Scaler { 271 private final float sMult; 272 private final float sAdd; 273 274 Scaler(float sMult, float sAdd) { 275 this.sMult = sMult - 1; 276 this.sAdd = sAdd; 277 } 278 279 public int scale(float height) { 280 int altVal = (int)(height * sMult + sAdd + 0.5); 281 int rndVal = Math.round(height * sMult + sAdd); 282 if (altVal != rndVal) { 283 Log.i("Scale", "expected scale: " + rndVal + 284 " != returned scale: " + altVal); 285 } 286 return rndVal; 287 } 288 } 289 290 /* package */ static LayoutBuilder builder() { 291 return new LayoutBuilder(); 292 } 293 294 /* package */ static class LayoutBuilder { 295 String text = "This is a test"; 296 TextPaint paint = new TextPaint(); // default 297 int width = 100; 298 Alignment align = ALIGN_NORMAL; 299 float spacingMult = 1; 300 float spacingAdd = 0; 301 boolean includePad = false; 302 303 LayoutBuilder setText(String text) { 304 this.text = text; 305 return this; 306 } 307 308 LayoutBuilder setPaint(TextPaint paint) { 309 this.paint = paint; 310 return this; 311 } 312 313 LayoutBuilder setWidth(int width) { 314 this.width = width; 315 return this; 316 } 317 318 LayoutBuilder setAlignment(Alignment align) { 319 this.align = align; 320 return this; 321 } 322 323 LayoutBuilder setSpacingMult(float spacingMult) { 324 this.spacingMult = spacingMult; 325 return this; 326 } 327 328 LayoutBuilder setSpacingAdd(float spacingAdd) { 329 this.spacingAdd = spacingAdd; 330 return this; 331 } 332 333 LayoutBuilder setIncludePad(boolean includePad) { 334 this.includePad = includePad; 335 return this; 336 } 337 338 Layout build() { 339 return new StaticLayout(text, paint, width, align, spacingMult, 340 spacingAdd, includePad); 341 } 342 } 343 344 private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) { 345 assertTopBotPadding(l, topPad, botPad); 346 assertLinesMetrics(l, values); 347 } 348 349 private void assertLinesMetrics(Layout l, int... values) { 350 // sanity check 351 if ((values.length & 0x1) != 0) { 352 throw new IllegalArgumentException(String.valueOf(values.length)); 353 } 354 355 int lines = values.length >> 1; 356 assertEquals(lines, l.getLineCount()); 357 358 int t = 0; 359 for (int i = 0, n = 0; i < lines; ++i, n += 2) { 360 int a = values[n]; 361 int d = values[n+1]; 362 int h = -a + d; 363 assertLineMetrics(l, i, t, a, d, h); 364 t += h; 365 } 366 367 assertEquals(t, l.getHeight()); 368 } 369 370 private void assertLineMetrics(Layout l, int line, 371 int top, int ascent, int descent, int height) { 372 String info = "line " + line; 373 assertEquals(info, top, l.getLineTop(line)); 374 assertEquals(info, ascent, l.getLineAscent(line)); 375 assertEquals(info, descent, l.getLineDescent(line)); 376 assertEquals(info, height, l.getLineBottom(line) - top); 377 } 378 379 private void assertTopBotPadding(Layout l, int topPad, int botPad) { 380 assertEquals(topPad, l.getTopPadding()); 381 assertEquals(botPad, l.getBottomPadding()); 382 } 383 384 private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) { 385 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 386 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 387 final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart); 388 state.mSelectionStart = state.mSelectionEnd = newOffset; 389 } 390 391 private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) { 392 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 393 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 394 final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart); 395 state.mSelectionStart = state.mSelectionEnd = newOffset; 396 } 397 398 /** 399 * Tests for keycap, variation selectors, flags are in CTS. 400 * See {@link android.text.cts.StaticLayoutTest}. 401 */ 402 @Test 403 public void testEmojiOffset() { 404 EditorState state = new EditorState(); 405 TextPaint paint = new TextPaint(); 406 407 // Odd numbered regional indicator symbols. 408 // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL 409 // LETTER C. 410 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 411 moveCursorToRightCursorableOffset(state, paint); 412 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 413 moveCursorToRightCursorableOffset(state, paint); 414 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 415 moveCursorToRightCursorableOffset(state, paint); 416 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 417 moveCursorToRightCursorableOffset(state, paint); 418 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 419 moveCursorToLeftCursorableOffset(state, paint); 420 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 421 moveCursorToLeftCursorableOffset(state, paint); 422 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 423 moveCursorToLeftCursorableOffset(state, paint); 424 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 425 moveCursorToLeftCursorableOffset(state, paint); 426 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 427 moveCursorToLeftCursorableOffset(state, paint); 428 429 // Zero width sequence 430 final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468"; 431 state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 432 moveCursorToRightCursorableOffset(state, paint); 433 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 434 moveCursorToRightCursorableOffset(state, paint); 435 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 436 moveCursorToRightCursorableOffset(state, paint); 437 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 438 moveCursorToRightCursorableOffset(state, paint); 439 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 440 moveCursorToLeftCursorableOffset(state, paint); 441 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 442 moveCursorToLeftCursorableOffset(state, paint); 443 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 444 moveCursorToLeftCursorableOffset(state, paint); 445 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 446 moveCursorToLeftCursorableOffset(state, paint); 447 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 448 moveCursorToLeftCursorableOffset(state, paint); 449 450 // Emoji modifiers 451 // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2. 452 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 453 moveCursorToRightCursorableOffset(state, paint); 454 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 455 moveCursorToRightCursorableOffset(state, paint); 456 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 457 moveCursorToRightCursorableOffset(state, paint); 458 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 459 moveCursorToRightCursorableOffset(state, paint); 460 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 461 moveCursorToLeftCursorableOffset(state, paint); 462 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 463 moveCursorToLeftCursorableOffset(state, paint); 464 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 465 moveCursorToLeftCursorableOffset(state, paint); 466 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 467 moveCursorToLeftCursorableOffset(state, paint); 468 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 469 moveCursorToLeftCursorableOffset(state, paint); 470 } 471 472 private StaticLayout createEllipsizeStaticLayout(CharSequence text, 473 TextUtils.TruncateAt ellipsize, int maxLines) { 474 return new StaticLayout(text, 0, text.length(), 475 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, 476 TextDirectionHeuristics.FIRSTSTRONG_LTR, 477 SPACE_MULTI, SPACE_ADD, true /* include pad */, 478 ellipsize, 479 ELLIPSIZE_WIDTH, 480 maxLines); 481 } 482 483 @Test 484 public void testEllipsis_singleLine() { 485 { 486 // Single line case and TruncateAt.END so that we have some ellipsis 487 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 488 TextUtils.TruncateAt.END, 1); 489 assertTrue(layout.getEllipsisCount(0) > 0); 490 } 491 { 492 // Single line case and TruncateAt.MIDDLE so that we have some ellipsis 493 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 494 TextUtils.TruncateAt.MIDDLE, 1); 495 assertTrue(layout.getEllipsisCount(0) > 0); 496 } 497 { 498 // Single line case and TruncateAt.END so that we have some ellipsis 499 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 500 TextUtils.TruncateAt.END, 1); 501 assertTrue(layout.getEllipsisCount(0) > 0); 502 } 503 { 504 // Single line case and TruncateAt.MARQUEE so that we have NO ellipsis 505 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 506 TextUtils.TruncateAt.MARQUEE, 1); 507 assertTrue(layout.getEllipsisCount(0) == 0); 508 } 509 { 510 final String text = "\u3042" // HIRAGANA LETTER A 511 + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; 512 final float textWidth = mDefaultPaint.measureText(text); 513 final int halfWidth = (int) (textWidth / 2.0f); 514 { 515 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 516 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 517 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); 518 assertTrue(layout.getEllipsisCount(0) > 0); 519 assertTrue(layout.getEllipsisStart(0) > 0); 520 } 521 { 522 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 523 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 524 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1); 525 assertTrue(layout.getEllipsisCount(0) > 0); 526 assertEquals(0, mDefaultLayout.getEllipsisStart(0)); 527 } 528 { 529 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 530 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 531 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1); 532 assertTrue(layout.getEllipsisCount(0) > 0); 533 assertTrue(layout.getEllipsisStart(0) > 0); 534 } 535 { 536 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 537 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 538 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1); 539 assertEquals(0, layout.getEllipsisCount(0)); 540 } 541 } 542 543 { 544 // The white spaces in this text will be trailing if maxLines is larger than 1, but 545 // width of the trailing white spaces must not be ignored if ellipsis is applied. 546 final String text = "abc def"; 547 final float textWidth = mDefaultPaint.measureText(text); 548 final int halfWidth = (int) (textWidth / 2.0f); 549 { 550 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 551 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 552 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); 553 assertTrue(layout.getEllipsisCount(0) > 0); 554 assertTrue(layout.getEllipsisStart(0) > 0); 555 } 556 } 557 558 { 559 // 2 family emojis (11 code units + 11 code units). 560 final String text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66" 561 + "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; 562 final float textWidth = mDefaultPaint.measureText(text); 563 564 final TextUtils.TruncateAt[] kinds = {TextUtils.TruncateAt.START, 565 TextUtils.TruncateAt.MIDDLE, TextUtils.TruncateAt.END}; 566 for (final TextUtils.TruncateAt kind : kinds) { 567 for (int i = 0; i <= 8; i++) { 568 int avail = (int) (textWidth * i / 7.0f); 569 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 570 avail, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 571 SPACE_MULTI, SPACE_ADD, false, kind, avail, 1); 572 573 assertTrue(layout.getEllipsisCount(0) == text.length() 574 || layout.getEllipsisCount(0) == text.length() / 2 575 || layout.getEllipsisCount(0) == 0); 576 } 577 } 578 } 579 } 580 581 // String wrapper for testing not well known implementation of CharSequence. 582 private class FakeCharSequence implements CharSequence { 583 private String mStr; 584 585 FakeCharSequence(String str) { 586 mStr = str; 587 } 588 589 @Override 590 public char charAt(int index) { 591 return mStr.charAt(index); 592 } 593 594 @Override 595 public int length() { 596 return mStr.length(); 597 } 598 599 @Override 600 public CharSequence subSequence(int start, int end) { 601 return mStr.subSequence(start, end); 602 } 603 604 @Override 605 public String toString() { 606 return mStr; 607 } 608 }; 609 610 private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) { 611 List<CharSequence> result = new ArrayList<>(); 612 613 List<String> normalizedStrings = new ArrayList<>(); 614 for (Normalizer.Form form: forms) { 615 normalizedStrings.add(Normalizer.normalize(testString, form)); 616 } 617 618 for (String str: normalizedStrings) { 619 result.add(str); 620 result.add(new SpannedString(str)); 621 result.add(new SpannableString(str)); 622 result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation. 623 result.add(new FakeCharSequence(str)); // as a not well known implementation. 624 } 625 return result; 626 } 627 628 private String buildTestMessage(CharSequence seq) { 629 String normalized; 630 if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) { 631 normalized = "NFC"; 632 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) { 633 normalized = "NFD"; 634 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) { 635 normalized = "NFKC"; 636 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) { 637 normalized = "NFKD"; 638 } else { 639 throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD"); 640 } 641 642 StringBuilder builder = new StringBuilder(); 643 for (int i = 0; i < seq.length(); ++i) { 644 builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i)))); 645 } 646 647 return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" 648 + ", class: " + seq.getClass().getName() 649 + ", Normalization: " + normalized; 650 } 651 652 @Test 653 public void testGetOffset_UNICODE_Hebrew() { 654 String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters 655 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 656 StaticLayout layout = new StaticLayout(seq, mDefaultPaint, 657 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, 658 TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true); 659 660 String testLabel = buildTestMessage(seq); 661 662 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0)); 663 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1)); 664 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2)); 665 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3)); 666 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4)); 667 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5)); 668 669 assertEquals(testLabel, 0, layout.getOffsetToRightOf(0)); 670 assertEquals(testLabel, 0, layout.getOffsetToRightOf(1)); 671 assertEquals(testLabel, 1, layout.getOffsetToRightOf(2)); 672 assertEquals(testLabel, 2, layout.getOffsetToRightOf(3)); 673 assertEquals(testLabel, 3, layout.getOffsetToRightOf(4)); 674 assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); 675 } 676 } 677 678 @Test 679 public void testLocaleSpanAffectsHyphenation() { 680 TextPaint paint = new TextPaint(); 681 paint.setTextLocale(Locale.US); 682 // Private use language, with no hyphenation rules. 683 final Locale privateLocale = Locale.forLanguageTag("qaa"); 684 685 final String longWord = "philanthropic"; 686 final float wordWidth = paint.measureText(longWord); 687 // Wide enough that words get hyphenated by default. 688 final int paraWidth = Math.round(wordWidth * 1.8f); 689 final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " " 690 + longWord + " " + longWord; 691 692 final int numEnglishLines = StaticLayout.Builder 693 .obtain(sentence, 0, sentence.length(), paint, paraWidth) 694 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 695 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 696 .build() 697 .getLineCount(); 698 699 { 700 final SpannableString text = new SpannableString(sentence); 701 text.setSpan(new LocaleSpan(privateLocale), 0, text.length(), 702 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 703 final int numPrivateLocaleLines = StaticLayout.Builder 704 .obtain(text, 0, text.length(), paint, paraWidth) 705 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 706 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 707 .build() 708 .getLineCount(); 709 710 // Since the paragraph set to English gets hyphenated, the number of lines would be 711 // smaller than the number of lines when there is a span setting a language that 712 // doesn't get hyphenated. 713 assertTrue(numEnglishLines < numPrivateLocaleLines); 714 } 715 { 716 // Same as the above test, except that the locale span now uses a locale list starting 717 // with the private non-hyphenating locale. 718 final SpannableString text = new SpannableString(sentence); 719 final LocaleList locales = new LocaleList(privateLocale, Locale.US); 720 text.setSpan(new LocaleSpan(locales), 0, text.length(), 721 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 722 final int numPrivateLocaleLines = StaticLayout.Builder 723 .obtain(text, 0, text.length(), paint, paraWidth) 724 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 725 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 726 .build() 727 .getLineCount(); 728 729 assertTrue(numEnglishLines < numPrivateLocaleLines); 730 } 731 { 732 final SpannableString text = new SpannableString(sentence); 733 // Apply the private LocaleSpan only to the first word, which is not getting hyphenated 734 // anyway. 735 text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(), 736 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 737 final int numPrivateLocaleLines = StaticLayout.Builder 738 .obtain(text, 0, text.length(), paint, paraWidth) 739 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 740 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 741 .build() 742 .getLineCount(); 743 744 // Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan 745 // should not affect the layout. 746 assertEquals(numEnglishLines, numPrivateLocaleLines); 747 } 748 } 749} 750