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;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Paint;
30 import android.graphics.Path;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.platform.test.annotations.Presubmit;
34 import android.text.Layout.Alignment;
35 import android.text.style.StrikethroughSpan;
36 
37 import androidx.test.filters.SmallTest;
38 import androidx.test.runner.AndroidJUnit4;
39 
40 import org.junit.Before;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Locale;
47 
48 @Presubmit
49 @SmallTest
50 @RunWith(AndroidJUnit4.class)
51 public class LayoutTest {
52     private static final int LINE_COUNT = 5;
53     private static final int LINE_HEIGHT = 12;
54     private static final int LINE_DESCENT = 4;
55     private static final CharSequence LAYOUT_TEXT = "alwei\t;sdfs\ndf @";
56 
57     private SpannableString mSpannedText;
58 
59     private int mWidth;
60     private Layout.Alignment mAlign;
61     private float mSpacingMult;
62     private float mSpacingAdd;
63     private TextPaint mTextPaint;
64 
65     @Before
setup()66     public void setup() {
67         mTextPaint = new TextPaint();
68         mSpannedText = new SpannableString(LAYOUT_TEXT);
69         mSpannedText.setSpan(new StrikethroughSpan(), 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
70         mWidth = 11;
71         mAlign = Alignment.ALIGN_CENTER;
72         mSpacingMult = 1;
73         mSpacingAdd = 2;
74     }
75 
76     @Test
testConstructor()77     public void testConstructor() {
78         new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, mSpacingAdd);
79     }
80 
81     @Test(expected = IllegalArgumentException.class)
testConstructorNull()82     public void testConstructorNull() {
83         new MockLayout(null, null, -1, null, 0, 0);
84     }
85 
86     @Test
testGetText()87     public void testGetText() {
88         CharSequence text = "test case 1";
89         Layout layout = new MockLayout(text, mTextPaint, mWidth,
90                 mAlign, mSpacingMult, mSpacingAdd);
91         assertEquals(text, layout.getText());
92 
93         layout = new MockLayout(null, mTextPaint, mWidth, mAlign, mSpacingMult, mSpacingAdd);
94         assertNull(layout.getText());
95     }
96 
97     @Test
testGetPaint()98     public void testGetPaint() {
99         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
100                 mAlign, mSpacingMult, mSpacingAdd);
101 
102         assertSame(mTextPaint, layout.getPaint());
103 
104         layout = new MockLayout(LAYOUT_TEXT, null, mWidth, mAlign, mSpacingMult, mSpacingAdd);
105         assertNull(layout.getPaint());
106     }
107 
108     @Test
testGetWidth()109     public void testGetWidth() {
110         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 10,
111                 mAlign, mSpacingMult, mSpacingAdd);
112         assertEquals(10,  layout.getWidth());
113 
114         layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 0, mAlign, mSpacingMult, mSpacingAdd);
115         assertEquals(0,  layout.getWidth());
116     }
117 
118     @Test
testGetEllipsizedWidth()119     public void testGetEllipsizedWidth() {
120         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 15,
121                 mAlign, mSpacingMult, mSpacingAdd);
122         assertEquals(15, layout.getEllipsizedWidth());
123 
124         layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 0, mAlign, mSpacingMult, mSpacingAdd);
125         assertEquals(0,  layout.getEllipsizedWidth());
126     }
127 
128     @Test
testIncreaseWidthTo()129     public void testIncreaseWidthTo() {
130         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
131                 mAlign, mSpacingMult, mSpacingAdd);
132         int oldWidth = layout.getWidth();
133 
134         layout.increaseWidthTo(oldWidth);
135         assertEquals(oldWidth, layout.getWidth());
136 
137         try {
138             layout.increaseWidthTo(oldWidth - 1);
139             fail("should throw runtime exception attempted to reduce Layout width");
140         } catch (RuntimeException e) {
141         }
142 
143         layout.increaseWidthTo(oldWidth + 1);
144         assertEquals(oldWidth + 1, layout.getWidth());
145     }
146 
147     @Test
testGetHeight()148     public void testGetHeight() {
149         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
150                 mAlign, mSpacingMult, mSpacingAdd);
151         assertEquals(60, layout.getHeight());
152     }
153 
154     @Test
testGetAlignment()155     public void testGetAlignment() {
156         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
157                 mAlign, mSpacingMult, mSpacingAdd);
158         assertSame(mAlign, layout.getAlignment());
159 
160         layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, null, mSpacingMult, mSpacingAdd);
161         assertNull(layout.getAlignment());
162     }
163 
164     @Test
testGetSpacingMultiplier()165     public void testGetSpacingMultiplier() {
166         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, -1, mSpacingAdd);
167         assertEquals(-1.0f, layout.getSpacingMultiplier(), 0.0f);
168 
169         layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, 5, mSpacingAdd);
170         assertEquals(5.0f, layout.getSpacingMultiplier(), 0.0f);
171     }
172 
173     @Test
testGetSpacingAdd()174     public void testGetSpacingAdd() {
175         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, -1);
176         assertEquals(-1.0f, layout.getSpacingAdd(), 0.0f);
177 
178         layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, 20);
179         assertEquals(20.0f, layout.getSpacingAdd(), 0.0f);
180     }
181 
182     @Test
testGetLineBounds()183     public void testGetLineBounds() {
184         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
185                 mAlign, mSpacingMult, mSpacingAdd);
186         Rect bounds = new Rect();
187 
188         assertEquals(32, layout.getLineBounds(2, bounds));
189         assertEquals(0, bounds.left);
190         assertEquals(mWidth, bounds.right);
191         assertEquals(24, bounds.top);
192         assertEquals(36, bounds.bottom);
193     }
194 
195     @Test
testGetLineForVertical()196     public void testGetLineForVertical() {
197         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
198                 mAlign, mSpacingMult, mSpacingAdd);
199         assertEquals(0, layout.getLineForVertical(-1));
200         assertEquals(0, layout.getLineForVertical(0));
201         assertEquals(0, layout.getLineForVertical(LINE_COUNT));
202         assertEquals(LINE_COUNT - 1, layout.getLineForVertical(1000));
203     }
204 
205     @Test
testGetLineForOffset()206     public void testGetLineForOffset() {
207         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
208                 mAlign, mSpacingMult, mSpacingAdd);
209         assertEquals(0, layout.getLineForOffset(-1));
210         assertEquals(1, layout.getLineForOffset(1));
211         assertEquals(LINE_COUNT - 1, layout.getLineForOffset(LINE_COUNT - 1));
212         assertEquals(LINE_COUNT - 1, layout.getLineForOffset(1000));
213     }
214 
215     @Test
testGetLineEnd()216     public void testGetLineEnd() {
217         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
218                 mAlign, mSpacingMult, mSpacingAdd);
219         assertEquals(2, layout.getLineEnd(1));
220     }
221 
222     @Test
testGetLineExtra_returnsZeroByDefault()223     public void testGetLineExtra_returnsZeroByDefault() {
224         final String text = "a\nb\nc\n";
225         final Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
226                 mAlign, 100 /* spacingMult*/, 100 /*spacingAdd*/);
227         final int lineCount = text.split("\n").length;
228         for (int i = 0; i < lineCount; i++) {
229             assertEquals(0, layout.getLineExtra(i));
230         }
231     }
232 
233     @Test
testGetLineVisibleEnd()234     public void testGetLineVisibleEnd() {
235         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
236                 mAlign, mSpacingMult, mSpacingAdd);
237 
238         assertEquals(2, layout.getLineVisibleEnd(1));
239         assertEquals(LINE_COUNT, layout.getLineVisibleEnd(LINE_COUNT - 1));
240         assertEquals(LAYOUT_TEXT.length(), layout.getLineVisibleEnd(LAYOUT_TEXT.length() - 1));
241         try {
242             layout.getLineVisibleEnd(LAYOUT_TEXT.length());
243             fail("should throw .StringIndexOutOfBoundsException here");
244         } catch (StringIndexOutOfBoundsException e) {
245         }
246     }
247 
248     @Test
testGetLineBottom()249     public void testGetLineBottom() {
250         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
251                 mAlign, mSpacingMult, mSpacingAdd);
252         assertEquals(LINE_HEIGHT, layout.getLineBottom(0));
253     }
254 
255     @Test
testGetLineBaseline()256     public void testGetLineBaseline() {
257         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
258                 mAlign, mSpacingMult, mSpacingAdd);
259         assertEquals(8, layout.getLineBaseline(0));
260     }
261 
262     @Test
testGetLineAscent()263     public void testGetLineAscent() {
264         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
265                 mAlign, mSpacingMult, mSpacingAdd);
266         assertEquals(-8, layout.getLineAscent(0));
267     }
268 
269     @Test
testGetParagraphAlignment()270     public void testGetParagraphAlignment() {
271         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
272                 mAlign, mSpacingMult, mSpacingAdd);
273         assertSame(mAlign, layout.getParagraphAlignment(0));
274 
275         layout = new MockLayout(mSpannedText, mTextPaint, mWidth,
276                 mAlign, mSpacingMult, mSpacingAdd);
277         assertSame(mAlign, layout.getParagraphAlignment(0));
278         assertSame(mAlign, layout.getParagraphAlignment(1));
279     }
280 
281     @Test
testGetParagraphLeft()282     public void testGetParagraphLeft() {
283         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
284                 mAlign, mSpacingMult, mSpacingAdd);
285         assertEquals(0, layout.getParagraphLeft(0));
286     }
287 
288     @Test
testGetParagraphRight()289     public void testGetParagraphRight() {
290         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
291                 mAlign, mSpacingMult, mSpacingAdd);
292         assertEquals(mWidth, layout.getParagraphRight(0));
293     }
294 
295     @Test
testGetSelectionWithEmptySelection()296     public void testGetSelectionWithEmptySelection() {
297         final Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
298                 mAlign, mSpacingMult, mSpacingAdd);
299 
300         /*
301          * When the selection is empty (i.e. the start and the end index are the same), we do not
302          * expect any rectangles to be generated.
303          */
304 
305         layout.getSelection(5 /* startIndex */, 5 /* endIndex */,
306                 (left, top, right, bottom, textSelectionLayout) -> fail(
307                         String.format(Locale.getDefault(),
308                         "Did not expect any rectangles, got a rectangle with (left: %f,"
309                                 + " top: %f), (right: %f, bottom: %f)",
310                         left, top, right, bottom)));
311     }
312 
313     @Test
testGetSelectionWithASingleLineSelection()314     public void testGetSelectionWithASingleLineSelection() {
315         final Layout layout = new StaticLayout("abc", mTextPaint, Integer.MAX_VALUE,
316                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
317 
318         final List<RectF> rectangles = new ArrayList<>();
319 
320         layout.getSelection(0 /* startIndex */, 1 /* endIndex */,
321                 (left, top, right, bottom, textSelectionLayout) -> rectangles.add(
322                         new RectF(left, top, right, bottom)));
323 
324         /*
325          * The selection we expect will only cover the letter "a". Hence, we expect one rectangle
326          * to be generated and this rectangle should start at the top left of the canvas and should
327          * end somewhere to the right and down.
328          *
329          * | a | b c
330          *
331          */
332 
333         assertEquals(1, rectangles.size());
334 
335         final RectF rectangle = rectangles.get(0);
336 
337         assertEquals(0, rectangle.left, 0.0f);
338         assertEquals(0, rectangle.top, 0.0f);
339         assertTrue(rectangle.right > 0);
340         assertTrue(rectangle.bottom > 0);
341     }
342 
343     @Test
344     public void
testGetSelectionWithMultilineSelection_secondLineSelectionEndsBeforeFirstCharacter()345             testGetSelectionWithMultilineSelection_secondLineSelectionEndsBeforeFirstCharacter() {
346         final Layout layout = new StaticLayout("a\nb\nc", mTextPaint, Integer.MAX_VALUE,
347                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
348 
349         final List<RectF> rectangles = new ArrayList<>();
350 
351         layout.getSelection(0 /* startIndex */, 2 /* endIndex */,
352                 (left, top, right, bottom, textSelectionLayout) -> rectangles.add(
353                         new RectF(left, top, right, bottom)));
354 
355         /*
356          * The selection that will be selected is "a\n" - the selection starts at the beginning
357          * of the first line and ends at the start of the second line. This means the selection
358          * highlight will span from the beginning of the first line to the end of the first line
359          * and will appear as a zero width line at the beginning of the second line.
360          *
361          * Hence, we expect three rectangles - one that will select the "a" on the first line,
362          * one that will extend the selection from the "a" to the end of the first line and one
363          * that will prepare the selection for the second line.
364          *
365          * | a | *topToEndOfLineRectangle* |
366          * | b
367          *   c
368          */
369 
370         assertEquals(3, rectangles.size());
371 
372         final RectF topRectangle = rectangles.get(0);
373         final RectF topToEndOfLineRectangle = rectangles.get(1);
374         final RectF bottomLineStartRectangle = rectangles.get(2);
375 
376         assertFalse(topRectangle.intersect(bottomLineStartRectangle));
377         assertTrue(topRectangle.top < bottomLineStartRectangle.top);
378         assertTrue(topRectangle.left == bottomLineStartRectangle.left);
379 
380         assertFalse(topRectangle.intersect(topToEndOfLineRectangle));
381         assertEquals(Integer.MAX_VALUE, topToEndOfLineRectangle.right, 1);
382         assertTrue(topRectangle.top == topToEndOfLineRectangle.top);
383         assertTrue(topRectangle.right == topToEndOfLineRectangle.left);
384         assertTrue(topRectangle.bottom == topToEndOfLineRectangle.bottom);
385 
386         assertEquals(0, bottomLineStartRectangle.left, 0.0f);
387         assertEquals(0, bottomLineStartRectangle.right, 0.0f);
388     }
389 
390     @Test
391     public void testGetSelectionWithMultilineSelection_secondLineSelectionEndsAfterACharacter() {
392         final Layout layout = new StaticLayout("a\nb\nc", mTextPaint, Integer.MAX_VALUE,
393                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
394 
395         final List<RectF> rectangles = new ArrayList<>();
396 
397         layout.getSelection(0 /* startIndex */, 3 /* endIndex */,
398                 (left, top, right, bottom, textSelectionLayout) -> rectangles.add(
399                         new RectF(left, top, right, bottom)));
400 
401         /*
402          * The selection that will be selected is "a\nb" - the selection starts at the beginning
403          * of the first line and ends at the end of the letter "b". This means the selection
404          * highlight will span from the beginning of the first line to the end of the first line
405          * and will also cover the letter "b" on the second line.
406          *
407          * We expect four rectangles - one that will select the "a" on the first line,
408          * one that will extend the selection from the "a" to the end of the first line the one
409          * from the previous case that will prepare the selection for the second line and finally
410          * one that will select the letter b.
411          *
412          *  | a | *topToEndOfLineRectangle* |
413          * || b |
414          *    c
415          */
416 
417         assertEquals(4, rectangles.size());
418 
419         final RectF topRectangle = rectangles.get(0);
420         final RectF topToEndOfLineRectangle = rectangles.get(1);
421         final RectF bottomRectangle = rectangles.get(2);
422         final RectF bottomLineStartRectangle = rectangles.get(3);
423 
424         assertTrue(topRectangle.top == topToEndOfLineRectangle.top);
425         assertTrue(bottomLineStartRectangle.top == bottomRectangle.top);
426         assertTrue(bottomLineStartRectangle.bottom == bottomRectangle.bottom);
427         assertEquals(0, bottomLineStartRectangle.left, 0.0f);
428         assertEquals(0, bottomLineStartRectangle.right, 0.0f);
429         assertEquals(0, bottomRectangle.left, 0.0f);
430         assertTrue(bottomRectangle.right > 0);
431     }
432 
433     @Test
testGetSelectionPathWithASingleLineSelection()434     public void testGetSelectionPathWithASingleLineSelection() {
435         final Layout layout = new StaticLayout("abc", mTextPaint, Integer.MAX_VALUE,
436                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
437 
438         final List<RectF> rectangles = new ArrayList<>();
439 
440         layout.getSelection(0 /* startIndex */, 1 /* endIndex */,
441                 (left, top, right, bottom, textSelectionLayout) -> rectangles.add(
442                         new RectF(left, top, right, bottom)));
443 
444         /*
445          * In the single line selection case, we expect that only one rectangle covering the letter
446          * "a" will be generated. Hence, we expect that the generated path will only consist of
447          * that rectangle as well.
448          *
449          * | a | b c
450          *
451          */
452 
453         assertEquals(1, rectangles.size());
454 
455         final RectF rectangle = rectangles.get(0);
456 
457         final Path generatedPath = new Path();
458         layout.getSelectionPath(0 /* startIndex */, 1 /* endIndex */, generatedPath);
459 
460         final RectF pathRectangle = new RectF();
461 
462         assertTrue(generatedPath.isRect(pathRectangle));
463         assertEquals(rectangle, pathRectangle);
464     }
465 
466     @Test
testGetSelection_latinTextDirection()467     public void testGetSelection_latinTextDirection() {
468         final Layout layout = new StaticLayout("abc", mTextPaint, Integer.MAX_VALUE,
469                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
470 
471         layout.getSelection(0 /* startIndex */, 2 /* endIndex */,
472                 (left, top, right, bottom, textSelectionLayout) ->
473                         assertEquals(Layout.TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT,
474                                 textSelectionLayout));
475     }
476 
477     @Test
testGetSelection_arabicTextDirection()478     public void testGetSelection_arabicTextDirection() {
479         final Layout layout = new StaticLayout("غينيا", mTextPaint, Integer.MAX_VALUE,
480                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
481 
482         layout.getSelection(0 /* startIndex */, 2 /* endIndex */,
483                 (left, top, right, bottom, textSelectionLayout) ->
484                         assertEquals(Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
485                                 textSelectionLayout));
486     }
487 
488     @Test
testGetSelection_mixedLatinAndArabicTextDirection()489     public void testGetSelection_mixedLatinAndArabicTextDirection() {
490         final Layout layout = new StaticLayout("abcغينيا", mTextPaint, Integer.MAX_VALUE,
491                 Alignment.ALIGN_LEFT, mSpacingMult, mSpacingAdd, false);
492 
493         final List<Integer> layouts = new ArrayList<>(2);
494 
495         layout.getSelection(0 /* startIndex */, 6 /* endIndex */,
496                 (left, top, right, bottom, textSelectionLayout) -> layouts.add(
497                         textSelectionLayout));
498 
499         assertEquals(2, layouts.size());
500         assertEquals(Layout.TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT, (long) layouts.get(0));
501         assertEquals(Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, (long) layouts.get(1));
502     }
503 
504     @Test
testIsSpanned()505     public void testIsSpanned() {
506         MockLayout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
507                 mAlign, mSpacingMult, mSpacingAdd);
508         // default is not spanned text
509         assertFalse(layout.mockIsSpanned());
510 
511         // try to create a spanned text
512         layout = new MockLayout(mSpannedText, mTextPaint, mWidth,
513                 mAlign, mSpacingMult, mSpacingAdd);
514         assertTrue(layout.mockIsSpanned());
515     }
516 
517     private static final class MockLayout extends Layout {
MockLayout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)518         MockLayout(CharSequence text, TextPaint paint, int width,
519                 Alignment align, float spacingmult, float spacingadd) {
520             super(text, paint, width, align, spacingmult, spacingadd);
521         }
522 
mockIsSpanned()523         protected boolean mockIsSpanned() {
524             return super.isSpanned();
525         }
526 
527         @Override
getBottomPadding()528         public int getBottomPadding() {
529             return 0;
530         }
531 
532         @Override
getEllipsisCount(int line)533         public int getEllipsisCount(int line) {
534             return 0;
535         }
536 
537         @Override
getEllipsisStart(int line)538         public int getEllipsisStart(int line) {
539             return 0;
540         }
541 
542         @Override
getLineContainsTab(int line)543         public boolean getLineContainsTab(int line) {
544             return false;
545         }
546 
547         @Override
getLineCount()548         public int getLineCount() {
549             return LINE_COUNT;
550         }
551 
552         @Override
getLineDescent(int line)553         public int getLineDescent(int line) {
554             return LINE_DESCENT;
555         }
556 
557         @Override
getLineDirections(int line)558         public Directions getLineDirections(int line) {
559             return Layout.DIRS_ALL_LEFT_TO_RIGHT;
560         }
561 
562         @Override
getLineStart(int line)563         public int getLineStart(int line) {
564             if (line < 0) {
565                 return 0;
566             }
567             return line;
568         }
569 
570         @Override
getLineTop(int line)571         public int getLineTop(int line) {
572             if (line < 0) {
573                 return 0;
574             }
575             return LINE_HEIGHT * (line);
576         }
577 
578         @Override
getParagraphDirection(int line)579         public int getParagraphDirection(int line) {
580             return 0;
581         }
582 
583         @Override
getTopPadding()584         public int getTopPadding() {
585             return 0;
586         }
587     }
588 
589     @Test
testGetLineWidth()590     public void testGetLineWidth() {
591         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
592                 mAlign, mSpacingMult, mSpacingAdd);
593         for (int i = 0; i < LINE_COUNT; i++) {
594             int start = layout.getLineStart(i);
595             int end = layout.getLineEnd(i);
596             String text = LAYOUT_TEXT.toString().substring(start, end);
597             assertEquals(mTextPaint.measureText(text), layout.getLineWidth(i), 1.0f);
598         }
599     }
600 
601     @Test
testGetCursorPath()602     public void testGetCursorPath() {
603         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
604                 mAlign, mSpacingMult, mSpacingAdd);
605         Path path = new Path();
606         final float epsilon = 1.0f;
607         for (int i = 0; i < LINE_COUNT; i++) {
608             layout.getCursorPath(i, path, LAYOUT_TEXT);
609             RectF bounds = new RectF();
610             path.computeBounds(bounds, false);
611             assertTrue(bounds.top >= layout.getLineTop(i) - epsilon);
612             assertTrue(bounds.bottom <= layout.getLineBottom(i) + epsilon);
613         }
614     }
615 
616     @Test
testDraw()617     public void testDraw() {
618         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
619                 mAlign, mSpacingMult, mSpacingAdd);
620         final int width = 256;
621         final int height = 256;
622         MockCanvas c = new MockCanvas(width, height);
623         layout.draw(c);
624         List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
625         assertEquals(LINE_COUNT, drawCommands.size());
626         for (int i = 0; i < LINE_COUNT; i++) {
627             MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
628             int start = layout.getLineStart(i);
629             int end = layout.getLineEnd(i);
630             assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text);
631             float expected_y = (i + 1) * LINE_HEIGHT - LINE_DESCENT;
632             assertEquals(expected_y, drawCommand.y, 0.0f);
633         }
634     }
635 
636     private final class MockCanvas extends Canvas {
637 
638         class DrawCommand {
639             public final String text;
640             public final float x;
641             public final float y;
642 
DrawCommand(String text, float x, float y)643             DrawCommand(String text, float x, float y) {
644                 this.text = text;
645                 this.x = x;
646                 this.y = y;
647             }
648         }
649 
650         List<DrawCommand> mDrawCommands;
651 
MockCanvas(int width, int height)652         MockCanvas(int width, int height) {
653             super();
654             mDrawCommands = new ArrayList<>();
655             Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
656             setBitmap(bitmap);
657         }
658 
659         // Drawing text with either drawText or drawTextRun is valid; we don't care which.
660         // We also don't care which of the string representations is used.
661 
662         @Override
drawText(String text, int start, int end, float x, float y, Paint p)663         public void drawText(String text, int start, int end, float x, float y, Paint p) {
664             mDrawCommands.add(new DrawCommand(text.substring(start, end), x, y));
665         }
666 
667         @Override
drawText(CharSequence text, int start, int end, float x, float y, Paint p)668         public void drawText(CharSequence text, int start, int end, float x, float y, Paint p) {
669             drawText(text.toString(), start, end, x, y, p);
670         }
671 
672         @Override
drawText(char[] text, int index, int count, float x, float y, Paint p)673         public void drawText(char[] text, int index, int count, float x, float y, Paint p) {
674             mDrawCommands.add(new DrawCommand(new String(text, index, count), x, y));
675         }
676 
677         @Override
drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)678         public void drawTextRun(CharSequence text, int start, int end, int contextStart,
679                 int contextEnd, float x, float y, boolean isRtl, Paint paint) {
680             drawText(text, start, end, x, y, paint);
681         }
682 
683         @Override
drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)684         public void drawTextRun(char[] text, int index, int count, int contextIndex,
685                 int contextCount, float x, float y, boolean isRtl, Paint paint) {
686             drawText(text, index, count, x, y, paint);
687         }
688 
getDrawCommands()689         List<DrawCommand> getDrawCommands() {
690             return mDrawCommands;
691         }
692     }
693 
694     private static final String LTR = "a";
695     private static final String RTL = "\u05D0";  // HEBREW LETTER ALEF
696     private static final String LTR_SP = "\uD801\uDCB0";  // OSAGE CAPITAL LETTER A
697     private static final String RTL_SP = "\uD83A\uDD00";  // ADLAM CAPITAL LETTER ALIF
698 
699     private static final String LRI = "\u2066";  // LEFT-TO-RIGHT ISOLATE
700     private static final String RLI = "\u2067";  // RIGHT-TO-LEFT ISOLATE
701     private static final String PDI = "\u2069";  // POP DIRECTIONAL ISOLATE
702 
assertPrimaryIsTrailingPrevious(String input, boolean[] expected)703     private static void assertPrimaryIsTrailingPrevious(String input, boolean[] expected) {
704         assertEquals(input.length() + 1, expected.length);
705 
706         boolean[] actual = new boolean[expected.length];
707         TextPaint paint = new TextPaint();
708         paint.setTextSize(16.0f);
709         Layout layout = StaticLayout.Builder.obtain(
710                 input, 0, input.length(), paint, Integer.MAX_VALUE).build();
711         for (int i = 0; i <= input.length(); ++i) {
712             actual[i] = layout.primaryIsTrailingPrevious(i);
713         }
714         assertArrayEquals(expected, actual);
715         assertArrayEquals(actual, layout.primaryIsTrailingPreviousAllLineOffsets(0));
716     }
717 
718     @Test
testPrimaryIsTrailingPrevious()719     public void testPrimaryIsTrailingPrevious() {
720         assertPrimaryIsTrailingPrevious(
721                 LTR + " " + LTR + LTR + " " + LTR + LTR + LTR,
722                 new boolean[]{false, false, false, false, false, false, false, false, false});
723         assertPrimaryIsTrailingPrevious(
724                 RTL + " " + RTL + RTL + " " + RTL + RTL + RTL,
725                 new boolean[]{false, false, false, false, false, false, false, false, false});
726         assertPrimaryIsTrailingPrevious(
727                 LTR + RTL + LTR + RTL + LTR,
728                 new boolean[]{false, true, false, true, false, false});
729         assertPrimaryIsTrailingPrevious(
730                 RTL + LTR + RTL + LTR + RTL,
731                 new boolean[]{false, true, false, true, false, false});
732         assertPrimaryIsTrailingPrevious(
733                 RTL_SP + LTR_SP + RTL_SP + LTR_SP + RTL_SP,
734                 new boolean[]{
735                         false, false, true, false, false, false, true, false, false, false, false});
736         assertPrimaryIsTrailingPrevious(
737                 LTR_SP + RTL_SP + LTR_SP + RTL_SP + LTR_SP,
738                 new boolean[]{
739                         false, false, true, false, false, false, true, false, false, false, false});
740         assertPrimaryIsTrailingPrevious(
741                 LTR + RLI + LTR + RTL + PDI + LTR,
742                 new boolean[]{false, false, true, false, false, false, false});
743         assertPrimaryIsTrailingPrevious(
744                 RTL + LRI + RTL + LTR + PDI + RTL,
745                 new boolean[]{false, false, true, false, false, false, false});
746         assertPrimaryIsTrailingPrevious(
747                 "",
748                 new boolean[]{false});
749     }
750 }
751 
752