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