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