blob: fb60e38fcbf6070868220b0f3b9dfb994ee26b85 [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
Doug Felt6ad5a7a2010-02-19 15:44:35 -080024import android.graphics.Paint.FontMetricsInt;
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -070025import android.os.LocaleList;
Siyamed Sinir68089c82016-06-29 16:55:35 -070026import android.support.test.filters.SmallTest;
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +010027import android.support.test.runner.AndroidJUnit4;
Doug Felt6ad5a7a2010-02-19 15:44:35 -080028import android.text.Layout.Alignment;
Seigo Nonakaf4afb092016-03-15 15:50:06 +090029import android.text.method.EditorState;
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -070030import android.text.style.LocaleSpan;
Doug Felt6ad5a7a2010-02-19 15:44:35 -080031import android.util.Log;
32
Roozbeh Pournader02f167c2017-06-06 11:31:33 -070033import org.junit.Before;
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +010034import org.junit.Test;
35import org.junit.runner.RunWith;
Siyamed Sinir68089c82016-06-29 16:55:35 -070036
Roozbeh Pournader02f167c2017-06-06 11:31:33 -070037import java.text.Normalizer;
38import java.util.ArrayList;
39import java.util.List;
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -070040import java.util.Locale;
Roozbeh Pournader02f167c2017-06-06 11:31:33 -070041
Doug Felt6ad5a7a2010-02-19 15:44:35 -080042/**
43 * Tests StaticLayout vertical metrics behavior.
44 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +010045@SmallTest
46@RunWith(AndroidJUnit4.class)
47public class StaticLayoutTest {
Roozbeh Pournader02f167c2017-06-06 11:31:33 -070048 private static final float SPACE_MULTI = 1.0f;
49 private static final float SPACE_ADD = 0.0f;
50 private static final int DEFAULT_OUTER_WIDTH = 150;
51
52 private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar"
53 + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong";
54 private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence";
55
56 private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
57 private static final int ELLIPSIZE_WIDTH = 8;
58
59 private StaticLayout mDefaultLayout;
60 private TextPaint mDefaultPaint;
61
62 @Before
63 public void setup() {
64 mDefaultPaint = new TextPaint();
65 mDefaultLayout = createDefaultStaticLayout();
66 }
67
68 private StaticLayout createDefaultStaticLayout() {
69 return new StaticLayout(LAYOUT_TEXT, mDefaultPaint,
70 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
71 }
72
73 @Test
74 public void testBuilder() {
75 {
76 // Obtain.
77 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
78 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
79 final StaticLayout layout = builder.build();
80 // Check default value.
81 assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
82 layout.getTextDirectionHeuristic());
83 }
84 {
85 // setTextDirection.
86 final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
87 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
88 builder.setTextDirection(TextDirectionHeuristics.RTL);
89 final StaticLayout layout = builder.build();
90 // Always returns TextDirectionHeuristics.FIRSTSTRONG_LTR.
91 assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
92 layout.getTextDirectionHeuristic());
93 }
94 }
95
Doug Felt6ad5a7a2010-02-19 15:44:35 -080096 /**
97 * Basic test showing expected behavior and relationship between font
98 * metrics and line metrics.
99 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100100 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800101 public void testGetters1() {
102 LayoutBuilder b = builder();
103 FontMetricsInt fmi = b.paint.getFontMetricsInt();
104
105 // check default paint
106 Log.i("TG1:paint", fmi.toString());
107
108 Layout l = b.build();
109 assertVertMetrics(l, 0, 0,
110 fmi.ascent, fmi.descent);
111
112 // other quick metrics
113 assertEquals(0, l.getLineStart(0));
114 assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0));
115 assertEquals(false, l.getLineContainsTab(0));
116 assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0));
117 assertEquals(0, l.getEllipsisCount(0));
118 assertEquals(0, l.getEllipsisStart(0));
119 assertEquals(b.width, l.getEllipsizedWidth());
120 }
121
122 /**
123 * Basic test showing effect of includePad = true with 1 line.
124 * Top and bottom padding are affected, as is the line descent and height.
125 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100126 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800127 public void testGetters2() {
128 LayoutBuilder b = builder()
129 .setIncludePad(true);
130 FontMetricsInt fmi = b.paint.getFontMetricsInt();
131
132 Layout l = b.build();
133 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
134 fmi.top, fmi.bottom);
135 }
136
137 /**
138 * Basic test showing effect of includePad = true wrapping to 2 lines.
139 * Ascent of top line and descent of bottom line are affected.
140 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100141 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800142 public void testGetters3() {
143 LayoutBuilder b = builder()
144 .setIncludePad(true)
145 .setWidth(50);
146 FontMetricsInt fmi = b.paint.getFontMetricsInt();
147
148 Layout l = b.build();
149 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
150 fmi.top, fmi.descent,
151 fmi.ascent, fmi.bottom);
152 }
153
154 /**
155 * Basic test showing effect of includePad = true wrapping to 3 lines.
156 * First line ascent is top, bottom line descent is bottom.
157 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100158 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800159 public void testGetters4() {
160 LayoutBuilder b = builder()
161 .setText("This is a longer test")
162 .setIncludePad(true)
163 .setWidth(50);
164 FontMetricsInt fmi = b.paint.getFontMetricsInt();
165
166 Layout l = b.build();
167 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
168 fmi.top, fmi.descent,
169 fmi.ascent, fmi.descent,
170 fmi.ascent, fmi.bottom);
171 }
172
173 /**
174 * Basic test showing effect of includePad = true wrapping to 3 lines and
175 * large text. See effect of leading. Currently, we don't expect there to
176 * even be non-zero leading.
177 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100178 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800179 public void testGetters5() {
180 LayoutBuilder b = builder()
181 .setText("This is a longer test")
182 .setIncludePad(true)
183 .setWidth(150);
184 b.paint.setTextSize(36);
185 FontMetricsInt fmi = b.paint.getFontMetricsInt();
186
187 if (fmi.leading == 0) { // nothing to test
188 Log.i("TG5", "leading is 0, skipping test");
189 return;
190 }
191
192 // So far, leading is not used, so this is the same as TG4. If we start
193 // using leading, this will fail.
194 Layout l = b.build();
195 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
196 fmi.top, fmi.descent,
197 fmi.ascent, fmi.descent,
198 fmi.ascent, fmi.bottom);
199 }
200
201 /**
202 * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping
203 * to 3 lines.
204 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100205 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800206 public void testGetters6() {
207 int spacingAdd = 2; // int so expressions return int
208 LayoutBuilder b = builder()
209 .setText("This is a longer test")
210 .setIncludePad(true)
211 .setWidth(50)
212 .setSpacingAdd(spacingAdd);
213 FontMetricsInt fmi = b.paint.getFontMetricsInt();
214
215 Layout l = b.build();
216 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
217 fmi.top, fmi.descent + spacingAdd,
218 fmi.ascent, fmi.descent + spacingAdd,
Siyamed Sinired09ae12016-02-16 14:36:26 -0800219 fmi.ascent, fmi.bottom);
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800220 }
221
222 /**
223 * Basic test showing effect of includePad = true, spacingAdd = 2,
224 * spacingMult = 1.5, wrapping to 3 lines.
225 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100226 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800227 public void testGetters7() {
228 LayoutBuilder b = builder()
229 .setText("This is a longer test")
230 .setIncludePad(true)
231 .setWidth(50)
232 .setSpacingAdd(2)
233 .setSpacingMult(1.5f);
234 FontMetricsInt fmi = b.paint.getFontMetricsInt();
235 Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
236
237 Layout l = b.build();
238 assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
239 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
240 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
Siyamed Sinired09ae12016-02-16 14:36:26 -0800241 fmi.ascent, fmi.bottom);
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800242 }
243
244 /**
245 * Basic test showing effect of includePad = true, spacingAdd = 0,
246 * spacingMult = 0.8 when wrapping to 3 lines.
247 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100248 @Test
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800249 public void testGetters8() {
250 LayoutBuilder b = builder()
251 .setText("This is a longer test")
252 .setIncludePad(true)
253 .setWidth(50)
254 .setSpacingAdd(2)
255 .setSpacingMult(.8f);
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,
261 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
262 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
Siyamed Sinired09ae12016-02-16 14:36:26 -0800263 fmi.ascent, fmi.bottom);
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800264 }
265
266 // ----- test utility classes and methods -----
267
268 // Models the effect of the scale and add parameters. I think the current
269 // implementation misbehaves.
270 private static class Scaler {
271 private final float sMult;
272 private final float sAdd;
273
274 Scaler(float sMult, float sAdd) {
275 this.sMult = sMult - 1;
276 this.sAdd = sAdd;
277 }
278
279 public int scale(float height) {
Doug Felt10657582010-02-22 11:19:01 -0800280 int altVal = (int)(height * sMult + sAdd + 0.5);
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800281 int rndVal = Math.round(height * sMult + sAdd);
282 if (altVal != rndVal) {
283 Log.i("Scale", "expected scale: " + rndVal +
284 " != returned scale: " + altVal);
285 }
Doug Felt10657582010-02-22 11:19:01 -0800286 return rndVal;
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800287 }
288 }
289
Doug Felt9f7a4442010-03-01 12:45:56 -0800290 /* package */ static LayoutBuilder builder() {
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800291 return new LayoutBuilder();
292 }
293
Doug Felt9f7a4442010-03-01 12:45:56 -0800294 /* package */ static class LayoutBuilder {
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800295 String text = "This is a test";
296 TextPaint paint = new TextPaint(); // default
297 int width = 100;
298 Alignment align = ALIGN_NORMAL;
299 float spacingMult = 1;
300 float spacingAdd = 0;
301 boolean includePad = false;
302
303 LayoutBuilder setText(String text) {
304 this.text = text;
305 return this;
306 }
307
308 LayoutBuilder setPaint(TextPaint paint) {
309 this.paint = paint;
310 return this;
311 }
312
313 LayoutBuilder setWidth(int width) {
314 this.width = width;
315 return this;
316 }
317
318 LayoutBuilder setAlignment(Alignment align) {
319 this.align = align;
320 return this;
321 }
322
323 LayoutBuilder setSpacingMult(float spacingMult) {
324 this.spacingMult = spacingMult;
325 return this;
326 }
327
328 LayoutBuilder setSpacingAdd(float spacingAdd) {
329 this.spacingAdd = spacingAdd;
330 return this;
331 }
332
333 LayoutBuilder setIncludePad(boolean includePad) {
334 this.includePad = includePad;
335 return this;
336 }
337
338 Layout build() {
339 return new StaticLayout(text, paint, width, align, spacingMult,
340 spacingAdd, includePad);
341 }
342 }
343
344 private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) {
345 assertTopBotPadding(l, topPad, botPad);
346 assertLinesMetrics(l, values);
347 }
348
349 private void assertLinesMetrics(Layout l, int... values) {
350 // sanity check
351 if ((values.length & 0x1) != 0) {
352 throw new IllegalArgumentException(String.valueOf(values.length));
353 }
354
355 int lines = values.length >> 1;
356 assertEquals(lines, l.getLineCount());
357
358 int t = 0;
359 for (int i = 0, n = 0; i < lines; ++i, n += 2) {
360 int a = values[n];
361 int d = values[n+1];
362 int h = -a + d;
363 assertLineMetrics(l, i, t, a, d, h);
364 t += h;
365 }
366
367 assertEquals(t, l.getHeight());
368 }
369
370 private void assertLineMetrics(Layout l, int line,
371 int top, int ascent, int descent, int height) {
372 String info = "line " + line;
373 assertEquals(info, top, l.getLineTop(line));
374 assertEquals(info, ascent, l.getLineAscent(line));
375 assertEquals(info, descent, l.getLineDescent(line));
376 assertEquals(info, height, l.getLineBottom(line) - top);
377 }
378
379 private void assertTopBotPadding(Layout l, int topPad, int botPad) {
380 assertEquals(topPad, l.getTopPadding());
381 assertEquals(botPad, l.getBottomPadding());
382 }
Seigo Nonakaf4afb092016-03-15 15:50:06 +0900383
384 private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) {
385 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
386 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
387 final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
388 state.mSelectionStart = state.mSelectionEnd = newOffset;
389 }
390
391 private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) {
392 assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
393 final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
394 final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
395 state.mSelectionStart = state.mSelectionEnd = newOffset;
396 }
397
398 /**
399 * Tests for keycap, variation selectors, flags are in CTS.
400 * See {@link android.text.cts.StaticLayoutTest}.
401 */
Andrei Stingaceanue1a7d0d2017-04-24 16:53:17 +0100402 @Test
Seigo Nonakaf4afb092016-03-15 15:50:06 +0900403 public void testEmojiOffset() {
404 EditorState state = new EditorState();
405 TextPaint paint = new TextPaint();
406
407 // Odd numbered regional indicator symbols.
408 // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL
409 // LETTER C.
410 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
411 moveCursorToRightCursorableOffset(state, paint);
412 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
413 moveCursorToRightCursorableOffset(state, paint);
414 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
415 moveCursorToRightCursorableOffset(state, paint);
416 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
417 moveCursorToRightCursorableOffset(state, paint);
418 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
419 moveCursorToLeftCursorableOffset(state, paint);
420 state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
421 moveCursorToLeftCursorableOffset(state, paint);
422 state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
423 moveCursorToLeftCursorableOffset(state, paint);
424 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
425 moveCursorToLeftCursorableOffset(state, paint);
426 state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
427 moveCursorToLeftCursorableOffset(state, paint);
428
429 // Zero width sequence
430 final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468";
431 state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
432 moveCursorToRightCursorableOffset(state, paint);
433 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
434 moveCursorToRightCursorableOffset(state, paint);
435 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
436 moveCursorToRightCursorableOffset(state, paint);
437 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
438 moveCursorToRightCursorableOffset(state, paint);
439 state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
440 moveCursorToLeftCursorableOffset(state, paint);
441 state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
442 moveCursorToLeftCursorableOffset(state, paint);
443 state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
444 moveCursorToLeftCursorableOffset(state, paint);
445 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
446 moveCursorToLeftCursorableOffset(state, paint);
447 state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
448 moveCursorToLeftCursorableOffset(state, paint);
449
450 // Emoji modifiers
451 // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
452 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
453 moveCursorToRightCursorableOffset(state, paint);
454 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
455 moveCursorToRightCursorableOffset(state, paint);
456 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
457 moveCursorToRightCursorableOffset(state, paint);
458 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
459 moveCursorToRightCursorableOffset(state, paint);
460 state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
461 moveCursorToLeftCursorableOffset(state, paint);
462 state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
463 moveCursorToLeftCursorableOffset(state, paint);
464 state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
465 moveCursorToLeftCursorableOffset(state, paint);
466 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
467 moveCursorToLeftCursorableOffset(state, paint);
468 state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
469 moveCursorToLeftCursorableOffset(state, paint);
470 }
Roozbeh Pournader02f167c2017-06-06 11:31:33 -0700471
472 private StaticLayout createEllipsizeStaticLayout(CharSequence text,
473 TextUtils.TruncateAt ellipsize, int maxLines) {
474 return new StaticLayout(text, 0, text.length(),
475 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
476 TextDirectionHeuristics.FIRSTSTRONG_LTR,
477 SPACE_MULTI, SPACE_ADD, true /* include pad */,
478 ellipsize,
479 ELLIPSIZE_WIDTH,
480 maxLines);
481 }
482
483 @Test
484 public void testEllipsis_singleLine() {
485 {
486 // Single line case and TruncateAt.END so that we have some ellipsis
487 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
488 TextUtils.TruncateAt.END, 1);
489 assertTrue(layout.getEllipsisCount(0) > 0);
490 }
491 {
492 // Single line case and TruncateAt.MIDDLE so that we have some ellipsis
493 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
494 TextUtils.TruncateAt.MIDDLE, 1);
495 assertTrue(layout.getEllipsisCount(0) > 0);
496 }
497 {
498 // Single line case and TruncateAt.END so that we have some ellipsis
499 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
500 TextUtils.TruncateAt.END, 1);
501 assertTrue(layout.getEllipsisCount(0) > 0);
502 }
503 {
504 // Single line case and TruncateAt.MARQUEE so that we have NO ellipsis
505 StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
506 TextUtils.TruncateAt.MARQUEE, 1);
507 assertTrue(layout.getEllipsisCount(0) == 0);
508 }
509 {
510 final String text = "\u3042" // HIRAGANA LETTER A
511 + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
512 final float textWidth = mDefaultPaint.measureText(text);
513 final int halfWidth = (int) (textWidth / 2.0f);
514 {
515 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
516 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
517 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1);
518 assertTrue(layout.getEllipsisCount(0) > 0);
519 assertTrue(layout.getEllipsisStart(0) > 0);
520 }
521 {
522 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
523 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
524 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1);
525 assertTrue(layout.getEllipsisCount(0) > 0);
526 assertEquals(0, mDefaultLayout.getEllipsisStart(0));
527 }
528 {
529 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
530 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
531 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1);
532 assertTrue(layout.getEllipsisCount(0) > 0);
533 assertTrue(layout.getEllipsisStart(0) > 0);
534 }
535 {
536 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
537 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
538 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1);
539 assertEquals(0, layout.getEllipsisCount(0));
540 }
541 }
542
543 {
544 // The white spaces in this text will be trailing if maxLines is larger than 1, but
545 // width of the trailing white spaces must not be ignored if ellipsis is applied.
546 final String text = "abc def";
547 final float textWidth = mDefaultPaint.measureText(text);
548 final int halfWidth = (int) (textWidth / 2.0f);
549 {
550 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
551 halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
552 SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1);
553 assertTrue(layout.getEllipsisCount(0) > 0);
554 assertTrue(layout.getEllipsisStart(0) > 0);
555 }
556 }
557
558 {
559 // 2 family emojis (11 code units + 11 code units).
560 final String text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
561 + "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66";
562 final float textWidth = mDefaultPaint.measureText(text);
563
564 final TextUtils.TruncateAt[] kinds = {TextUtils.TruncateAt.START,
565 TextUtils.TruncateAt.MIDDLE, TextUtils.TruncateAt.END};
566 for (final TextUtils.TruncateAt kind : kinds) {
567 for (int i = 0; i <= 8; i++) {
568 int avail = (int) (textWidth * i / 7.0f);
569 StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
570 avail, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
571 SPACE_MULTI, SPACE_ADD, false, kind, avail, 1);
572
573 assertTrue(layout.getEllipsisCount(0) == text.length()
574 || layout.getEllipsisCount(0) == text.length() / 2
575 || layout.getEllipsisCount(0) == 0);
576 }
577 }
578 }
579 }
580
581 // String wrapper for testing not well known implementation of CharSequence.
582 private class FakeCharSequence implements CharSequence {
583 private String mStr;
584
585 FakeCharSequence(String str) {
586 mStr = str;
587 }
588
589 @Override
590 public char charAt(int index) {
591 return mStr.charAt(index);
592 }
593
594 @Override
595 public int length() {
596 return mStr.length();
597 }
598
599 @Override
600 public CharSequence subSequence(int start, int end) {
601 return mStr.subSequence(start, end);
602 }
603
604 @Override
605 public String toString() {
606 return mStr;
607 }
608 };
609
610 private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) {
611 List<CharSequence> result = new ArrayList<>();
612
613 List<String> normalizedStrings = new ArrayList<>();
614 for (Normalizer.Form form: forms) {
615 normalizedStrings.add(Normalizer.normalize(testString, form));
616 }
617
618 for (String str: normalizedStrings) {
619 result.add(str);
620 result.add(new SpannedString(str));
621 result.add(new SpannableString(str));
622 result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation.
623 result.add(new FakeCharSequence(str)); // as a not well known implementation.
624 }
625 return result;
626 }
627
628 private String buildTestMessage(CharSequence seq) {
629 String normalized;
630 if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) {
631 normalized = "NFC";
632 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) {
633 normalized = "NFD";
634 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) {
635 normalized = "NFKC";
636 } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) {
637 normalized = "NFKD";
638 } else {
639 throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD");
640 }
641
642 StringBuilder builder = new StringBuilder();
643 for (int i = 0; i < seq.length(); ++i) {
644 builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i))));
645 }
646
647 return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]"
648 + ", class: " + seq.getClass().getName()
649 + ", Normalization: " + normalized;
650 }
651
652 @Test
653 public void testGetOffset_UNICODE_Hebrew() {
654 String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters
655 for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
656 StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
657 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
658 TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true);
659
660 String testLabel = buildTestMessage(seq);
661
662 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
663 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
664 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2));
665 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
666 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4));
667 assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5));
668
669 assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
670 assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
671 assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
672 assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
673 assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
674 assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
675 }
676 }
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700677
678 @Test
679 public void testLocaleSpanAffectsHyphenation() {
680 TextPaint paint = new TextPaint();
681 paint.setTextLocale(Locale.US);
682 // Private use language, with no hyphenation rules.
683 final Locale privateLocale = Locale.forLanguageTag("qaa");
684
685 final String longWord = "philanthropic";
686 final float wordWidth = paint.measureText(longWord);
687 // Wide enough that words get hyphenated by default.
688 final int paraWidth = Math.round(wordWidth * 1.8f);
689 final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " "
690 + longWord + " " + longWord;
691
692 final int numEnglishLines = StaticLayout.Builder
693 .obtain(sentence, 0, sentence.length(), paint, paraWidth)
694 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
695 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
696 .build()
697 .getLineCount();
698
699 {
700 final SpannableString text = new SpannableString(sentence);
701 text.setSpan(new LocaleSpan(privateLocale), 0, text.length(),
702 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
703 final int numPrivateLocaleLines = StaticLayout.Builder
704 .obtain(text, 0, text.length(), paint, paraWidth)
705 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
706 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
707 .build()
708 .getLineCount();
709
710 // Since the paragraph set to English gets hyphenated, the number of lines would be
711 // smaller than the number of lines when there is a span setting a language that
712 // doesn't get hyphenated.
713 assertTrue(numEnglishLines < numPrivateLocaleLines);
714 }
715 {
716 // Same as the above test, except that the locale span now uses a locale list starting
717 // with the private non-hyphenating locale.
718 final SpannableString text = new SpannableString(sentence);
719 final LocaleList locales = new LocaleList(privateLocale, Locale.US);
720 text.setSpan(new LocaleSpan(locales), 0, text.length(),
721 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
722 final int numPrivateLocaleLines = StaticLayout.Builder
723 .obtain(text, 0, text.length(), paint, paraWidth)
724 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
725 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
726 .build()
727 .getLineCount();
728
729 assertTrue(numEnglishLines < numPrivateLocaleLines);
730 }
731 {
732 final SpannableString text = new SpannableString(sentence);
733 // Apply the private LocaleSpan only to the first word, which is not getting hyphenated
734 // anyway.
735 text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(),
736 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
737 final int numPrivateLocaleLines = StaticLayout.Builder
738 .obtain(text, 0, text.length(), paint, paraWidth)
739 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
740 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
741 .build()
742 .getLineCount();
743
744 // Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan
745 // should not affect the layout.
746 assertEquals(numEnglishLines, numPrivateLocaleLines);
747 }
748 }
Doug Felt6ad5a7a2010-02-19 15:44:35 -0800749}