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