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 import static org.junit.Assert.assertEquals;
21 
22 import android.graphics.Paint.FontMetricsInt;
23 import android.support.test.filters.SmallTest;
24 import android.support.test.runner.AndroidJUnit4;
25 import android.text.Layout.Alignment;
26 import android.text.method.EditorState;
27 import android.util.Log;
28 
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 
32 /**
33  * Tests StaticLayout vertical metrics behavior.
34  */
35 @SmallTest
36 @RunWith(AndroidJUnit4.class)
37 public class StaticLayoutTest {
38     /**
39      * Basic test showing expected behavior and relationship between font
40      * metrics and line metrics.
41      */
42     @Test
testGetters1()43     public void testGetters1() {
44         LayoutBuilder b = builder();
45         FontMetricsInt fmi = b.paint.getFontMetricsInt();
46 
47         // check default paint
48         Log.i("TG1:paint", fmi.toString());
49 
50         Layout l = b.build();
51         assertVertMetrics(l, 0, 0,
52                 fmi.ascent, fmi.descent);
53 
54         // other quick metrics
55         assertEquals(0, l.getLineStart(0));
56         assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0));
57         assertEquals(false, l.getLineContainsTab(0));
58         assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0));
59         assertEquals(0, l.getEllipsisCount(0));
60         assertEquals(0, l.getEllipsisStart(0));
61         assertEquals(b.width, l.getEllipsizedWidth());
62     }
63 
64     /**
65      * Basic test showing effect of includePad = true with 1 line.
66      * Top and bottom padding are affected, as is the line descent and height.
67      */
68     @Test
testGetters2()69     public void testGetters2() {
70         LayoutBuilder b = builder()
71             .setIncludePad(true);
72         FontMetricsInt fmi = b.paint.getFontMetricsInt();
73 
74         Layout l = b.build();
75         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
76                 fmi.top, fmi.bottom);
77     }
78 
79     /**
80      * Basic test showing effect of includePad = true wrapping to 2 lines.
81      * Ascent of top line and descent of bottom line are affected.
82      */
83     @Test
testGetters3()84     public void testGetters3() {
85         LayoutBuilder b = builder()
86             .setIncludePad(true)
87             .setWidth(50);
88         FontMetricsInt fmi = b.paint.getFontMetricsInt();
89 
90         Layout l =  b.build();
91         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
92             fmi.top, fmi.descent,
93             fmi.ascent, fmi.bottom);
94     }
95 
96     /**
97      * Basic test showing effect of includePad = true wrapping to 3 lines.
98      * First line ascent is top, bottom line descent is bottom.
99      */
100     @Test
testGetters4()101     public void testGetters4() {
102         LayoutBuilder b = builder()
103             .setText("This is a longer test")
104             .setIncludePad(true)
105             .setWidth(50);
106         FontMetricsInt fmi = b.paint.getFontMetricsInt();
107 
108         Layout l = b.build();
109         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
110                 fmi.top, fmi.descent,
111                 fmi.ascent, fmi.descent,
112                 fmi.ascent, fmi.bottom);
113     }
114 
115     /**
116      * Basic test showing effect of includePad = true wrapping to 3 lines and
117      * large text. See effect of leading. Currently, we don't expect there to
118      * even be non-zero leading.
119      */
120     @Test
testGetters5()121     public void testGetters5() {
122         LayoutBuilder b = builder()
123             .setText("This is a longer test")
124             .setIncludePad(true)
125             .setWidth(150);
126         b.paint.setTextSize(36);
127         FontMetricsInt fmi = b.paint.getFontMetricsInt();
128 
129         if (fmi.leading == 0) { // nothing to test
130             Log.i("TG5", "leading is 0, skipping test");
131             return;
132         }
133 
134         // So far, leading is not used, so this is the same as TG4.  If we start
135         // using leading, this will fail.
136         Layout l = b.build();
137         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
138                 fmi.top, fmi.descent,
139                 fmi.ascent, fmi.descent,
140                 fmi.ascent, fmi.bottom);
141     }
142 
143     /**
144      * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping
145      * to 3 lines.
146      */
147     @Test
testGetters6()148     public void testGetters6() {
149         int spacingAdd = 2; // int so expressions return int
150         LayoutBuilder b = builder()
151             .setText("This is a longer test")
152             .setIncludePad(true)
153             .setWidth(50)
154             .setSpacingAdd(spacingAdd);
155         FontMetricsInt fmi = b.paint.getFontMetricsInt();
156 
157         Layout l = b.build();
158         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
159                 fmi.top, fmi.descent + spacingAdd,
160                 fmi.ascent, fmi.descent + spacingAdd,
161                 fmi.ascent, fmi.bottom);
162     }
163 
164     /**
165      * Basic test showing effect of includePad = true, spacingAdd = 2,
166      * spacingMult = 1.5, wrapping to 3 lines.
167      */
168     @Test
testGetters7()169     public void testGetters7() {
170         LayoutBuilder b = builder()
171             .setText("This is a longer test")
172             .setIncludePad(true)
173             .setWidth(50)
174             .setSpacingAdd(2)
175             .setSpacingMult(1.5f);
176         FontMetricsInt fmi = b.paint.getFontMetricsInt();
177         Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
178 
179         Layout l = b.build();
180         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
181                 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
182                 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
183                 fmi.ascent, fmi.bottom);
184     }
185 
186     /**
187      * Basic test showing effect of includePad = true, spacingAdd = 0,
188      * spacingMult = 0.8 when wrapping to 3 lines.
189      */
190     @Test
testGetters8()191     public void testGetters8() {
192         LayoutBuilder b = builder()
193             .setText("This is a longer test")
194             .setIncludePad(true)
195             .setWidth(50)
196             .setSpacingAdd(2)
197             .setSpacingMult(.8f);
198         FontMetricsInt fmi = b.paint.getFontMetricsInt();
199         Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
200 
201         Layout l = b.build();
202         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
203                 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
204                 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
205                 fmi.ascent, fmi.bottom);
206     }
207 
208     // ----- test utility classes and methods -----
209 
210     // Models the effect of the scale and add parameters.  I think the current
211     // implementation misbehaves.
212     private static class Scaler {
213         private final float sMult;
214         private final float sAdd;
215 
Scaler(float sMult, float sAdd)216         Scaler(float sMult, float sAdd) {
217             this.sMult = sMult - 1;
218             this.sAdd = sAdd;
219         }
220 
scale(float height)221         public int scale(float height) {
222             int altVal = (int)(height * sMult + sAdd + 0.5);
223             int rndVal = Math.round(height * sMult + sAdd);
224             if (altVal != rndVal) {
225                 Log.i("Scale", "expected scale: " + rndVal +
226                         " != returned scale: " + altVal);
227             }
228             return rndVal;
229         }
230     }
231 
builder()232     /* package */ static LayoutBuilder builder() {
233         return new LayoutBuilder();
234     }
235 
236     /* package */ static class LayoutBuilder {
237         String text = "This is a test";
238         TextPaint paint = new TextPaint(); // default
239         int width = 100;
240         Alignment align = ALIGN_NORMAL;
241         float spacingMult = 1;
242         float spacingAdd = 0;
243         boolean includePad = false;
244 
setText(String text)245         LayoutBuilder setText(String text) {
246             this.text = text;
247             return this;
248         }
249 
setPaint(TextPaint paint)250         LayoutBuilder setPaint(TextPaint paint) {
251             this.paint = paint;
252             return this;
253         }
254 
setWidth(int width)255         LayoutBuilder setWidth(int width) {
256             this.width = width;
257             return this;
258         }
259 
setAlignment(Alignment align)260         LayoutBuilder setAlignment(Alignment align) {
261             this.align = align;
262             return this;
263         }
264 
setSpacingMult(float spacingMult)265         LayoutBuilder setSpacingMult(float spacingMult) {
266             this.spacingMult = spacingMult;
267             return this;
268         }
269 
setSpacingAdd(float spacingAdd)270         LayoutBuilder setSpacingAdd(float spacingAdd) {
271             this.spacingAdd = spacingAdd;
272             return this;
273         }
274 
setIncludePad(boolean includePad)275         LayoutBuilder setIncludePad(boolean includePad) {
276             this.includePad = includePad;
277             return this;
278         }
279 
build()280        Layout build() {
281             return  new StaticLayout(text, paint, width, align, spacingMult,
282                 spacingAdd, includePad);
283         }
284     }
285 
assertVertMetrics(Layout l, int topPad, int botPad, int... values)286     private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) {
287         assertTopBotPadding(l, topPad, botPad);
288         assertLinesMetrics(l, values);
289     }
290 
assertLinesMetrics(Layout l, int... values)291     private void assertLinesMetrics(Layout l, int... values) {
292         // sanity check
293         if ((values.length & 0x1) != 0) {
294             throw new IllegalArgumentException(String.valueOf(values.length));
295         }
296 
297         int lines = values.length >> 1;
298         assertEquals(lines, l.getLineCount());
299 
300         int t = 0;
301         for (int i = 0, n = 0; i < lines; ++i, n += 2) {
302             int a = values[n];
303             int d = values[n+1];
304             int h = -a + d;
305             assertLineMetrics(l, i, t, a, d, h);
306             t += h;
307         }
308 
309         assertEquals(t, l.getHeight());
310     }
311 
assertLineMetrics(Layout l, int line, int top, int ascent, int descent, int height)312     private void assertLineMetrics(Layout l, int line,
313             int top, int ascent, int descent, int height) {
314         String info = "line " + line;
315         assertEquals(info, top, l.getLineTop(line));
316         assertEquals(info, ascent, l.getLineAscent(line));
317         assertEquals(info, descent, l.getLineDescent(line));
318         assertEquals(info, height, l.getLineBottom(line) - top);
319     }
320 
assertTopBotPadding(Layout l, int topPad, int botPad)321     private void assertTopBotPadding(Layout l, int topPad, int botPad) {
322         assertEquals(topPad, l.getTopPadding());
323         assertEquals(botPad, l.getBottomPadding());
324     }
325 
moveCursorToRightCursorableOffset(EditorState state, TextPaint paint)326     private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) {
327         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
328         final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
329         final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
330         state.mSelectionStart = state.mSelectionEnd = newOffset;
331     }
332 
moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint)333     private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) {
334         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
335         final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
336         final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
337         state.mSelectionStart = state.mSelectionEnd = newOffset;
338     }
339 
340     /**
341      * Tests for keycap, variation selectors, flags are in CTS.
342      * See {@link android.text.cts.StaticLayoutTest}.
343      */
344     @Test
testEmojiOffset()345     public void testEmojiOffset() {
346         EditorState state = new EditorState();
347         TextPaint paint = new TextPaint();
348 
349         // Odd numbered regional indicator symbols.
350         // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL
351         // LETTER C.
352         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
353         moveCursorToRightCursorableOffset(state, paint);
354         state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
355         moveCursorToRightCursorableOffset(state, paint);
356         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
357         moveCursorToRightCursorableOffset(state, paint);
358         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
359         moveCursorToRightCursorableOffset(state, paint);
360         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
361         moveCursorToLeftCursorableOffset(state, paint);
362         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
363         moveCursorToLeftCursorableOffset(state, paint);
364         state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
365         moveCursorToLeftCursorableOffset(state, paint);
366         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
367         moveCursorToLeftCursorableOffset(state, paint);
368         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
369         moveCursorToLeftCursorableOffset(state, paint);
370 
371         // Zero width sequence
372         final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468";
373         state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
374         moveCursorToRightCursorableOffset(state, paint);
375         state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
376         moveCursorToRightCursorableOffset(state, paint);
377         state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
378         moveCursorToRightCursorableOffset(state, paint);
379         state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
380         moveCursorToRightCursorableOffset(state, paint);
381         state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
382         moveCursorToLeftCursorableOffset(state, paint);
383         state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
384         moveCursorToLeftCursorableOffset(state, paint);
385         state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
386         moveCursorToLeftCursorableOffset(state, paint);
387         state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
388         moveCursorToLeftCursorableOffset(state, paint);
389         state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
390         moveCursorToLeftCursorableOffset(state, paint);
391 
392         // Emoji modifiers
393         // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
394         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
395         moveCursorToRightCursorableOffset(state, paint);
396         state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
397         moveCursorToRightCursorableOffset(state, paint);
398         state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
399         moveCursorToRightCursorableOffset(state, paint);
400         state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
401         moveCursorToRightCursorableOffset(state, paint);
402         state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
403         moveCursorToLeftCursorableOffset(state, paint);
404         state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
405         moveCursorToLeftCursorableOffset(state, paint);
406         state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
407         moveCursorToLeftCursorableOffset(state, paint);
408         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
409         moveCursorToLeftCursorableOffset(state, paint);
410         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
411         moveCursorToLeftCursorableOffset(state, paint);
412     }
413 }
414