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