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 17 package android.text; 18 19 import static android.text.Layout.Alignment.ALIGN_NORMAL; 20 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertTrue; 23 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Paint.FontMetricsInt; 27 import android.os.LocaleList; 28 import android.platform.test.annotations.Presubmit; 29 import android.text.Layout.Alignment; 30 import android.text.method.EditorState; 31 import android.text.style.LocaleSpan; 32 import android.util.Log; 33 34 import androidx.test.filters.SmallTest; 35 import androidx.test.runner.AndroidJUnit4; 36 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 41 import java.text.Normalizer; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Locale; 45 46 /** 47 * Tests StaticLayout vertical metrics behavior. 48 */ 49 @Presubmit 50 @SmallTest 51 @RunWith(AndroidJUnit4.class) 52 public class StaticLayoutTest { 53 private static final float SPACE_MULTI = 1.0f; 54 private static final float SPACE_ADD = 0.0f; 55 private static final int DEFAULT_OUTER_WIDTH = 150; 56 57 private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar" 58 + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong"; 59 private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence"; 60 61 private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER; 62 private static final int ELLIPSIZE_WIDTH = 8; 63 64 private StaticLayout mDefaultLayout; 65 private TextPaint mDefaultPaint; 66 67 @Before setup()68 public void setup() { 69 mDefaultPaint = new TextPaint(); 70 mDefaultLayout = createDefaultStaticLayout(); 71 } 72 createDefaultStaticLayout()73 private StaticLayout createDefaultStaticLayout() { 74 return new StaticLayout(LAYOUT_TEXT, mDefaultPaint, 75 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); 76 } 77 78 @Test testBuilder_textDirection()79 public void testBuilder_textDirection() { 80 { 81 // Obtain. 82 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 83 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 84 final StaticLayout layout = builder.build(); 85 // Check default value. 86 assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR, 87 layout.getTextDirectionHeuristic()); 88 } 89 { 90 // setTextDirection. 91 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, 92 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); 93 builder.setTextDirection(TextDirectionHeuristics.RTL); 94 final StaticLayout layout = builder.build(); 95 assertEquals(TextDirectionHeuristics.RTL, 96 layout.getTextDirectionHeuristic()); 97 } 98 } 99 100 /** 101 * Basic test showing expected behavior and relationship between font 102 * metrics and line metrics. 103 */ 104 @Test testGetters1()105 public void testGetters1() { 106 LayoutBuilder b = builder(); 107 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 108 109 // check default paint 110 Log.i("TG1:paint", fmi.toString()); 111 112 Layout l = b.build(); 113 assertVertMetrics(l, 0, 0, 114 new int[][]{{fmi.ascent, fmi.descent, 0}}); 115 116 // other quick metrics 117 assertEquals(0, l.getLineStart(0)); 118 assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0)); 119 assertEquals(false, l.getLineContainsTab(0)); 120 assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0)); 121 assertEquals(0, l.getEllipsisCount(0)); 122 assertEquals(0, l.getEllipsisStart(0)); 123 assertEquals(b.width, l.getEllipsizedWidth()); 124 } 125 126 /** 127 * Basic test showing effect of includePad = true with 1 line. 128 * Top and bottom padding are affected, as is the line descent and height. 129 */ 130 @Test testLineMetrics_withPadding()131 public void testLineMetrics_withPadding() { 132 LayoutBuilder b = builder() 133 .setIncludePad(true); 134 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 135 136 Layout l = b.build(); 137 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 138 new int[][]{{fmi.top, fmi.bottom, 0}}); 139 } 140 141 /** 142 * Basic test showing effect of includePad = true wrapping to 2 lines. 143 * Ascent of top line and descent of bottom line are affected. 144 */ 145 @Test testLineMetrics_withPaddingAndWidth()146 public void testLineMetrics_withPaddingAndWidth() { 147 LayoutBuilder b = builder() 148 .setIncludePad(true) 149 .setWidth(50); 150 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 151 152 Layout l = b.build(); 153 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 154 new int[][]{ 155 {fmi.top, fmi.descent, 0}, 156 {fmi.ascent, fmi.bottom, 0} 157 }); 158 } 159 160 /** 161 * Basic test showing effect of includePad = true wrapping to 3 lines. 162 * First line ascent is top, bottom line descent is bottom. 163 */ 164 @Test testLineMetrics_withThreeLines()165 public void testLineMetrics_withThreeLines() { 166 LayoutBuilder b = builder() 167 .setText("This is a longer test") 168 .setIncludePad(true) 169 .setWidth(50); 170 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 171 172 Layout l = b.build(); 173 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 174 new int[][]{ 175 {fmi.top, fmi.descent, 0}, 176 {fmi.ascent, fmi.descent, 0}, 177 {fmi.ascent, fmi.bottom, 0} 178 }); 179 } 180 181 /** 182 * Basic test showing effect of includePad = true wrapping to 3 lines and 183 * large text. See effect of leading. Currently, we don't expect there to 184 * even be non-zero leading. 185 */ 186 @Test testLineMetrics_withLargeText()187 public void testLineMetrics_withLargeText() { 188 LayoutBuilder b = builder() 189 .setText("This is a longer test") 190 .setIncludePad(true) 191 .setWidth(150); 192 b.paint.setTextSize(36); 193 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 194 195 if (fmi.leading == 0) { // nothing to test 196 Log.i("TG5", "leading is 0, skipping test"); 197 return; 198 } 199 200 // So far, leading is not used, so this is the same as TG4. If we start 201 // using leading, this will fail. 202 Layout l = b.build(); 203 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 204 new int[][]{ 205 {fmi.top, fmi.descent, 0}, 206 {fmi.ascent, fmi.descent, 0}, 207 {fmi.ascent, fmi.bottom, 0} 208 }); 209 } 210 211 /** 212 * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping 213 * to 3 lines. 214 */ 215 @Test testLineMetrics_withSpacingAdd()216 public void testLineMetrics_withSpacingAdd() { 217 int spacingAdd = 2; // int so expressions return int 218 LayoutBuilder b = builder() 219 .setText("This is a longer test") 220 .setIncludePad(true) 221 .setWidth(50) 222 .setSpacingAdd(spacingAdd); 223 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 224 225 Layout l = b.build(); 226 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 227 new int[][]{ 228 {fmi.top, fmi.descent + spacingAdd, spacingAdd}, 229 {fmi.ascent, fmi.descent + spacingAdd, spacingAdd}, 230 {fmi.ascent, fmi.bottom, 0} 231 }); 232 } 233 234 /** 235 * Basic test showing effect of includePad = true, spacingAdd = 2, 236 * spacingMult = 1.5, wrapping to 3 lines. 237 */ 238 @Test testLineMetrics_withSpacingMult()239 public void testLineMetrics_withSpacingMult() { 240 LayoutBuilder b = builder() 241 .setText("This is a longer test") 242 .setIncludePad(true) 243 .setWidth(50) 244 .setSpacingAdd(2) 245 .setSpacingMult(1.5f); 246 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 247 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 248 249 Layout l = b.build(); 250 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 251 new int[][]{ 252 {fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 253 s.scale(fmi.descent - fmi.top)}, 254 {fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 255 s.scale(fmi.descent - fmi.ascent)}, 256 {fmi.ascent, fmi.bottom, 0} 257 }); 258 } 259 260 /** 261 * Basic test showing effect of includePad = true, spacingAdd = 0, 262 * spacingMult = 0.8 when wrapping to 3 lines. 263 */ 264 @Test testLineMetrics_withUnitIntervalSpacingMult()265 public void testLineMetrics_withUnitIntervalSpacingMult() { 266 LayoutBuilder b = builder() 267 .setText("This is a longer test") 268 .setIncludePad(true) 269 .setWidth(50) 270 .setSpacingAdd(2) 271 .setSpacingMult(.8f); 272 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 273 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 274 275 Layout l = b.build(); 276 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 277 new int[][]{ 278 {fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 279 s.scale(fmi.descent - fmi.top)}, 280 {fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 281 s.scale(fmi.descent - fmi.ascent)}, 282 {fmi.ascent, fmi.bottom, 0} 283 }); 284 } 285 286 @Test(expected = IndexOutOfBoundsException.class) testGetLineExtra_withNegativeValue()287 public void testGetLineExtra_withNegativeValue() { 288 final Layout layout = builder().build(); 289 layout.getLineExtra(-1); 290 } 291 292 @Test(expected = IndexOutOfBoundsException.class) testGetLineExtra_withParamGreaterThanLineCount()293 public void testGetLineExtra_withParamGreaterThanLineCount() { 294 final Layout layout = builder().build(); 295 layout.getLineExtra(100); 296 } 297 298 // ----- test utility classes and methods ----- 299 300 // Models the effect of the scale and add parameters. I think the current 301 // implementation misbehaves. 302 private static class Scaler { 303 private final float sMult; 304 private final float sAdd; 305 Scaler(float sMult, float sAdd)306 Scaler(float sMult, float sAdd) { 307 this.sMult = sMult - 1; 308 this.sAdd = sAdd; 309 } 310 scale(float height)311 public int scale(float height) { 312 int altVal = (int)(height * sMult + sAdd + 0.5); 313 int rndVal = Math.round(height * sMult + sAdd); 314 if (altVal != rndVal) { 315 Log.i("Scale", "expected scale: " + rndVal + 316 " != returned scale: " + altVal); 317 } 318 return rndVal; 319 } 320 } 321 builder()322 /* package */ static LayoutBuilder builder() { 323 return new LayoutBuilder(); 324 } 325 326 /* package */ static class LayoutBuilder { 327 String text = "This is a test"; 328 TextPaint paint = new TextPaint(); // default 329 int width = 100; 330 Alignment align = ALIGN_NORMAL; 331 float spacingMult = 1; 332 float spacingAdd = 0; 333 boolean includePad = false; 334 setText(String text)335 LayoutBuilder setText(String text) { 336 this.text = text; 337 return this; 338 } 339 setPaint(TextPaint paint)340 LayoutBuilder setPaint(TextPaint paint) { 341 this.paint = paint; 342 return this; 343 } 344 setWidth(int width)345 LayoutBuilder setWidth(int width) { 346 this.width = width; 347 return this; 348 } 349 setAlignment(Alignment align)350 LayoutBuilder setAlignment(Alignment align) { 351 this.align = align; 352 return this; 353 } 354 setSpacingMult(float spacingMult)355 LayoutBuilder setSpacingMult(float spacingMult) { 356 this.spacingMult = spacingMult; 357 return this; 358 } 359 setSpacingAdd(float spacingAdd)360 LayoutBuilder setSpacingAdd(float spacingAdd) { 361 this.spacingAdd = spacingAdd; 362 return this; 363 } 364 setIncludePad(boolean includePad)365 LayoutBuilder setIncludePad(boolean includePad) { 366 this.includePad = includePad; 367 return this; 368 } 369 build()370 Layout build() { 371 return new StaticLayout(text, paint, width, align, spacingMult, 372 spacingAdd, includePad); 373 } 374 } 375 376 /** 377 * Assert vertical metrics such as top, bottom, ascent, descent. 378 * @param l layout instance 379 * @param topPad top padding 380 * @param botPad bottom padding 381 * @param values values for each line where first is ascent, second is descent, and last one is 382 * extra 383 */ assertVertMetrics(Layout l, int topPad, int botPad, int[][] values)384 private void assertVertMetrics(Layout l, int topPad, int botPad, int[][] values) { 385 assertTopBotPadding(l, topPad, botPad); 386 assertLinesMetrics(l, values); 387 } 388 389 /** 390 * Check given expected values against the Layout values. 391 * @param l layout instance 392 * @param values values for each line where first is ascent, second is descent, and last one is 393 * extra 394 */ assertLinesMetrics(Layout l, int[][] values)395 private void assertLinesMetrics(Layout l, int[][] values) { 396 final int lines = values.length; 397 assertEquals(lines, l.getLineCount()); 398 399 int t = 0; 400 for (int i = 0, n = 0; i < lines; ++i, n += 3) { 401 if (values[i].length != 3) { 402 throw new IllegalArgumentException(String.valueOf(values.length)); 403 } 404 int a = values[i][0]; 405 int d = values[i][1]; 406 int extra = values[i][2]; 407 int h = -a + d; 408 assertLineMetrics(l, i, t, a, d, h, extra); 409 t += h; 410 } 411 412 assertEquals(t, l.getHeight()); 413 } 414 assertLineMetrics(Layout l, int line, int top, int ascent, int descent, int height, int extra)415 private void assertLineMetrics(Layout l, int line, 416 int top, int ascent, int descent, int height, int extra) { 417 String info = "line " + line; 418 assertEquals(info, top, l.getLineTop(line)); 419 assertEquals(info, ascent, l.getLineAscent(line)); 420 assertEquals(info, descent, l.getLineDescent(line)); 421 assertEquals(info, height, l.getLineBottom(line) - top); 422 assertEquals(info, extra, l.getLineExtra(line)); 423 } 424 assertTopBotPadding(Layout l, int topPad, int botPad)425 private void assertTopBotPadding(Layout l, int topPad, int botPad) { 426 assertEquals(topPad, l.getTopPadding()); 427 assertEquals(botPad, l.getBottomPadding()); 428 } 429 moveCursorToRightCursorableOffset(EditorState state, TextPaint paint)430 private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) { 431 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 432 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 433 final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart); 434 state.mSelectionStart = state.mSelectionEnd = newOffset; 435 } 436 moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint)437 private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) { 438 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 439 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 440 final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart); 441 state.mSelectionStart = state.mSelectionEnd = newOffset; 442 } 443 444 /** 445 * Tests for keycap, variation selectors, flags are in CTS. 446 * See {@link android.text.cts.StaticLayoutTest}. 447 */ 448 @Test testEmojiOffset()449 public void testEmojiOffset() { 450 EditorState state = new EditorState(); 451 TextPaint paint = new TextPaint(); 452 453 // Odd numbered regional indicator symbols. 454 // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL 455 // LETTER C. 456 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 457 moveCursorToRightCursorableOffset(state, paint); 458 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 459 moveCursorToRightCursorableOffset(state, paint); 460 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 461 moveCursorToRightCursorableOffset(state, paint); 462 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 463 moveCursorToRightCursorableOffset(state, paint); 464 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 465 moveCursorToLeftCursorableOffset(state, paint); 466 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 467 moveCursorToLeftCursorableOffset(state, paint); 468 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 469 moveCursorToLeftCursorableOffset(state, paint); 470 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 471 moveCursorToLeftCursorableOffset(state, paint); 472 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 473 moveCursorToLeftCursorableOffset(state, paint); 474 475 // Zero width sequence 476 final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468"; 477 state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 478 moveCursorToRightCursorableOffset(state, paint); 479 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 480 moveCursorToRightCursorableOffset(state, paint); 481 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 482 moveCursorToRightCursorableOffset(state, paint); 483 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 484 moveCursorToRightCursorableOffset(state, paint); 485 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 486 moveCursorToLeftCursorableOffset(state, paint); 487 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 488 moveCursorToLeftCursorableOffset(state, paint); 489 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 490 moveCursorToLeftCursorableOffset(state, paint); 491 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 492 moveCursorToLeftCursorableOffset(state, paint); 493 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 494 moveCursorToLeftCursorableOffset(state, paint); 495 496 // Emoji modifiers 497 // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2. 498 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 499 moveCursorToRightCursorableOffset(state, paint); 500 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 501 moveCursorToRightCursorableOffset(state, paint); 502 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 503 moveCursorToRightCursorableOffset(state, paint); 504 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 505 moveCursorToRightCursorableOffset(state, paint); 506 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 507 moveCursorToLeftCursorableOffset(state, paint); 508 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 509 moveCursorToLeftCursorableOffset(state, paint); 510 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 511 moveCursorToLeftCursorableOffset(state, paint); 512 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 513 moveCursorToLeftCursorableOffset(state, paint); 514 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 515 moveCursorToLeftCursorableOffset(state, paint); 516 } 517 createEllipsizeStaticLayout(CharSequence text, TextUtils.TruncateAt ellipsize, int maxLines)518 private StaticLayout createEllipsizeStaticLayout(CharSequence text, 519 TextUtils.TruncateAt ellipsize, int maxLines) { 520 return new StaticLayout(text, 0, text.length(), 521 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, 522 TextDirectionHeuristics.FIRSTSTRONG_LTR, 523 SPACE_MULTI, SPACE_ADD, true /* include pad */, 524 ellipsize, 525 ELLIPSIZE_WIDTH, 526 maxLines); 527 } 528 529 @Test testEllipsis_singleLine()530 public void testEllipsis_singleLine() { 531 { 532 // Single line case and TruncateAt.END so that we have some ellipsis 533 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 534 TextUtils.TruncateAt.END, 1); 535 assertTrue(layout.getEllipsisCount(0) > 0); 536 } 537 { 538 // Single line case and TruncateAt.MIDDLE so that we have some ellipsis 539 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 540 TextUtils.TruncateAt.MIDDLE, 1); 541 assertTrue(layout.getEllipsisCount(0) > 0); 542 } 543 { 544 // Single line case and TruncateAt.END so that we have some ellipsis 545 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 546 TextUtils.TruncateAt.END, 1); 547 assertTrue(layout.getEllipsisCount(0) > 0); 548 } 549 { 550 // Single line case and TruncateAt.MARQUEE so that we have NO ellipsis 551 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, 552 TextUtils.TruncateAt.MARQUEE, 1); 553 assertTrue(layout.getEllipsisCount(0) == 0); 554 } 555 { 556 final String text = "\u3042" // HIRAGANA LETTER A 557 + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; 558 final float textWidth = mDefaultPaint.measureText(text); 559 final int halfWidth = (int) (textWidth / 2.0f); 560 { 561 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 562 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 563 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); 564 assertTrue(layout.getEllipsisCount(0) > 0); 565 assertTrue(layout.getEllipsisStart(0) > 0); 566 } 567 { 568 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 569 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 570 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1); 571 assertTrue(layout.getEllipsisCount(0) > 0); 572 assertEquals(0, mDefaultLayout.getEllipsisStart(0)); 573 } 574 { 575 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 576 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 577 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1); 578 assertTrue(layout.getEllipsisCount(0) > 0); 579 assertTrue(layout.getEllipsisStart(0) > 0); 580 } 581 { 582 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 583 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 584 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1); 585 assertEquals(0, layout.getEllipsisCount(0)); 586 } 587 } 588 589 { 590 // The white spaces in this text will be trailing if maxLines is larger than 1, but 591 // width of the trailing white spaces must not be ignored if ellipsis is applied. 592 final String text = "abc def"; 593 final float textWidth = mDefaultPaint.measureText(text); 594 final int halfWidth = (int) (textWidth / 2.0f); 595 { 596 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 597 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 598 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); 599 assertTrue(layout.getEllipsisCount(0) > 0); 600 assertTrue(layout.getEllipsisStart(0) > 0); 601 } 602 } 603 604 { 605 // 2 family emojis (11 code units + 11 code units). 606 final String text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66" 607 + "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; 608 final float textWidth = mDefaultPaint.measureText(text); 609 610 final TextUtils.TruncateAt[] kinds = {TextUtils.TruncateAt.START, 611 TextUtils.TruncateAt.MIDDLE, TextUtils.TruncateAt.END}; 612 for (final TextUtils.TruncateAt kind : kinds) { 613 for (int i = 0; i <= 8; i++) { 614 int avail = (int) (textWidth * i / 7.0f); 615 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, 616 avail, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, 617 SPACE_MULTI, SPACE_ADD, false, kind, avail, 1); 618 619 assertTrue(layout.getEllipsisCount(0) == text.length() 620 || layout.getEllipsisCount(0) == text.length() / 2 621 || layout.getEllipsisCount(0) == 0); 622 } 623 } 624 } 625 } 626 627 // String wrapper for testing not well known implementation of CharSequence. 628 private class FakeCharSequence implements CharSequence { 629 private String mStr; 630 FakeCharSequence(String str)631 FakeCharSequence(String str) { 632 mStr = str; 633 } 634 635 @Override charAt(int index)636 public char charAt(int index) { 637 return mStr.charAt(index); 638 } 639 640 @Override length()641 public int length() { 642 return mStr.length(); 643 } 644 645 @Override subSequence(int start, int end)646 public CharSequence subSequence(int start, int end) { 647 return mStr.subSequence(start, end); 648 } 649 650 @Override toString()651 public String toString() { 652 return mStr; 653 } 654 }; 655 buildTestCharSequences(String testString, Normalizer.Form[] forms)656 private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) { 657 List<CharSequence> result = new ArrayList<>(); 658 659 List<String> normalizedStrings = new ArrayList<>(); 660 for (Normalizer.Form form: forms) { 661 normalizedStrings.add(Normalizer.normalize(testString, form)); 662 } 663 664 for (String str: normalizedStrings) { 665 result.add(str); 666 result.add(new SpannedString(str)); 667 result.add(new SpannableString(str)); 668 result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation. 669 result.add(new FakeCharSequence(str)); // as a not well known implementation. 670 } 671 return result; 672 } 673 buildTestMessage(CharSequence seq)674 private String buildTestMessage(CharSequence seq) { 675 String normalized; 676 if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) { 677 normalized = "NFC"; 678 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) { 679 normalized = "NFD"; 680 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) { 681 normalized = "NFKC"; 682 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) { 683 normalized = "NFKD"; 684 } else { 685 throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD"); 686 } 687 688 StringBuilder builder = new StringBuilder(); 689 for (int i = 0; i < seq.length(); ++i) { 690 builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i)))); 691 } 692 693 return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" 694 + ", class: " + seq.getClass().getName() 695 + ", Normalization: " + normalized; 696 } 697 698 @Test testGetOffset_UNICODE_Hebrew()699 public void testGetOffset_UNICODE_Hebrew() { 700 String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters 701 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { 702 StaticLayout.Builder b = StaticLayout.Builder.obtain( 703 seq, 0, seq.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH) 704 .setAlignment(DEFAULT_ALIGN) 705 .setTextDirection(TextDirectionHeuristics.RTL) 706 .setLineSpacing(SPACE_ADD, SPACE_MULTI) 707 .setIncludePad(true); 708 StaticLayout layout = b.build(); 709 710 String testLabel = buildTestMessage(seq); 711 712 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0)); 713 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1)); 714 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2)); 715 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3)); 716 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4)); 717 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5)); 718 719 assertEquals(testLabel, 0, layout.getOffsetToRightOf(0)); 720 assertEquals(testLabel, 0, layout.getOffsetToRightOf(1)); 721 assertEquals(testLabel, 1, layout.getOffsetToRightOf(2)); 722 assertEquals(testLabel, 2, layout.getOffsetToRightOf(3)); 723 assertEquals(testLabel, 3, layout.getOffsetToRightOf(4)); 724 assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); 725 } 726 } 727 728 @Test testLocaleSpanAffectsHyphenation()729 public void testLocaleSpanAffectsHyphenation() { 730 TextPaint paint = new TextPaint(); 731 paint.setTextLocale(Locale.US); 732 // Private use language, with no hyphenation rules. 733 final Locale privateLocale = Locale.forLanguageTag("qaa"); 734 735 final String longWord = "philanthropic"; 736 final float wordWidth = paint.measureText(longWord); 737 // Wide enough that words get hyphenated by default. 738 final int paraWidth = Math.round(wordWidth * 1.8f); 739 final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " " 740 + longWord + " " + longWord; 741 742 final int numEnglishLines = StaticLayout.Builder 743 .obtain(sentence, 0, sentence.length(), paint, paraWidth) 744 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 745 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 746 .build() 747 .getLineCount(); 748 749 { 750 final SpannableString text = new SpannableString(sentence); 751 text.setSpan(new LocaleSpan(privateLocale), 0, text.length(), 752 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 753 final int numPrivateLocaleLines = StaticLayout.Builder 754 .obtain(text, 0, text.length(), paint, paraWidth) 755 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 756 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 757 .build() 758 .getLineCount(); 759 760 // Since the paragraph set to English gets hyphenated, the number of lines would be 761 // smaller than the number of lines when there is a span setting a language that 762 // doesn't get hyphenated. 763 assertTrue(numEnglishLines < numPrivateLocaleLines); 764 } 765 { 766 // Same as the above test, except that the locale span now uses a locale list starting 767 // with the private non-hyphenating locale. 768 final SpannableString text = new SpannableString(sentence); 769 final LocaleList locales = new LocaleList(privateLocale, Locale.US); 770 text.setSpan(new LocaleSpan(locales), 0, text.length(), 771 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 772 final int numPrivateLocaleLines = StaticLayout.Builder 773 .obtain(text, 0, text.length(), paint, paraWidth) 774 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 775 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 776 .build() 777 .getLineCount(); 778 779 assertTrue(numEnglishLines < numPrivateLocaleLines); 780 } 781 { 782 final SpannableString text = new SpannableString(sentence); 783 // Apply the private LocaleSpan only to the first word, which is not getting hyphenated 784 // anyway. 785 text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(), 786 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 787 final int numPrivateLocaleLines = StaticLayout.Builder 788 .obtain(text, 0, text.length(), paint, paraWidth) 789 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) 790 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 791 .build() 792 .getLineCount(); 793 794 // Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan 795 // should not affect the layout. 796 assertEquals(numEnglishLines, numPrivateLocaleLines); 797 } 798 } 799 800 @Test 801 public void testLayoutDoesntModifyPaint() { 802 final TextPaint paint = new TextPaint(); 803 paint.setStartHyphenEdit(Paint.START_HYPHEN_EDIT_INSERT_HYPHEN); 804 paint.setEndHyphenEdit(Paint.END_HYPHEN_EDIT_INSERT_HYPHEN); 805 final StaticLayout layout = StaticLayout.Builder.obtain("", 0, 0, paint, 100).build(); 806 final Canvas canvas = new Canvas(); 807 layout.drawText(canvas, 0, 0); 808 assertEquals(Paint.START_HYPHEN_EDIT_INSERT_HYPHEN, paint.getStartHyphenEdit()); 809 assertEquals(Paint.END_HYPHEN_EDIT_INSERT_HYPHEN, paint.getEndHyphenEdit()); 810 } 811 812 @Test 813 public void testFallbackLineSpacing() { 814 // All glyphs in the fonts are 1em wide. 815 final String[] testFontFiles = { 816 // ascent == 1em, descent == 2em, only supports 'a' and space 817 "ascent1em-descent2em.ttf", 818 // ascent == 3em, descent == 4em, only supports 'b' 819 "ascent3em-descent4em.ttf" 820 }; 821 final String xml = "<?xml version='1.0' encoding='UTF-8'?>" 822 + "<familyset>" 823 + " <family name='sans-serif'>" 824 + " <font weight='400' style='normal'>ascent1em-descent2em.ttf</font>" 825 + " </family>" 826 + " <family>" 827 + " <font weight='400' style='normal'>ascent3em-descent4em.ttf</font>" 828 + " </family>" 829 + " <family>" 830 + " <font weight='400' style='normal'>ascent10em-descent10em.ttf</font>" 831 + " </family>" 832 + "</familyset>"; 833 834 try (FontFallbackSetup setup = 835 new FontFallbackSetup("StaticLayout", testFontFiles, xml)) { 836 final TextPaint paint = setup.getPaintFor("sans-serif"); 837 final int textSize = 100; 838 paint.setTextSize(textSize); 839 assertEquals(-textSize, paint.ascent(), 0.0f); 840 assertEquals(2 * textSize, paint.descent(), 0.0f); 841 842 final int paraWidth = 5 * textSize; 843 final String text = "aaaaa\naabaa\naaaaa\n"; // This should result in three lines. 844 845 // Old line spacing. All lines should get their ascent and descents from the first font. 846 StaticLayout layout = StaticLayout.Builder 847 .obtain(text, 0, text.length(), paint, paraWidth) 848 .setIncludePad(false) 849 .setUseLineSpacingFromFallbacks(false) 850 .build(); 851 assertEquals(4, layout.getLineCount()); 852 assertEquals(-textSize, layout.getLineAscent(0)); 853 assertEquals(2 * textSize, layout.getLineDescent(0)); 854 assertEquals(-textSize, layout.getLineAscent(1)); 855 assertEquals(2 * textSize, layout.getLineDescent(1)); 856 assertEquals(-textSize, layout.getLineAscent(2)); 857 assertEquals(2 * textSize, layout.getLineDescent(2)); 858 // The last empty line spacing should be the default line spacing. 859 // Maybe good to be a previous line spacing? 860 assertEquals(-textSize, layout.getLineAscent(3)); 861 assertEquals(2 * textSize, layout.getLineDescent(3)); 862 863 // New line spacing. The second line has a 'b', so it needs more ascent and descent. 864 layout = StaticLayout.Builder 865 .obtain(text, 0, text.length(), paint, paraWidth) 866 .setIncludePad(false) 867 .setUseLineSpacingFromFallbacks(true) 868 .build(); 869 assertEquals(4, layout.getLineCount()); 870 assertEquals(-textSize, layout.getLineAscent(0)); 871 assertEquals(2 * textSize, layout.getLineDescent(0)); 872 assertEquals(-3 * textSize, layout.getLineAscent(1)); 873 assertEquals(4 * textSize, layout.getLineDescent(1)); 874 assertEquals(-textSize, layout.getLineAscent(2)); 875 assertEquals(2 * textSize, layout.getLineDescent(2)); 876 assertEquals(-textSize, layout.getLineAscent(3)); 877 assertEquals(2 * textSize, layout.getLineDescent(3)); 878 879 // The default is the old line spacing, for backward compatibility. 880 layout = StaticLayout.Builder 881 .obtain(text, 0, text.length(), paint, paraWidth) 882 .setIncludePad(false) 883 .build(); 884 assertEquals(4, layout.getLineCount()); 885 assertEquals(-textSize, layout.getLineAscent(0)); 886 assertEquals(2 * textSize, layout.getLineDescent(0)); 887 assertEquals(-textSize, layout.getLineAscent(1)); 888 assertEquals(2 * textSize, layout.getLineDescent(1)); 889 assertEquals(-textSize, layout.getLineAscent(2)); 890 assertEquals(2 * textSize, layout.getLineDescent(2)); 891 assertEquals(-textSize, layout.getLineAscent(3)); 892 assertEquals(2 * textSize, layout.getLineDescent(3)); 893 894 layout = StaticLayout.Builder 895 .obtain("\n", 0, 1, paint, textSize) 896 .setIncludePad(false) 897 .setUseLineSpacingFromFallbacks(false) 898 .build(); 899 assertEquals(2, layout.getLineCount()); 900 assertEquals(-textSize, layout.getLineAscent(0)); 901 assertEquals(2 * textSize, layout.getLineDescent(0)); 902 assertEquals(-textSize, layout.getLineAscent(1)); 903 assertEquals(2 * textSize, layout.getLineDescent(1)); 904 905 layout = StaticLayout.Builder 906 .obtain("\n", 0, 1, paint, textSize) 907 .setIncludePad(false) 908 .setUseLineSpacingFromFallbacks(true) 909 .build(); 910 assertEquals(2, layout.getLineCount()); 911 assertEquals(-textSize, layout.getLineAscent(0)); 912 assertEquals(2 * textSize, layout.getLineDescent(0)); 913 assertEquals(-textSize, layout.getLineAscent(1)); 914 assertEquals(2 * textSize, layout.getLineDescent(1)); 915 } 916 } 917 918 @Test 919 public void testGetHeight_zeroMaxLines() { 920 final String text = "a\nb"; 921 final TextPaint paint = new TextPaint(); 922 final StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(), paint, 923 Integer.MAX_VALUE).setMaxLines(0).build(); 924 925 assertEquals(0, layout.getHeight(true)); 926 assertEquals(2, layout.getLineCount()); 927 } 928 } 929