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