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