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