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