1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.cts;
18 
19 import android.graphics.Typeface;
20 import android.test.AndroidTestCase;
21 import android.text.Editable;
22 import android.text.Layout;
23 import android.text.Layout.Alignment;
24 import android.text.SpannableString;
25 import android.text.SpannableStringBuilder;
26 import android.text.Spanned;
27 import android.text.SpannedString;
28 import android.text.StaticLayout;
29 import android.text.TextDirectionHeuristics;
30 import android.text.TextPaint;
31 import android.text.TextUtils;
32 import android.text.TextUtils.TruncateAt;
33 import android.text.method.cts.EditorState;
34 import android.text.style.StyleSpan;
35 
36 import java.text.Normalizer;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Locale;
40 
41 public class StaticLayoutTest extends AndroidTestCase {
42     private static final float SPACE_MULTI = 1.0f;
43     private static final float SPACE_ADD = 0.0f;
44     private static final int DEFAULT_OUTER_WIDTH = 150;
45 
46     private static final int LAST_LINE = 5;
47     private static final int LINE_COUNT = 6;
48     private static final int LARGER_THAN_LINE_COUNT  = 50;
49 
50     /* the first line must have one tab. the others not. totally 6 lines
51      */
52     private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar"
53             + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong";
54 
55     private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence";
56 
57     private static final int VERTICAL_BELOW_TEXT = 1000;
58 
59     private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
60 
61     private static final int ELLIPSIZE_WIDTH = 8;
62 
63     private StaticLayout mDefaultLayout;
64     private TextPaint mDefaultPaint;
65 
66     private class TestingTextPaint extends TextPaint {
67         // need to have a subclass to insure measurement happens in Java and not C++
68     }
69 
70     @Override
setUp()71     protected void setUp() throws Exception {
72         super.setUp();
73         if (mDefaultPaint == null) {
74             mDefaultPaint = new TextPaint();
75         }
76         if (mDefaultLayout == null) {
77             mDefaultLayout = createDefaultStaticLayout();
78         }
79     }
80 
createDefaultStaticLayout()81     private StaticLayout createDefaultStaticLayout() {
82         return new StaticLayout(LAYOUT_TEXT, mDefaultPaint,
83                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
84     }
85 
createEllipsizeStaticLayout()86     private StaticLayout createEllipsizeStaticLayout() {
87         return new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
88                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true,
89                 TextUtils.TruncateAt.MIDDLE, ELLIPSIZE_WIDTH);
90     }
91 
createEllipsizeStaticLayout(CharSequence text, TextUtils.TruncateAt ellipsize, int maxLines)92     private StaticLayout createEllipsizeStaticLayout(CharSequence text,
93             TextUtils.TruncateAt ellipsize, int maxLines) {
94         return new StaticLayout(text, 0, text.length(),
95                 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
96                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
97                 SPACE_MULTI, SPACE_ADD, true /* include pad */,
98                 ellipsize,
99                 ELLIPSIZE_WIDTH,
100                 maxLines);
101     }
102 
103 
104 
105     /**
106      * Constructor test
107      */
testConstructor()108     public void testConstructor() {
109         new StaticLayout(LAYOUT_TEXT, mDefaultPaint, DEFAULT_OUTER_WIDTH,
110                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
111 
112         new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
113                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
114 
115         new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
116                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, false, null, 0);
117 
118         try {
119             new StaticLayout(null, null, -1, null, 0, 0, true);
120             fail("should throw NullPointerException here");
121         } catch (NullPointerException e) {
122         }
123     }
124 
testBuilder()125     public void testBuilder() {
126         {
127             // Obtain.
128             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
129                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
130             StaticLayout layout = builder.build();
131             // Check values passed to obtain().
132             assertEquals(LAYOUT_TEXT, layout.getText());
133             assertEquals(mDefaultPaint, layout.getPaint());
134             assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
135             // Check default values.
136             assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
137                     layout.getTextDirectionHeuristic());
138             assertEquals(Alignment.ALIGN_NORMAL, layout.getAlignment());
139             assertEquals(0.0f, layout.getSpacingAdd());
140             assertEquals(1.0f, layout.getSpacingMultiplier());
141             assertEquals(DEFAULT_OUTER_WIDTH, layout.getEllipsizedWidth());
142         }
143         {
144             // Obtain with null objects.
145             StaticLayout.Builder builder = StaticLayout.Builder.obtain(null, 0, 0, null, 0);
146             try {
147                 StaticLayout layout = builder.build();
148                 fail("should throw NullPointerException here");
149             } catch (NullPointerException e) {
150             }
151         }
152         {
153             // setText.
154             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
155                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
156             builder.setText(LAYOUT_TEXT_SINGLE_LINE);
157             StaticLayout layout = builder.build();
158             assertEquals(LAYOUT_TEXT_SINGLE_LINE, layout.getText());
159         }
160         {
161             // setAlignment.
162             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
163                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
164             builder.setAlignment(DEFAULT_ALIGN);
165             StaticLayout layout = builder.build();
166             assertEquals(DEFAULT_ALIGN, layout.getAlignment());
167         }
168         {
169             // setTextDirection.
170             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
171                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
172             builder.setTextDirection(TextDirectionHeuristics.RTL);
173             StaticLayout layout = builder.build();
174             // Always returns TextDirectionHeuristics.FIRSTSTRONG_LTR.
175             assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
176                     layout.getTextDirectionHeuristic());
177         }
178         {
179             // setLineSpacing.
180             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
181                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
182             builder.setLineSpacing(1.0f, 2.0f);
183             StaticLayout layout = builder.build();
184             assertEquals(1.0f, layout.getSpacingAdd());
185             assertEquals(2.0f, layout.getSpacingMultiplier());
186         }
187         {
188             // setEllipsizedWidth and setEllipsize.
189             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
190                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
191             builder.setEllipsize(TruncateAt.END);
192             builder.setEllipsizedWidth(ELLIPSIZE_WIDTH);
193             StaticLayout layout = builder.build();
194             assertEquals(ELLIPSIZE_WIDTH, layout.getEllipsizedWidth());
195             assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
196             assertTrue(layout.getEllipsisCount(0) == 0);
197             assertTrue(layout.getEllipsisCount(5) > 0);
198         }
199         {
200             // setMaxLines.
201             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
202                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
203             builder.setMaxLines(1);
204             builder.setEllipsize(TruncateAt.END);
205             StaticLayout layout = builder.build();
206             assertTrue(layout.getEllipsisCount(0) > 0);
207             assertEquals(1, layout.getLineCount());
208         }
209         {
210             // Setter methods that cannot be directly tested.
211             // setBreakStrategy, setHyphenationFrequency, setIncludePad, and setIndents.
212             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
213                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
214             builder.setBreakStrategy(StaticLayout.BREAK_STRATEGY_HIGH_QUALITY);
215             builder.setHyphenationFrequency(StaticLayout.HYPHENATION_FREQUENCY_FULL);
216             builder.setIncludePad(true);
217             builder.setIndents(null, null);
218             StaticLayout layout = builder.build();
219             assertNotNull(layout);
220         }
221     }
222 
223     /*
224      * Get the line number corresponding to the specified vertical position.
225      *  If you ask for a position above 0, you get 0. above 0 means pixel above the fire line
226      *  if you ask for a position in the range of the height, return the pixel in line
227      *  if you ask for a position below the bottom of the text, you get the last line.
228      *  Test 4 values containing -1, 0, normal number and > count
229      */
testGetLineForVertical()230     public void testGetLineForVertical() {
231         assertEquals(0, mDefaultLayout.getLineForVertical(-1));
232         assertEquals(0, mDefaultLayout.getLineForVertical(0));
233         assertTrue(mDefaultLayout.getLineForVertical(50) > 0);
234         assertEquals(LAST_LINE, mDefaultLayout.getLineForVertical(VERTICAL_BELOW_TEXT));
235     }
236 
237     /**
238      * Return the number of lines of text in this layout.
239      */
testGetLineCount()240     public void testGetLineCount() {
241         assertEquals(LINE_COUNT, mDefaultLayout.getLineCount());
242     }
243 
244     /*
245      * Return the vertical position of the top of the specified line.
246      * If the specified line is one beyond the last line, returns the bottom of the last line.
247      * A line of text contains top and bottom in height. this method just get the top of a line
248      * Test 4 values containing -1, 0, normal number and > count
249      */
testGetLineTop()250     public void testGetLineTop() {
251         assertTrue(mDefaultLayout.getLineTop(0) >= 0);
252         assertTrue(mDefaultLayout.getLineTop(1) > mDefaultLayout.getLineTop(0));
253 
254         try {
255             mDefaultLayout.getLineTop(-1);
256             fail("should throw ArrayIndexOutOfBoundsException");
257         } catch (ArrayIndexOutOfBoundsException e) {
258         }
259 
260         try {
261             mDefaultLayout.getLineTop(LARGER_THAN_LINE_COUNT );
262             fail("should throw ArrayIndexOutOfBoundsException");
263         } catch (ArrayIndexOutOfBoundsException e) {
264         }
265     }
266 
267     /**
268      * Return the descent of the specified line.
269      * This method just like getLineTop, descent means the bottom pixel of the line
270      * Test 4 values containing -1, 0, normal number and > count
271      */
testGetLineDescent()272     public void testGetLineDescent() {
273         assertTrue(mDefaultLayout.getLineDescent(0) > 0);
274         assertTrue(mDefaultLayout.getLineDescent(1) > 0);
275 
276         try {
277             mDefaultLayout.getLineDescent(-1);
278             fail("should throw ArrayIndexOutOfBoundsException");
279         } catch (ArrayIndexOutOfBoundsException e) {
280         }
281 
282         try {
283             mDefaultLayout.getLineDescent(LARGER_THAN_LINE_COUNT );
284             fail("should throw ArrayIndexOutOfBoundsException");
285         } catch (ArrayIndexOutOfBoundsException e) {
286         }
287     }
288 
289     /**
290      * Returns the primary directionality of the paragraph containing the specified line.
291      * By default, each line should be same
292      */
testGetParagraphDirection()293     public void testGetParagraphDirection() {
294         assertEquals(mDefaultLayout.getParagraphDirection(0),
295                 mDefaultLayout.getParagraphDirection(1));
296         try {
297             mDefaultLayout.getParagraphDirection(-1);
298             fail("should throw ArrayIndexOutOfBoundsException");
299         } catch (ArrayIndexOutOfBoundsException e) {
300         }
301 
302         try {
303             mDefaultLayout.getParagraphDirection(LARGER_THAN_LINE_COUNT);
304             fail("should throw ArrayIndexOutOfBoundsException");
305         } catch (ArrayIndexOutOfBoundsException e) {
306         }
307     }
308 
309     /**
310      * Return the text offset of the beginning of the specified line.
311      * If the specified line is one beyond the last line, returns the end of the last line.
312      * Test 4 values containing -1, 0, normal number and > count
313      * Each line's offset must >= 0
314      */
testGetLineStart()315     public void testGetLineStart() {
316         assertTrue(mDefaultLayout.getLineStart(0) >= 0);
317         assertTrue(mDefaultLayout.getLineStart(1) >= 0);
318 
319         try {
320             mDefaultLayout.getLineStart(-1);
321             fail("should throw ArrayIndexOutOfBoundsException");
322         } catch (ArrayIndexOutOfBoundsException e) {
323         }
324 
325         try {
326             mDefaultLayout.getLineStart(LARGER_THAN_LINE_COUNT);
327             fail("should throw ArrayIndexOutOfBoundsException");
328         } catch (ArrayIndexOutOfBoundsException e) {
329         }
330     }
331 
332     /*
333      * Returns whether the specified line contains one or more tabs.
334      */
testGetContainsTab()335     public void testGetContainsTab() {
336         assertTrue(mDefaultLayout.getLineContainsTab(0));
337         assertFalse(mDefaultLayout.getLineContainsTab(1));
338 
339         try {
340             mDefaultLayout.getLineContainsTab(-1);
341             fail("should throw ArrayIndexOutOfBoundsException");
342         } catch (ArrayIndexOutOfBoundsException e) {
343         }
344 
345         try {
346             mDefaultLayout.getLineContainsTab(LARGER_THAN_LINE_COUNT );
347             fail("should throw ArrayIndexOutOfBoundsException");
348         } catch (ArrayIndexOutOfBoundsException e) {
349         }
350     }
351 
352     /**
353      * Returns an array of directionalities for the specified line.
354      * The array alternates counts of characters in left-to-right
355      * and right-to-left segments of the line.
356      * We can not check the return value, for Directions's field is package private
357      * So only check it not null
358      */
testGetLineDirections()359     public void testGetLineDirections() {
360         assertNotNull(mDefaultLayout.getLineDirections(0));
361         assertNotNull(mDefaultLayout.getLineDirections(1));
362 
363         try {
364             mDefaultLayout.getLineDirections(-1);
365             fail("should throw ArrayIndexOutOfBoundsException");
366         } catch (ArrayIndexOutOfBoundsException e) {
367         }
368 
369         try {
370             mDefaultLayout.getLineDirections(LARGER_THAN_LINE_COUNT);
371             fail("should throw ArrayIndexOutOfBoundsException");
372         } catch (ArrayIndexOutOfBoundsException e) {
373         }
374     }
375 
376     /**
377      * Returns the (negative) number of extra pixels of ascent padding
378      * in the top line of the Layout.
379      */
testGetTopPadding()380     public void testGetTopPadding() {
381         assertTrue(mDefaultLayout.getTopPadding() < 0);
382     }
383 
384     /**
385      * Returns the number of extra pixels of descent padding in the bottom line of the Layout.
386      */
387     public void testGetBottomPadding() {
388         assertTrue(mDefaultLayout.getBottomPadding() > 0);
389     }
390 
391     /*
392      * Returns the number of characters to be ellipsized away, or 0 if no ellipsis is to take place.
393      * So each line must >= 0
394      */
testGetEllipsisCount()395     public void testGetEllipsisCount() {
396         // Multilines (6 lines) and TruncateAt.START so no ellipsis at all
397         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
398                 TextUtils.TruncateAt.MIDDLE,
399                 Integer.MAX_VALUE /* maxLines */);
400 
401         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
402         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
403         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
404         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
405         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
406         assertTrue(mDefaultLayout.getEllipsisCount(5) == 0);
407 
408         try {
409             mDefaultLayout.getEllipsisCount(-1);
410             fail("should throw ArrayIndexOutOfBoundsException");
411         } catch (ArrayIndexOutOfBoundsException e) {
412         }
413 
414         try {
415             mDefaultLayout.getEllipsisCount(LARGER_THAN_LINE_COUNT);
416             fail("should throw ArrayIndexOutOfBoundsException");
417         } catch (ArrayIndexOutOfBoundsException e) {
418         }
419 
420         // Multilines (6 lines) and TruncateAt.MIDDLE so no ellipsis at all
421         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
422                 TextUtils.TruncateAt.MIDDLE,
423                 Integer.MAX_VALUE /* maxLines */);
424 
425         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
426         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
427         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
428         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
429         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
430         assertTrue(mDefaultLayout.getEllipsisCount(5) == 0);
431 
432         // Multilines (6 lines) and TruncateAt.END so ellipsis only on the last line
433         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
434                 TextUtils.TruncateAt.END,
435                 Integer.MAX_VALUE /* maxLines */);
436 
437         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
438         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
439         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
440         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
441         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
442         assertTrue(mDefaultLayout.getEllipsisCount(5) > 0);
443 
444         // Multilines (6 lines) and TruncateAt.MARQUEE so ellipsis only on the last line
445         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
446                 TextUtils.TruncateAt.END,
447                 Integer.MAX_VALUE /* maxLines */);
448 
449         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
450         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
451         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
452         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
453         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
454         assertTrue(mDefaultLayout.getEllipsisCount(5) > 0);
455     }
456 
457     /*
458      * Return the offset of the first character to be ellipsized away
459      * relative to the start of the line.
460      * (So 0 if the beginning of the line is ellipsized, not getLineStart().)
461      */
testGetEllipsisStart()462     public void testGetEllipsisStart() {
463         mDefaultLayout = createEllipsizeStaticLayout();
464         assertTrue(mDefaultLayout.getEllipsisStart(0) >= 0);
465         assertTrue(mDefaultLayout.getEllipsisStart(1) >= 0);
466 
467         try {
468             mDefaultLayout.getEllipsisStart(-1);
469             fail("should throw ArrayIndexOutOfBoundsException");
470         } catch (ArrayIndexOutOfBoundsException e) {
471         }
472 
473         try {
474             mDefaultLayout.getEllipsisStart(LARGER_THAN_LINE_COUNT);
475             fail("should throw ArrayIndexOutOfBoundsException");
476         } catch (ArrayIndexOutOfBoundsException e) {
477         }
478     }
479 
480     /*
481      * Return the width to which this Layout is ellipsizing
482      * or getWidth() if it is not doing anything special.
483      * The constructor's Argument TextUtils.TruncateAt defines which EllipsizedWidth to use
484      * ellipsizedWidth if argument is not null
485      * outerWidth if argument is null
486      */
testGetEllipsizedWidth()487     public void testGetEllipsizedWidth() {
488         int ellipsizedWidth = 60;
489         int outerWidth = 100;
490         StaticLayout layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(),
491                 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI,
492                 SPACE_ADD, false, TextUtils.TruncateAt.END, ellipsizedWidth);
493         assertEquals(ellipsizedWidth, layout.getEllipsizedWidth());
494 
495         layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(),
496                 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD,
497                 false, null, ellipsizedWidth);
498         assertEquals(outerWidth, layout.getEllipsizedWidth());
499     }
500 
testEllipsis_singleLine()501     public void testEllipsis_singleLine() {
502         {
503             // Single line case and TruncateAt.END so that we have some ellipsis
504             StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
505                     TextUtils.TruncateAt.END, 1);
506             assertTrue(layout.getEllipsisCount(0) > 0);
507         }
508         {
509             // Single line case and TruncateAt.MIDDLE so that we have some ellipsis
510             StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
511                     TextUtils.TruncateAt.MIDDLE, 1);
512             assertTrue(layout.getEllipsisCount(0) > 0);
513         }
514         {
515             // Single line case and TruncateAt.END so that we have some ellipsis
516             StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
517                     TextUtils.TruncateAt.END, 1);
518             assertTrue(layout.getEllipsisCount(0) > 0);
519         }
520         {
521             // Single line case and TruncateAt.MARQUEE so that we have NO ellipsis
522             StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
523                     TextUtils.TruncateAt.MARQUEE, 1);
524             assertTrue(layout.getEllipsisCount(0) == 0);
525         }
526 
527         final String text = "\u3042" // HIRAGANA LETTER A
528                 + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
529         final float textWidth = mDefaultPaint.measureText(text);
530         final int halfWidth = (int)(textWidth / 2.0f);
531         {
532             StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
533                     halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
534                     SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1);
535             assertTrue(layout.getEllipsisCount(0) > 0);
536             assertTrue(layout.getEllipsisStart(0) > 0);
537         }
538         {
539             StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
540                     halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
541                     SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1);
542             assertTrue(layout.getEllipsisCount(0) > 0);
543             assertEquals(0, mDefaultLayout.getEllipsisStart(0));
544         }
545         {
546             StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
547                     halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
548                     SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1);
549             assertTrue(layout.getEllipsisCount(0) > 0);
550             assertTrue(layout.getEllipsisStart(0) > 0);
551         }
552         {
553             StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
554                     halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
555                     SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1);
556             assertEquals(0, layout.getEllipsisCount(0));
557         }
558     }
559 
560     /**
561      * scenario description:
562      * 1. set the text.
563      * 2. change the text
564      * 3. Check the text won't change to the StaticLayout
565     */
testImmutableStaticLayout()566     public void testImmutableStaticLayout() {
567         Editable editable =  Editable.Factory.getInstance().newEditable("123\t\n555");
568         StaticLayout layout = new StaticLayout(editable, mDefaultPaint,
569                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
570 
571         assertEquals(2, layout.getLineCount());
572         assertTrue(mDefaultLayout.getLineContainsTab(0));
573 
574         // change the text
575         editable.delete(0, editable.length() - 1);
576 
577         assertEquals(2, layout.getLineCount());
578         assertTrue(layout.getLineContainsTab(0));
579 
580     }
581 
582     // String wrapper for testing not well known implementation of CharSequence.
583     private class FakeCharSequence implements CharSequence {
584         private String mStr;
585 
FakeCharSequence(String str)586         public FakeCharSequence(String str) {
587             mStr = str;
588         }
589 
590         @Override
charAt(int index)591         public char charAt(int index) {
592             return mStr.charAt(index);
593         }
594 
595         @Override
length()596         public int length() {
597             return mStr.length();
598         }
599 
600         @Override
subSequence(int start, int end)601         public CharSequence subSequence(int start, int end) {
602             return mStr.subSequence(start, end);
603         }
604 
605         @Override
toString()606         public String toString() {
607             return mStr;
608         }
609     };
610 
buildTestCharSequences(String testString, Normalizer.Form[] forms)611     private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) {
612         List<CharSequence> result = new ArrayList<CharSequence>();
613 
614         List<String> normalizedStrings = new ArrayList<String>();
615         for (Normalizer.Form form: forms) {
616             normalizedStrings.add(Normalizer.normalize(testString, form));
617         }
618 
619         for (String str: normalizedStrings) {
620             result.add(str);
621             result.add(new SpannedString(str));
622             result.add(new SpannableString(str));
623             result.add(new SpannableStringBuilder(str));  // as a GraphicsOperations implementation.
624             result.add(new FakeCharSequence(str));  // as a not well known implementation.
625         }
626         return result;
627     }
628 
buildTestMessage(CharSequence seq)629     private String buildTestMessage(CharSequence seq) {
630         String normalized;
631         if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) {
632             normalized = "NFC";
633         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) {
634             normalized = "NFD";
635         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) {
636             normalized = "NFKC";
637         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) {
638             normalized = "NFKD";
639         } else {
640             throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD");
641         }
642 
643         StringBuilder builder = new StringBuilder();
644         for (int i = 0; i < seq.length(); ++i) {
645             builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i))));
646         }
647 
648         return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" +
649                 ", class: " + seq.getClass().getName() +
650                 ", Normalization: " + normalized;
651     }
652 
testGetOffset_ASCII()653     public void testGetOffset_ASCII() {
654         String testStrings[] = { "abcde", "ab\ncd", "ab\tcd", "ab\n\nc", "ab\n\tc" };
655 
656         for (String testString: testStrings) {
657             for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
658                 StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
659                         DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
660 
661                 String testLabel = buildTestMessage(seq);
662 
663                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
664                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
665                 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
666                 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
667                 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
668                 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
669 
670                 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
671                 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
672                 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
673                 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
674                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
675                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
676             }
677         }
678 
679         String testString = "ab\r\nde";
680         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
681             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
682                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
683 
684             String testLabel = buildTestMessage(seq);
685 
686             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
687             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
688             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
689             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
690             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
691             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
692             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
693 
694             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
695             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
696             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
697             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
698             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
699             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
700             assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
701         }
702     }
703 
testGetOffset_UNICODE()704     public void testGetOffset_UNICODE() {
705         String testStrings[] = new String[] {
706               // Cyrillic alphabets.
707               "\u0410\u0411\u0412\u0413\u0414",
708               // Japanese Hiragana Characters.
709               "\u3042\u3044\u3046\u3048\u304A",
710         };
711 
712         for (String testString: testStrings) {
713             for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
714                 StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
715                         DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
716 
717                 String testLabel = buildTestMessage(seq);
718 
719                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
720                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
721                 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
722                 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
723                 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
724                 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
725 
726                 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
727                 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
728                 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
729                 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
730                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
731                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
732             }
733         }
734     }
735 
testGetOffset_UNICODE_Normalization()736     public void testGetOffset_UNICODE_Normalization() {
737         // "A" with acute, circumflex, tilde, diaeresis, ring above.
738         String testString = "\u00C1\u00C2\u00C3\u00C4\u00C5";
739         Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC };
740         for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) {
741             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
742                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
743 
744             String testLabel = buildTestMessage(seq);
745 
746             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
747             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
748             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
749             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
750             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
751             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
752 
753             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
754             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
755             assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
756             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
757             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
758             assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
759         }
760 
761         Normalizer.Form[] twoUnicodeForms = { Normalizer.Form.NFD, Normalizer.Form.NFKD };
762         for (CharSequence seq: buildTestCharSequences(testString, twoUnicodeForms)) {
763             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
764                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
765 
766             String testLabel = buildTestMessage(seq);
767 
768             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
769             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
770             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
771             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
772             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
773             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
774             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
775             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
776             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
777             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
778             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
779 
780             assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
781             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
782             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
783             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
784             assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
785             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
786             assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
787             assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
788             assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
789             assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
790             assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
791         }
792     }
793 
testGetOffset_UNICODE_SurrogatePairs()794     public void testGetOffset_UNICODE_SurrogatePairs() {
795         // Emoticons for surrogate pairs tests.
796         String testString =
797                 "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
798         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
799             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
800                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
801 
802             String testLabel = buildTestMessage(seq);
803 
804             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
805             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
806             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
807             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
808             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
809             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
810             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
811             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
812             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
813             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
814             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
815 
816             assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
817             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
818             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
819             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
820             assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
821             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
822             assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
823             assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
824             assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
825             assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
826             assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
827         }
828     }
829 
testGetOffset_UNICODE_Thai()830     public void testGetOffset_UNICODE_Thai() {
831         // Thai Characters. The expected cursorable boundary is
832         // | \u0E02 | \u0E2D | \u0E1A | \u0E04\u0E38 | \u0E13 |
833         String testString = "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13";
834         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
835             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
836                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
837 
838             String testLabel = buildTestMessage(seq);
839 
840             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
841             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
842             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
843             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
844             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
845             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
846             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
847 
848             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
849             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
850             assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
851             assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
852             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
853             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
854             assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
855         }
856     }
857 
testGetOffset_UNICODE_Hebrew()858     public void testGetOffset_UNICODE_Hebrew() {
859         String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters
860         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
861             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
862                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
863                     TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true);
864 
865             String testLabel = buildTestMessage(seq);
866 
867             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
868             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
869             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2));
870             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
871             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4));
872             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5));
873 
874             assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
875             assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
876             assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
877             assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
878             assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
879             assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
880         }
881     }
882 
testGetOffset_UNICODE_Arabic()883     public void testGetOffset_UNICODE_Arabic() {
884         // Arabic Characters. The expected cursorable boundary is
885         // | \u0623 \u064F | \u0633 \u0652 | \u0631 \u064E | \u0629 \u064C |";
886         String testString = "\u0623\u064F\u0633\u0652\u0631\u064E\u0629\u064C";
887 
888         Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC };
889         for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) {
890             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
891                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
892 
893             String testLabel = buildTestMessage(seq);
894 
895             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(0));
896             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
897             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
898             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
899             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(4));
900             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
901             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(6));
902             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(7));
903             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(8));
904 
905             assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
906             assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
907             assertEquals(testLabel, 0, layout.getOffsetToRightOf(2));
908             assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
909             assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
910             assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
911             assertEquals(testLabel, 4, layout.getOffsetToRightOf(6));
912             assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
913             assertEquals(testLabel, 6, layout.getOffsetToRightOf(8));
914         }
915     }
916 
testGetOffset_UNICODE_Bidi()917     public void testGetOffset_UNICODE_Bidi() {
918         // String having RTL characters and LTR characters
919 
920         // LTR Context
921         // The first and last two characters are LTR characters.
922         String testString = "\u0061\u0062\u05DE\u05E1\u05E2\u0063\u0064";
923         // Logical order: [L1] [L2] [R1] [R2] [R3] [L3] [L4]
924         //               0    1    2    3    4    5    6    7
925         // Display order: [L1] [L2] [R3] [R2] [R1] [L3] [L4]
926         //               0    1    2    4    3    5    6    7
927         // [L?] means ?th LTR character and [R?] means ?th RTL character.
928         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
929             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
930                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
931 
932             String testLabel = buildTestMessage(seq);
933 
934             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
935             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
936             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
937             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
938             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
939             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
940             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
941             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
942 
943             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
944             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
945             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
946             assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
947             assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
948             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
949             assertEquals(testLabel, 7, layout.getOffsetToRightOf(6));
950             assertEquals(testLabel, 7, layout.getOffsetToRightOf(7));
951         }
952 
953         // RTL Context
954         // The first and last two characters are RTL characters.
955         String testString2 = "\u05DE\u05E1\u0063\u0064\u0065\u05DE\u05E1";
956         // Logical order: [R1] [R2] [L1] [L2] [L3] [R3] [R4]
957         //               0    1    2    3    4    5    6    7
958         // Display order: [R4] [R3] [L1] [L2] [L3] [R2] [R1]
959         //               7    6    5    3    4    2    1    0
960         // [L?] means ?th LTR character and [R?] means ?th RTL character.
961         for (CharSequence seq: buildTestCharSequences(testString2, Normalizer.Form.values())) {
962             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
963                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
964 
965             String testLabel = buildTestMessage(seq);
966 
967             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
968             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
969             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
970             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(3));
971             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
972             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
973             assertEquals(testLabel, 7, layout.getOffsetToLeftOf(6));
974             assertEquals(testLabel, 7, layout.getOffsetToLeftOf(7));
975 
976             assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
977             assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
978             assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
979             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
980             assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
981             assertEquals(testLabel, 3, layout.getOffsetToRightOf(5));
982             assertEquals(testLabel, 5, layout.getOffsetToRightOf(6));
983             assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
984         }
985     }
986 
moveCursorToRightCursorableOffset(EditorState state)987     private void moveCursorToRightCursorableOffset(EditorState state) {
988         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
989         StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(),
990                 mDefaultPaint, DEFAULT_OUTER_WIDTH).build();
991         final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
992         state.mSelectionStart = state.mSelectionEnd = newOffset;
993     }
994 
moveCursorToLeftCursorableOffset(EditorState state)995     private void moveCursorToLeftCursorableOffset(EditorState state) {
996         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
997         StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(),
998                 mDefaultPaint, DEFAULT_OUTER_WIDTH).build();
999         final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
1000         state.mSelectionStart = state.mSelectionEnd = newOffset;
1001     }
1002 
testGetOffset_Emoji()1003     public void testGetOffset_Emoji() {
1004         EditorState state = new EditorState();
1005 
1006         // Emojis
1007         // U+00A9 is COPYRIGHT SIGN.
1008         state.setByString("| U+00A9 U+00A9 U+00A9");
1009         moveCursorToRightCursorableOffset(state);
1010         state.assertEquals("U+00A9 | U+00A9 U+00A9");
1011         moveCursorToRightCursorableOffset(state);
1012         state.assertEquals("U+00A9 U+00A9 | U+00A9");
1013         moveCursorToRightCursorableOffset(state);
1014         state.assertEquals("U+00A9 U+00A9 U+00A9 |");
1015         moveCursorToRightCursorableOffset(state);
1016         state.assertEquals("U+00A9 U+00A9 U+00A9 |");
1017         moveCursorToLeftCursorableOffset(state);
1018         state.assertEquals("U+00A9 U+00A9 | U+00A9");
1019         moveCursorToLeftCursorableOffset(state);
1020         state.assertEquals("U+00A9 | U+00A9 U+00A9");
1021         moveCursorToLeftCursorableOffset(state);
1022         state.assertEquals("| U+00A9 U+00A9 U+00A9");
1023         moveCursorToLeftCursorableOffset(state);
1024         state.assertEquals("| U+00A9 U+00A9 U+00A9");
1025 
1026         // Surrogate pairs
1027         // U+1F468 is MAN.
1028         state.setByString("| U+1F468 U+1F468 U+1F468");
1029         moveCursorToRightCursorableOffset(state);
1030         state.assertEquals("U+1F468 | U+1F468 U+1F468");
1031         moveCursorToRightCursorableOffset(state);
1032         state.assertEquals("U+1F468 U+1F468 | U+1F468");
1033         moveCursorToRightCursorableOffset(state);
1034         state.assertEquals("U+1F468 U+1F468 U+1F468 |");
1035         moveCursorToRightCursorableOffset(state);
1036         state.assertEquals("U+1F468 U+1F468 U+1F468 |");
1037         moveCursorToLeftCursorableOffset(state);
1038         state.assertEquals("U+1F468 U+1F468 | U+1F468");
1039         moveCursorToLeftCursorableOffset(state);
1040         state.assertEquals("U+1F468 | U+1F468 U+1F468");
1041         moveCursorToLeftCursorableOffset(state);
1042         state.assertEquals("| U+1F468 U+1F468 U+1F468");
1043         moveCursorToLeftCursorableOffset(state);
1044         state.assertEquals("| U+1F468 U+1F468 U+1F468");
1045 
1046         // Keycaps
1047         // U+20E3 is COMBINING ENCLOSING KEYCAP.
1048         state.setByString("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
1049         moveCursorToRightCursorableOffset(state);
1050         state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
1051         moveCursorToRightCursorableOffset(state);
1052         state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
1053         moveCursorToRightCursorableOffset(state);
1054         state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
1055         moveCursorToRightCursorableOffset(state);
1056         state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
1057         moveCursorToLeftCursorableOffset(state);
1058         state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
1059         moveCursorToLeftCursorableOffset(state);
1060         state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
1061         moveCursorToLeftCursorableOffset(state);
1062         state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
1063         moveCursorToLeftCursorableOffset(state);
1064         state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
1065 
1066         // Variation selectors
1067         // U+00A9 is COPYRIGHT SIGN, U+FE0E is VARIATION SELECTOR-15. U+FE0F is VARIATION
1068         // SELECTOR-16.
1069         state.setByString("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
1070         moveCursorToRightCursorableOffset(state);
1071         state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
1072         moveCursorToRightCursorableOffset(state);
1073         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
1074         moveCursorToRightCursorableOffset(state);
1075         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
1076         moveCursorToRightCursorableOffset(state);
1077         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
1078         moveCursorToLeftCursorableOffset(state);
1079         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
1080         moveCursorToLeftCursorableOffset(state);
1081         state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
1082         moveCursorToLeftCursorableOffset(state);
1083         state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
1084         moveCursorToLeftCursorableOffset(state);
1085         state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
1086 
1087         // Keycap + variation selector
1088         state.setByString("| '1' U+FE0E U+20E3 '1' U+FE0E U+20E3 '1' U+FE0E U+20E3");
1089         moveCursorToRightCursorableOffset(state);
1090         state.assertEquals("'1' U+FE0E U+20E3 | '1' U+FE0E U+20E3 '1' U+FE0E U+20E3");
1091         moveCursorToRightCursorableOffset(state);
1092         state.assertEquals("'1' U+FE0E U+20E3 '1' U+FE0E U+20E3 | '1' U+FE0E U+20E3");
1093         moveCursorToRightCursorableOffset(state);
1094         state.assertEquals("'1' U+FE0E U+20E3 '1' U+FE0E U+20E3 '1' U+FE0E U+20E3 |");
1095         moveCursorToRightCursorableOffset(state);
1096         state.assertEquals("'1' U+FE0E U+20E3 '1' U+FE0E U+20E3 '1' U+FE0E U+20E3 |");
1097         moveCursorToLeftCursorableOffset(state);
1098         state.assertEquals("'1' U+FE0E U+20E3 '1' U+FE0E U+20E3 | '1' U+FE0E U+20E3");
1099         moveCursorToLeftCursorableOffset(state);
1100         state.assertEquals("'1' U+FE0E U+20E3 | '1' U+FE0E U+20E3 '1' U+FE0E U+20E3");
1101         moveCursorToLeftCursorableOffset(state);
1102         state.assertEquals("| '1' U+FE0E U+20E3 '1' U+FE0E U+20E3 '1' U+FE0E U+20E3");
1103         moveCursorToLeftCursorableOffset(state);
1104         state.assertEquals("| '1' U+FE0E U+20E3 '1' U+FE0E U+20E3 '1' U+FE0E U+20E3");
1105 
1106         // Flags
1107         // U+1F1E6 U+1F1E8 is Ascension Island flag.
1108         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
1109         moveCursorToRightCursorableOffset(state);
1110         state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
1111         moveCursorToRightCursorableOffset(state);
1112         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
1113         moveCursorToRightCursorableOffset(state);
1114         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
1115         moveCursorToRightCursorableOffset(state);
1116         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
1117         moveCursorToLeftCursorableOffset(state);
1118         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
1119         moveCursorToLeftCursorableOffset(state);
1120         state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
1121         moveCursorToLeftCursorableOffset(state);
1122         state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
1123         moveCursorToLeftCursorableOffset(state);
1124         state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
1125     }
1126 
testGetOffsetForHorizontal_Multilines()1127     public void testGetOffsetForHorizontal_Multilines() {
1128         // Emoticons for surrogate pairs tests.
1129         String testString = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
1130         final float width = mDefaultPaint.measureText(testString, 0, 6);
1131         StaticLayout layout = new StaticLayout(testString, mDefaultPaint, (int)width,
1132                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
1133         // We expect the line break to be after the third emoticon, but we allow flexibility of the
1134         // line break algorithm as long as the break is within the string. These other cases might
1135         // happen if for example the font has kerning between emoticons.
1136         final int lineBreakOffset = layout.getOffsetForHorizontal(1, 0.0f);
1137         assertEquals(0, layout.getLineForOffset(lineBreakOffset - 1));
1138 
1139         assertEquals(0, layout.getOffsetForHorizontal(0, 0.0f));
1140         assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width));
1141         assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width * 2));
1142 
1143         final int lineCount = layout.getLineCount();
1144         assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width));
1145         assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width * 2));
1146     }
1147 
testIsRtlCharAt()1148     public void testIsRtlCharAt() {
1149         {
1150             String testString = "ab(\u0623\u0624)c\u0625";
1151             StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
1152                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
1153 
1154             assertFalse(layout.isRtlCharAt(0));
1155             assertFalse(layout.isRtlCharAt(1));
1156             assertFalse(layout.isRtlCharAt(2));
1157             assertTrue(layout.isRtlCharAt(3));
1158             assertTrue(layout.isRtlCharAt(4));
1159             assertFalse(layout.isRtlCharAt(5));
1160             assertFalse(layout.isRtlCharAt(6));
1161             assertTrue(layout.isRtlCharAt(7));
1162         }
1163         {
1164             String testString = "\u0623\u0624(ab)\u0625c";
1165             StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
1166                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
1167 
1168             assertTrue(layout.isRtlCharAt(0));
1169             assertTrue(layout.isRtlCharAt(1));
1170             assertTrue(layout.isRtlCharAt(2));
1171             assertFalse(layout.isRtlCharAt(3));
1172             assertFalse(layout.isRtlCharAt(4));
1173             assertTrue(layout.isRtlCharAt(5));
1174             assertTrue(layout.isRtlCharAt(6));
1175             assertFalse(layout.isRtlCharAt(7));
1176             assertFalse(layout.isRtlCharAt(8));
1177         }
1178     }
1179 
testGetHorizontal()1180     public void testGetHorizontal() {
1181         String testString = "abc\u0623\u0624\u0625def";
1182         StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
1183                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
1184 
1185         assertEquals(layout.getPrimaryHorizontal(0), layout.getSecondaryHorizontal(0));
1186         assertTrue(layout.getPrimaryHorizontal(0) < layout.getPrimaryHorizontal(3));
1187         assertTrue(layout.getPrimaryHorizontal(3) < layout.getSecondaryHorizontal(3));
1188         assertTrue(layout.getPrimaryHorizontal(4) < layout.getSecondaryHorizontal(3));
1189         assertEquals(layout.getPrimaryHorizontal(4), layout.getSecondaryHorizontal(4));
1190         assertEquals(layout.getPrimaryHorizontal(3), layout.getSecondaryHorizontal(6));
1191         assertEquals(layout.getPrimaryHorizontal(6), layout.getSecondaryHorizontal(3));
1192         assertEquals(layout.getPrimaryHorizontal(7), layout.getSecondaryHorizontal(7));
1193     }
1194 
1195     public void testVeryLargeString() {
1196         final int MAX_COUNT = 1 << 21;
1197         final int WORD_SIZE = 32;
1198         char[] longText = new char[MAX_COUNT];
1199         for (int n = 0; n < MAX_COUNT; n++) {
1200             longText[n] = (n % WORD_SIZE) == 0 ? ' ' : 'm';
1201         }
1202         String longTextString = new String(longText);
1203         TextPaint paint = new TestingTextPaint();
1204         StaticLayout layout = new StaticLayout(longTextString, paint, DEFAULT_OUTER_WIDTH,
1205                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
1206         assertNotNull(layout);
1207     }
1208 
1209     public void testDoesntCrashWhenWordStyleOverlap() {
1210        // test case where word boundary overlaps multiple style spans
1211        SpannableStringBuilder text = new SpannableStringBuilder("word boundaries, overlap style");
1212        // span covers "boundaries"
1213        text.setSpan(new StyleSpan(Typeface.BOLD),
1214                    "word ".length(), "word boundaries".length(),
1215                    Spanned.SPAN_INCLUSIVE_INCLUSIVE);
1216        mDefaultPaint.setTextLocale(Locale.US);
1217        StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(),
1218                mDefaultPaint, DEFAULT_OUTER_WIDTH)
1219                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)  // enable hyphenation
1220                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
1221                .build();
1222        assertNotNull(layout);
1223     }
1224 }
1225