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 import static org.junit.Assert.assertEquals; 21 22 import android.graphics.Paint.FontMetricsInt; 23 import android.support.test.filters.SmallTest; 24 import android.support.test.runner.AndroidJUnit4; 25 import android.text.Layout.Alignment; 26 import android.text.method.EditorState; 27 import android.util.Log; 28 29 import org.junit.Test; 30 import org.junit.runner.RunWith; 31 32 /** 33 * Tests StaticLayout vertical metrics behavior. 34 */ 35 @SmallTest 36 @RunWith(AndroidJUnit4.class) 37 public class StaticLayoutTest { 38 /** 39 * Basic test showing expected behavior and relationship between font 40 * metrics and line metrics. 41 */ 42 @Test testGetters1()43 public void testGetters1() { 44 LayoutBuilder b = builder(); 45 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 46 47 // check default paint 48 Log.i("TG1:paint", fmi.toString()); 49 50 Layout l = b.build(); 51 assertVertMetrics(l, 0, 0, 52 fmi.ascent, fmi.descent); 53 54 // other quick metrics 55 assertEquals(0, l.getLineStart(0)); 56 assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0)); 57 assertEquals(false, l.getLineContainsTab(0)); 58 assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0)); 59 assertEquals(0, l.getEllipsisCount(0)); 60 assertEquals(0, l.getEllipsisStart(0)); 61 assertEquals(b.width, l.getEllipsizedWidth()); 62 } 63 64 /** 65 * Basic test showing effect of includePad = true with 1 line. 66 * Top and bottom padding are affected, as is the line descent and height. 67 */ 68 @Test testGetters2()69 public void testGetters2() { 70 LayoutBuilder b = builder() 71 .setIncludePad(true); 72 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 73 74 Layout l = b.build(); 75 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 76 fmi.top, fmi.bottom); 77 } 78 79 /** 80 * Basic test showing effect of includePad = true wrapping to 2 lines. 81 * Ascent of top line and descent of bottom line are affected. 82 */ 83 @Test testGetters3()84 public void testGetters3() { 85 LayoutBuilder b = builder() 86 .setIncludePad(true) 87 .setWidth(50); 88 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 89 90 Layout l = b.build(); 91 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 92 fmi.top, fmi.descent, 93 fmi.ascent, fmi.bottom); 94 } 95 96 /** 97 * Basic test showing effect of includePad = true wrapping to 3 lines. 98 * First line ascent is top, bottom line descent is bottom. 99 */ 100 @Test testGetters4()101 public void testGetters4() { 102 LayoutBuilder b = builder() 103 .setText("This is a longer test") 104 .setIncludePad(true) 105 .setWidth(50); 106 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 107 108 Layout l = b.build(); 109 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 110 fmi.top, fmi.descent, 111 fmi.ascent, fmi.descent, 112 fmi.ascent, fmi.bottom); 113 } 114 115 /** 116 * Basic test showing effect of includePad = true wrapping to 3 lines and 117 * large text. See effect of leading. Currently, we don't expect there to 118 * even be non-zero leading. 119 */ 120 @Test testGetters5()121 public void testGetters5() { 122 LayoutBuilder b = builder() 123 .setText("This is a longer test") 124 .setIncludePad(true) 125 .setWidth(150); 126 b.paint.setTextSize(36); 127 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 128 129 if (fmi.leading == 0) { // nothing to test 130 Log.i("TG5", "leading is 0, skipping test"); 131 return; 132 } 133 134 // So far, leading is not used, so this is the same as TG4. If we start 135 // using leading, this will fail. 136 Layout l = b.build(); 137 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 138 fmi.top, fmi.descent, 139 fmi.ascent, fmi.descent, 140 fmi.ascent, fmi.bottom); 141 } 142 143 /** 144 * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping 145 * to 3 lines. 146 */ 147 @Test testGetters6()148 public void testGetters6() { 149 int spacingAdd = 2; // int so expressions return int 150 LayoutBuilder b = builder() 151 .setText("This is a longer test") 152 .setIncludePad(true) 153 .setWidth(50) 154 .setSpacingAdd(spacingAdd); 155 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 156 157 Layout l = b.build(); 158 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 159 fmi.top, fmi.descent + spacingAdd, 160 fmi.ascent, fmi.descent + spacingAdd, 161 fmi.ascent, fmi.bottom); 162 } 163 164 /** 165 * Basic test showing effect of includePad = true, spacingAdd = 2, 166 * spacingMult = 1.5, wrapping to 3 lines. 167 */ 168 @Test testGetters7()169 public void testGetters7() { 170 LayoutBuilder b = builder() 171 .setText("This is a longer test") 172 .setIncludePad(true) 173 .setWidth(50) 174 .setSpacingAdd(2) 175 .setSpacingMult(1.5f); 176 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 177 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 178 179 Layout l = b.build(); 180 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 181 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 182 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 183 fmi.ascent, fmi.bottom); 184 } 185 186 /** 187 * Basic test showing effect of includePad = true, spacingAdd = 0, 188 * spacingMult = 0.8 when wrapping to 3 lines. 189 */ 190 @Test testGetters8()191 public void testGetters8() { 192 LayoutBuilder b = builder() 193 .setText("This is a longer test") 194 .setIncludePad(true) 195 .setWidth(50) 196 .setSpacingAdd(2) 197 .setSpacingMult(.8f); 198 FontMetricsInt fmi = b.paint.getFontMetricsInt(); 199 Scaler s = new Scaler(b.spacingMult, b.spacingAdd); 200 201 Layout l = b.build(); 202 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent, 203 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top), 204 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent), 205 fmi.ascent, fmi.bottom); 206 } 207 208 // ----- test utility classes and methods ----- 209 210 // Models the effect of the scale and add parameters. I think the current 211 // implementation misbehaves. 212 private static class Scaler { 213 private final float sMult; 214 private final float sAdd; 215 Scaler(float sMult, float sAdd)216 Scaler(float sMult, float sAdd) { 217 this.sMult = sMult - 1; 218 this.sAdd = sAdd; 219 } 220 scale(float height)221 public int scale(float height) { 222 int altVal = (int)(height * sMult + sAdd + 0.5); 223 int rndVal = Math.round(height * sMult + sAdd); 224 if (altVal != rndVal) { 225 Log.i("Scale", "expected scale: " + rndVal + 226 " != returned scale: " + altVal); 227 } 228 return rndVal; 229 } 230 } 231 builder()232 /* package */ static LayoutBuilder builder() { 233 return new LayoutBuilder(); 234 } 235 236 /* package */ static class LayoutBuilder { 237 String text = "This is a test"; 238 TextPaint paint = new TextPaint(); // default 239 int width = 100; 240 Alignment align = ALIGN_NORMAL; 241 float spacingMult = 1; 242 float spacingAdd = 0; 243 boolean includePad = false; 244 setText(String text)245 LayoutBuilder setText(String text) { 246 this.text = text; 247 return this; 248 } 249 setPaint(TextPaint paint)250 LayoutBuilder setPaint(TextPaint paint) { 251 this.paint = paint; 252 return this; 253 } 254 setWidth(int width)255 LayoutBuilder setWidth(int width) { 256 this.width = width; 257 return this; 258 } 259 setAlignment(Alignment align)260 LayoutBuilder setAlignment(Alignment align) { 261 this.align = align; 262 return this; 263 } 264 setSpacingMult(float spacingMult)265 LayoutBuilder setSpacingMult(float spacingMult) { 266 this.spacingMult = spacingMult; 267 return this; 268 } 269 setSpacingAdd(float spacingAdd)270 LayoutBuilder setSpacingAdd(float spacingAdd) { 271 this.spacingAdd = spacingAdd; 272 return this; 273 } 274 setIncludePad(boolean includePad)275 LayoutBuilder setIncludePad(boolean includePad) { 276 this.includePad = includePad; 277 return this; 278 } 279 build()280 Layout build() { 281 return new StaticLayout(text, paint, width, align, spacingMult, 282 spacingAdd, includePad); 283 } 284 } 285 assertVertMetrics(Layout l, int topPad, int botPad, int... values)286 private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) { 287 assertTopBotPadding(l, topPad, botPad); 288 assertLinesMetrics(l, values); 289 } 290 assertLinesMetrics(Layout l, int... values)291 private void assertLinesMetrics(Layout l, int... values) { 292 // sanity check 293 if ((values.length & 0x1) != 0) { 294 throw new IllegalArgumentException(String.valueOf(values.length)); 295 } 296 297 int lines = values.length >> 1; 298 assertEquals(lines, l.getLineCount()); 299 300 int t = 0; 301 for (int i = 0, n = 0; i < lines; ++i, n += 2) { 302 int a = values[n]; 303 int d = values[n+1]; 304 int h = -a + d; 305 assertLineMetrics(l, i, t, a, d, h); 306 t += h; 307 } 308 309 assertEquals(t, l.getHeight()); 310 } 311 assertLineMetrics(Layout l, int line, int top, int ascent, int descent, int height)312 private void assertLineMetrics(Layout l, int line, 313 int top, int ascent, int descent, int height) { 314 String info = "line " + line; 315 assertEquals(info, top, l.getLineTop(line)); 316 assertEquals(info, ascent, l.getLineAscent(line)); 317 assertEquals(info, descent, l.getLineDescent(line)); 318 assertEquals(info, height, l.getLineBottom(line) - top); 319 } 320 assertTopBotPadding(Layout l, int topPad, int botPad)321 private void assertTopBotPadding(Layout l, int topPad, int botPad) { 322 assertEquals(topPad, l.getTopPadding()); 323 assertEquals(botPad, l.getBottomPadding()); 324 } 325 moveCursorToRightCursorableOffset(EditorState state, TextPaint paint)326 private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) { 327 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 328 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 329 final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart); 330 state.mSelectionStart = state.mSelectionEnd = newOffset; 331 } 332 moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint)333 private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) { 334 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd); 335 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build(); 336 final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart); 337 state.mSelectionStart = state.mSelectionEnd = newOffset; 338 } 339 340 /** 341 * Tests for keycap, variation selectors, flags are in CTS. 342 * See {@link android.text.cts.StaticLayoutTest}. 343 */ 344 @Test testEmojiOffset()345 public void testEmojiOffset() { 346 EditorState state = new EditorState(); 347 TextPaint paint = new TextPaint(); 348 349 // Odd numbered regional indicator symbols. 350 // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL 351 // LETTER C. 352 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 353 moveCursorToRightCursorableOffset(state, paint); 354 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 355 moveCursorToRightCursorableOffset(state, paint); 356 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 357 moveCursorToRightCursorableOffset(state, paint); 358 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 359 moveCursorToRightCursorableOffset(state, paint); 360 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |"); 361 moveCursorToLeftCursorableOffset(state, paint); 362 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6"); 363 moveCursorToLeftCursorableOffset(state, paint); 364 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6"); 365 moveCursorToLeftCursorableOffset(state, paint); 366 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 367 moveCursorToLeftCursorableOffset(state, paint); 368 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6"); 369 moveCursorToLeftCursorableOffset(state, paint); 370 371 // Zero width sequence 372 final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468"; 373 state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 374 moveCursorToRightCursorableOffset(state, paint); 375 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 376 moveCursorToRightCursorableOffset(state, paint); 377 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 378 moveCursorToRightCursorableOffset(state, paint); 379 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 380 moveCursorToRightCursorableOffset(state, paint); 381 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |"); 382 moveCursorToLeftCursorableOffset(state, paint); 383 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence); 384 moveCursorToLeftCursorableOffset(state, paint); 385 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence); 386 moveCursorToLeftCursorableOffset(state, paint); 387 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 388 moveCursorToLeftCursorableOffset(state, paint); 389 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence); 390 moveCursorToLeftCursorableOffset(state, paint); 391 392 // Emoji modifiers 393 // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2. 394 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 395 moveCursorToRightCursorableOffset(state, paint); 396 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 397 moveCursorToRightCursorableOffset(state, paint); 398 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 399 moveCursorToRightCursorableOffset(state, paint); 400 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 401 moveCursorToRightCursorableOffset(state, paint); 402 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |"); 403 moveCursorToLeftCursorableOffset(state, paint); 404 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB"); 405 moveCursorToLeftCursorableOffset(state, paint); 406 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB"); 407 moveCursorToLeftCursorableOffset(state, paint); 408 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 409 moveCursorToLeftCursorableOffset(state, paint); 410 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); 411 moveCursorToLeftCursorableOffset(state, paint); 412 } 413 } 414