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