1 /*
2  * Copyright (C) 2017 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 static android.text.TextDirectionHeuristics.LTR;
20 import static android.text.TextDirectionHeuristics.RTL;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertNotSame;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28 
29 import android.content.Context;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.Rect;
34 import android.graphics.Typeface;
35 import android.text.Layout;
36 import android.text.PrecomputedText;
37 import android.text.PrecomputedText.Params;
38 import android.text.Spannable;
39 import android.text.SpannableStringBuilder;
40 import android.text.Spanned;
41 import android.text.TextDirectionHeuristics;
42 import android.text.TextPaint;
43 import android.text.style.BackgroundColorSpan;
44 import android.text.style.LocaleSpan;
45 import android.text.style.TextAppearanceSpan;
46 import android.text.style.TypefaceSpan;
47 
48 import androidx.annotation.IntRange;
49 import androidx.annotation.NonNull;
50 import androidx.test.InstrumentationRegistry;
51 import androidx.test.filters.SmallTest;
52 import androidx.test.runner.AndroidJUnit4;
53 
54 import org.junit.Test;
55 import org.junit.runner.RunWith;
56 
57 import java.util.Locale;
58 
59 @SmallTest
60 @RunWith(AndroidJUnit4.class)
61 public class PrecomputedTextTest {
62 
63     private static final CharSequence NULL_CHAR_SEQUENCE = null;
64     private static final String STRING = "Hello, World!";
65     private static final String MULTIPARA_STRING = "Hello,\nWorld!";
66 
67     private static final int SPAN_START = 3;
68     private static final int SPAN_END = 7;
69     private static final LocaleSpan SPAN = new LocaleSpan(Locale.US);
70     private static final Spanned SPANNED;
71     static {
72         final SpannableStringBuilder ssb = new SpannableStringBuilder(STRING);
ssb.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)73         ssb.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
74         SPANNED = ssb;
75     }
76 
77     private static final TextPaint PAINT = new TextPaint();
78 
79     @Test
testParams_create()80     public void testParams_create() {
81         assertNotNull(new Params.Builder(PAINT).build());
82         assertNotNull(new Params.Builder(PAINT)
83                 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build());
84         assertNotNull(new Params.Builder(PAINT)
85                 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
86                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL).build());
87         assertNotNull(new Params.Builder(PAINT)
88                 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
89                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
90                 .setTextDirection(LTR).build());
91     }
92 
93     @Test
testParams_SetGet()94     public void testParams_SetGet() {
95         assertEquals(Layout.BREAK_STRATEGY_SIMPLE, new Params.Builder(PAINT)
96                 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build().getBreakStrategy());
97         assertEquals(Layout.HYPHENATION_FREQUENCY_NONE, new Params.Builder(PAINT)
98                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE).build()
99                         .getHyphenationFrequency());
100         assertEquals(RTL, new Params.Builder(PAINT).setTextDirection(RTL).build()
101                 .getTextDirection());
102     }
103 
104     @Test
testParams_GetDefaultValues()105     public void testParams_GetDefaultValues() {
106         assertEquals(Layout.BREAK_STRATEGY_HIGH_QUALITY,
107                      new Params.Builder(PAINT).build().getBreakStrategy());
108         assertEquals(Layout.HYPHENATION_FREQUENCY_NORMAL,
109                      new Params.Builder(PAINT).build().getHyphenationFrequency());
110         assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
111                      new Params.Builder(PAINT).build().getTextDirection());
112     }
113 
114     @Test
testParams_equals()115     public void testParams_equals() {
116         final Params base = new Params.Builder(PAINT)
117                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
118                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
119                 .setTextDirection(LTR).build();
120 
121         assertFalse(base.equals(null));
122         assertTrue(base.equals(base));
123         assertFalse(base.equals(new Object()));
124 
125         Params other = new Params.Builder(PAINT)
126                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
127                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
128                 .setTextDirection(LTR).build();
129         assertTrue(base.equals(other));
130         assertTrue(other.equals(base));
131         assertEquals(base.hashCode(), other.hashCode());
132 
133         other = new Params.Builder(PAINT)
134                 .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
135                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
136                 .setTextDirection(LTR).build();
137         assertFalse(base.equals(other));
138         assertFalse(other.equals(base));
139 
140         other = new Params.Builder(PAINT)
141                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
142                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
143                 .setTextDirection(LTR).build();
144         assertFalse(base.equals(other));
145         assertFalse(other.equals(base));
146 
147 
148         other = new Params.Builder(PAINT)
149                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
150                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
151                 .setTextDirection(RTL).build();
152         assertFalse(base.equals(other));
153         assertFalse(other.equals(base));
154 
155 
156         TextPaint anotherPaint = new TextPaint(PAINT);
157         anotherPaint.setTextSize(PAINT.getTextSize() * 2.0f);
158         other = new Params.Builder(anotherPaint)
159                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
160                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
161                 .setTextDirection(LTR).build();
162         assertFalse(base.equals(other));
163         assertFalse(other.equals(base));
164 
165     }
166 
167     @Test
testCreate_withNull()168     public void testCreate_withNull() {
169         final Params param = new Params.Builder(PAINT).build();
170         try {
171             PrecomputedText.create(NULL_CHAR_SEQUENCE, param);
172             fail();
173         } catch (NullPointerException e) {
174             // pass
175         }
176         try {
177             PrecomputedText.create(STRING, null);
178             fail();
179         } catch (NullPointerException e) {
180             // pass
181         }
182     }
183 
184     @Test
testCreateForDifferentDirection()185     public void testCreateForDifferentDirection() {
186         final Params param = new Params.Builder(PAINT).setTextDirection(LTR).build();
187         final PrecomputedText textWithLTR = PrecomputedText.create(STRING, param);
188         final Params newParam = new Params.Builder(PAINT).setTextDirection(RTL).build();
189         final PrecomputedText textWithRTL = PrecomputedText.create(textWithLTR, newParam);
190         assertNotNull(textWithRTL);
191         assertNotSame(textWithLTR, textWithRTL);
192         assertEquals(textWithLTR.toString(), textWithRTL.toString());
193     }
194 
195     @Test
testCharSequenceInteface()196     public void testCharSequenceInteface() {
197         final Params param = new Params.Builder(PAINT).build();
198         final CharSequence s = PrecomputedText.create(STRING, param);
199         assertEquals(STRING.length(), s.length());
200         assertEquals('H', s.charAt(0));
201         assertEquals('e', s.charAt(1));
202         assertEquals('l', s.charAt(2));
203         assertEquals('l', s.charAt(3));
204         assertEquals('o', s.charAt(4));
205         assertEquals(',', s.charAt(5));
206         assertEquals("Hello, World!", s.toString());
207 
208         final CharSequence s3 = s.subSequence(0, 3);
209         assertEquals(3, s3.length());
210         assertEquals('H', s3.charAt(0));
211         assertEquals('e', s3.charAt(1));
212         assertEquals('l', s3.charAt(2));
213 
214     }
215 
216     @Test
testSpannedInterface_Spanned()217     public void testSpannedInterface_Spanned() {
218         final Params param = new Params.Builder(PAINT).build();
219         final Spanned s = PrecomputedText.create(SPANNED, param);
220         final LocaleSpan[] spans = s.getSpans(0, s.length(), LocaleSpan.class);
221         assertNotNull(spans);
222         assertEquals(1, spans.length);
223         assertEquals(SPAN, spans[0]);
224 
225         assertEquals(SPAN_START, s.getSpanStart(SPAN));
226         assertEquals(SPAN_END, s.getSpanEnd(SPAN));
227         assertTrue((s.getSpanFlags(SPAN) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
228 
229         assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), LocaleSpan.class));
230         assertEquals(SPAN_END, s.nextSpanTransition(SPAN_START, s.length(), LocaleSpan.class));
231     }
232 
233     @Test
testSpannedInterface_Spannable()234     public void testSpannedInterface_Spannable() {
235         final BackgroundColorSpan span = new BackgroundColorSpan(Color.RED);
236         final Params param = new Params.Builder(PAINT).build();
237         final Spannable s = PrecomputedText.create(STRING, param);
238         assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
239 
240         s.setSpan(span, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
241 
242         final BackgroundColorSpan[] spans = s.getSpans(0, s.length(), BackgroundColorSpan.class);
243         assertEquals(SPAN_START, s.getSpanStart(span));
244         assertEquals(SPAN_END, s.getSpanEnd(span));
245         assertTrue((s.getSpanFlags(span) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
246 
247         assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), BackgroundColorSpan.class));
248         assertEquals(SPAN_END,
249                 s.nextSpanTransition(SPAN_START, s.length(), BackgroundColorSpan.class));
250 
251         s.removeSpan(span);
252         assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
253     }
254 
255     @Test(expected = IllegalArgumentException.class)
testSpannedInterface_Spannable_setSpan_MetricsAffectingSpan()256     public void testSpannedInterface_Spannable_setSpan_MetricsAffectingSpan() {
257         final Params param = new Params.Builder(PAINT).build();
258         final Spannable s = PrecomputedText.create(SPANNED, param);
259         s.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
260     }
261 
262     @Test(expected = IllegalArgumentException.class)
testSpannedInterface_Spannable_removeSpan_MetricsAffectingSpan()263     public void testSpannedInterface_Spannable_removeSpan_MetricsAffectingSpan() {
264         final Params param = new Params.Builder(PAINT).build();
265         final Spannable s = PrecomputedText.create(SPANNED, param);
266         s.removeSpan(SPAN);
267     }
268 
269     @Test
testSpannedInterface_String()270     public void testSpannedInterface_String() {
271         final Params param = new Params.Builder(PAINT).build();
272         final Spanned s = PrecomputedText.create(STRING, param);
273         LocaleSpan[] spans = s.getSpans(0, s.length(), LocaleSpan.class);
274         assertNotNull(spans);
275         assertEquals(0, spans.length);
276 
277         assertEquals(-1, s.getSpanStart(SPAN));
278         assertEquals(-1, s.getSpanEnd(SPAN));
279         assertEquals(0, s.getSpanFlags(SPAN));
280 
281         assertEquals(s.length(), s.nextSpanTransition(0, s.length(), LocaleSpan.class));
282     }
283 
284     @Test
testGetParagraphCount()285     public void testGetParagraphCount() {
286         final Params param = new Params.Builder(PAINT).build();
287         final PrecomputedText pm = PrecomputedText.create(STRING, param);
288         assertEquals(1, pm.getParagraphCount());
289         assertEquals(0, pm.getParagraphStart(0));
290         assertEquals(STRING.length(), pm.getParagraphEnd(0));
291 
292         final PrecomputedText pm1 = PrecomputedText.create(MULTIPARA_STRING, param);
293         assertEquals(2, pm1.getParagraphCount());
294         assertEquals(0, pm1.getParagraphStart(0));
295         assertEquals(7, pm1.getParagraphEnd(0));
296         assertEquals(7, pm1.getParagraphStart(1));
297         assertEquals(pm1.length(), pm1.getParagraphEnd(1));
298     }
299 
300     @Test
testGetWidth()301     public void testGetWidth() {
302         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
303 
304         // The test font has following coverage and width.
305         // U+0020: 10em
306         // U+002E (.): 10em
307         // U+0043 (C): 100em
308         // U+0049 (I): 1em
309         // U+004C (L): 50em
310         // U+0056 (V): 5em
311         // U+0058 (X): 10em
312         // U+005F (_): 0em
313         // U+FFFD (invalid surrogate will be replaced to this): 7em
314         // U+10331 (\uD800\uDF31): 10em
315         final Typeface tf = new Typeface.Builder(context.getAssets(),
316                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
317         final TextPaint paint = new TextPaint();
318         paint.setTypeface(tf);
319         paint.setTextSize(1);  // Make 1em = 1px
320 
321         final Params param = new Params.Builder(paint).build();
322         assertEquals(0.0f, PrecomputedText.create("", param).getWidth(0, 0), 0.0f);
323 
324         assertEquals(0.0f, PrecomputedText.create("I", param).getWidth(0, 0), 0.0f);
325         assertEquals(0.0f, PrecomputedText.create("I", param).getWidth(1, 1), 0.0f);
326         assertEquals(1.0f, PrecomputedText.create("I", param).getWidth(0, 1), 0.0f);
327 
328         assertEquals(0.0f, PrecomputedText.create("V", param).getWidth(0, 0), 0.0f);
329         assertEquals(0.0f, PrecomputedText.create("V", param).getWidth(1, 1), 0.0f);
330         assertEquals(5.0f, PrecomputedText.create("V", param).getWidth(0, 1), 0.0f);
331 
332         assertEquals(0.0f, PrecomputedText.create("IV", param).getWidth(0, 0), 0.0f);
333         assertEquals(0.0f, PrecomputedText.create("IV", param).getWidth(1, 1), 0.0f);
334         assertEquals(0.0f, PrecomputedText.create("IV", param).getWidth(2, 2), 0.0f);
335         assertEquals(1.0f, PrecomputedText.create("IV", param).getWidth(0, 1), 0.0f);
336         assertEquals(5.0f, PrecomputedText.create("IV", param).getWidth(1, 2), 0.0f);
337         assertEquals(6.0f, PrecomputedText.create("IV", param).getWidth(0, 2), 0.0f);
338 
339         assertEquals(0.0f, PrecomputedText.create("I\nV", param).getWidth(0, 0), 0.0f);
340         assertEquals(0.0f, PrecomputedText.create("I\nV", param).getWidth(1, 1), 0.0f);
341         assertEquals(0.0f, PrecomputedText.create("I\nV", param).getWidth(2, 2), 0.0f);
342         assertEquals(0.0f, PrecomputedText.create("I\nV", param).getWidth(3, 3), 0.0f);
343         assertEquals(1.0f, PrecomputedText.create("I\nV", param).getWidth(0, 1), 0.0f);
344         assertEquals(1.0f, PrecomputedText.create("I\nV", param).getWidth(0, 2), 0.0f);
345         assertEquals(5.0f, PrecomputedText.create("I\nV", param).getWidth(2, 3), 0.0f);
346     }
347 
348     @Test
testGetWidth_multiStyle()349     public void testGetWidth_multiStyle() {
350         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
351         final SpannableStringBuilder ssb = new SpannableStringBuilder("II");
352         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 1 /* text size */,
353                 null /* color */, null /* link color */), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
354         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
355                 null /* color */, null /* link color */), 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
356 
357         final Typeface tf = new Typeface.Builder(context.getAssets(),
358                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
359         final TextPaint paint = new TextPaint();
360         paint.setTypeface(tf);
361         paint.setTextSize(1);  // Make 1em = 1px
362 
363         final Params param = new Params.Builder(paint).build();
364 
365         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(0, 0), 0.0f);
366         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(1, 1), 0.0f);
367         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(2, 2), 0.0f);
368 
369         assertEquals(1.0f, PrecomputedText.create(ssb, param).getWidth(0, 1), 0.0f);
370         assertEquals(5.0f, PrecomputedText.create(ssb, param).getWidth(1, 2), 0.0f);
371 
372         assertEquals(6.0f, PrecomputedText.create(ssb, param).getWidth(0, 2), 0.0f);
373     }
374 
375     @Test
testGetWidth_multiStyle2()376     public void testGetWidth_multiStyle2() {
377         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
378         final SpannableStringBuilder ssb = new SpannableStringBuilder("IVI");
379         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 1 /* text size */,
380                 null /* color */, null /* link color */), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
381         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
382                 null /* color */, null /* link color */), 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
383         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
384                 null /* color */, null /* link color */), 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
385 
386         final Typeface tf = new Typeface.Builder(context.getAssets(),
387                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
388         final TextPaint paint = new TextPaint();
389         paint.setTypeface(tf);
390         paint.setTextSize(1);  // Make 1em = 1px
391 
392         final Params param = new Params.Builder(paint).build();
393 
394         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(0, 0), 0.0f);
395         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(1, 1), 0.0f);
396         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(2, 2), 0.0f);
397         assertEquals(0.0f, PrecomputedText.create(ssb, param).getWidth(3, 3), 0.0f);
398 
399         assertEquals(1.0f, PrecomputedText.create(ssb, param).getWidth(0, 1), 0.0f);
400         assertEquals(25.0f, PrecomputedText.create(ssb, param).getWidth(1, 2), 0.0f);
401         assertEquals(5.0f, PrecomputedText.create(ssb, param).getWidth(2, 3), 0.0f);
402 
403         assertEquals(26.0f, PrecomputedText.create(ssb, param).getWidth(0, 2), 0.0f);
404         assertEquals(30.0f, PrecomputedText.create(ssb, param).getWidth(1, 3), 0.0f);
405         assertEquals(31.0f, PrecomputedText.create(ssb, param).getWidth(0, 3), 0.0f);
406     }
407 
408     @Test(expected = IllegalArgumentException.class)
testGetWidth_negative_start_offset()409     public void testGetWidth_negative_start_offset() {
410         final Params param = new Params.Builder(PAINT).build();
411         PrecomputedText.create("a", param).getWidth(-1, 0);
412     }
413 
414     @Test(expected = IllegalArgumentException.class)
testGetWidth_negative_end_offset()415     public void testGetWidth_negative_end_offset() {
416         final Params param = new Params.Builder(PAINT).build();
417         PrecomputedText.create("a", param).getWidth(0, -1);
418     }
419 
420     @Test(expected = IllegalArgumentException.class)
testGetWidth_index_out_of_bounds_start_offset()421     public void testGetWidth_index_out_of_bounds_start_offset() {
422         final Params param = new Params.Builder(PAINT).build();
423         PrecomputedText.create("a", param).getWidth(2, 2);
424     }
425 
426     @Test(expected = IllegalArgumentException.class)
testGetWidth_index_out_of_bounds_end_offset()427     public void testGetWidth_index_out_of_bounds_end_offset() {
428         final Params param = new Params.Builder(PAINT).build();
429         PrecomputedText.create("a", param).getWidth(0, 2);
430     }
431 
432     @Test(expected = IllegalArgumentException.class)
testGetWidth_reverse_offset()433     public void testGetWidth_reverse_offset() {
434         final Params param = new Params.Builder(PAINT).build();
435         PrecomputedText.create("a", param).getWidth(1, 0);
436     }
437 
438     @Test(expected = IllegalArgumentException.class)
testGetWidth_across_paragraph_boundary()439     public void testGetWidth_across_paragraph_boundary() {
440         final Params param = new Params.Builder(PAINT).build();
441         PrecomputedText.create("a\nb", param).getWidth(0, 3);
442     }
443 
444     @Test
testGetBounds()445     public void testGetBounds() {
446         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
447 
448         // The test font has following coverage and width.
449         // U+0020: 10em
450         // U+002E (.): 10em
451         // U+0043 (C): 100em
452         // U+0049 (I): 1em
453         // U+004C (L): 50em
454         // U+0056 (V): 5em
455         // U+0058 (X): 10em
456         // U+005F (_): 0em
457         // U+FFFD (invalid surrogate will be replaced to this): 7em
458         // U+10331 (\uD800\uDF31): 10em
459         final Typeface tf = new Typeface.Builder(context.getAssets(),
460                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
461         final TextPaint paint = new TextPaint();
462         paint.setTypeface(tf);
463         paint.setTextSize(1);  // Make 1em = 1px
464 
465         final Params param = new Params.Builder(paint).build();
466         final Rect rect = new Rect();
467 
468         rect.set(0, 0, 0, 0);
469         PrecomputedText.create("", param).getBounds(0, 0, rect);
470         assertEquals(new Rect(0, 0, 0, 0), rect);
471 
472         rect.set(0, 0, 0, 0);
473         PrecomputedText.create("I", param).getBounds(0, 1, rect);
474         assertEquals(new Rect(0, -1, 1, 0), rect);
475 
476         rect.set(0, 0, 0, 0);
477         PrecomputedText.create("I", param).getBounds(1, 1, rect);
478         assertEquals(new Rect(0, 0, 0, 0), rect);
479 
480         rect.set(0, 0, 0, 0);
481         PrecomputedText.create("IV", param).getBounds(0, 0, rect);
482         assertEquals(new Rect(0, 0, 0, 0), rect);
483 
484         rect.set(0, 0, 0, 0);
485         PrecomputedText.create("IV", param).getBounds(0, 0, rect);
486         assertEquals(new Rect(0, 0, 0, 0), rect);
487 
488         rect.set(0, 0, 0, 0);
489         PrecomputedText.create("IV", param).getBounds(1, 1, rect);
490         assertEquals(new Rect(0, 0, 0, 0), rect);
491 
492         rect.set(0, 0, 0, 0);
493         PrecomputedText.create("IV", param).getBounds(2, 2, rect);
494         assertEquals(new Rect(0, 0, 0, 0), rect);
495 
496         rect.set(0, 0, 0, 0);
497         PrecomputedText.create("IV", param).getBounds(0, 1, rect);
498         assertEquals(new Rect(0, -1, 1, 0), rect);
499 
500         rect.set(0, 0, 0, 0);
501         PrecomputedText.create("IV", param).getBounds(1, 2, rect);
502         assertEquals(new Rect(0, -5, 5, 0), rect);
503 
504         rect.set(0, 0, 0, 0);
505         PrecomputedText.create("IV", param).getBounds(0, 2, rect);
506         assertEquals(new Rect(0, -5, 6, 0), rect);
507     }
508 
509     @Test
testGetBounds_multiStyle()510     public void testGetBounds_multiStyle() {
511         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
512         final SpannableStringBuilder ssb = new SpannableStringBuilder("II");
513         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 1 /* text size */,
514                 null /* color */, null /* link color */), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
515         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
516                 null /* color */, null /* link color */), 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
517 
518         final Typeface tf = new Typeface.Builder(context.getAssets(),
519                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
520         final TextPaint paint = new TextPaint();
521         paint.setTypeface(tf);
522         paint.setTextSize(1);  // Make 1em = 1px
523 
524         final Params param = new Params.Builder(paint).build();
525         final Rect rect = new Rect();
526 
527         rect.set(0, 0, 0, 0);
528         PrecomputedText.create(ssb, param).getBounds(0, 0, rect);
529         assertEquals(new Rect(0, 0, 0, 0), rect);
530 
531         rect.set(0, 0, 0, 0);
532         PrecomputedText.create(ssb, param).getBounds(0, 1, rect);
533         assertEquals(new Rect(0, -1, 1, 0), rect);
534 
535         rect.set(0, 0, 0, 0);
536         PrecomputedText.create(ssb, param).getBounds(1, 2, rect);
537         assertEquals(new Rect(0, -5, 5, 0), rect);
538 
539         rect.set(0, 0, 0, 0);
540         PrecomputedText.create(ssb, param).getBounds(0, 2, rect);
541         assertEquals(new Rect(0, -5, 6, 0), rect);
542     }
543 
544     @Test
testGetBounds_multiStyle2()545     public void testGetBounds_multiStyle2() {
546         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
547         final SpannableStringBuilder ssb = new SpannableStringBuilder("IVI");
548         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 1 /* text size */,
549                 null /* color */, null /* link color */), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
550         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
551                 null /* color */, null /* link color */), 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
552         ssb.setSpan(new TextAppearanceSpan(null /* family */, Typeface.NORMAL, 5 /* text size */,
553                 null /* color */, null /* link color */), 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
554 
555         final Typeface tf = new Typeface.Builder(context.getAssets(),
556                 "fonts/StaticLayoutLineBreakingTestFont.ttf").build();
557         final TextPaint paint = new TextPaint();
558         paint.setTypeface(tf);
559         paint.setTextSize(1);  // Make 1em = 1px
560 
561         final Params param = new Params.Builder(paint).build();
562         final Rect rect = new Rect();
563 
564         rect.set(0, 0, 0, 0);
565         PrecomputedText.create(ssb, param).getBounds(0, 0, rect);
566         assertEquals(new Rect(0, 0, 0, 0), rect);
567 
568         rect.set(0, 0, 0, 0);
569         PrecomputedText.create(ssb, param).getBounds(0, 1, rect);
570         assertEquals(new Rect(0, -1, 1, 0), rect);
571 
572         rect.set(0, 0, 0, 0);
573         PrecomputedText.create(ssb, param).getBounds(1, 2, rect);
574         assertEquals(new Rect(0, -25, 25, 0), rect);
575 
576         rect.set(0, 0, 0, 0);
577         PrecomputedText.create(ssb, param).getBounds(2, 3, rect);
578         assertEquals(new Rect(0, -5, 5, 0), rect);
579 
580         rect.set(0, 0, 0, 0);
581         PrecomputedText.create(ssb, param).getBounds(0, 2, rect);
582         assertEquals(new Rect(0, -25, 26, 0), rect);
583 
584         rect.set(0, 0, 0, 0);
585         PrecomputedText.create(ssb, param).getBounds(1, 3, rect);
586         assertEquals(new Rect(0, -25, 30, 0), rect);
587 
588         rect.set(0, 0, 0, 0);
589         PrecomputedText.create(ssb, param).getBounds(0, 3, rect);
590         assertEquals(new Rect(0, -25, 31, 0), rect);
591     }
592 
593     @Test(expected = IllegalArgumentException.class)
testGetBounds_negative_start_offset()594     public void testGetBounds_negative_start_offset() {
595         final Rect rect = new Rect();
596         final Params param = new Params.Builder(PAINT).build();
597         PrecomputedText.create("a", param).getBounds(-1, 0, rect);
598     }
599 
600     @Test(expected = IllegalArgumentException.class)
testGetBounds_negative_end_offset()601     public void testGetBounds_negative_end_offset() {
602         final Rect rect = new Rect();
603         final Params param = new Params.Builder(PAINT).build();
604         PrecomputedText.create("a", param).getBounds(0, -1, rect);
605     }
606 
607     @Test(expected = IllegalArgumentException.class)
testGetBounds_index_out_of_bounds_start_offset()608     public void testGetBounds_index_out_of_bounds_start_offset() {
609         final Rect rect = new Rect();
610         final Params param = new Params.Builder(PAINT).build();
611         PrecomputedText.create("a", param).getBounds(2, 2, rect);
612     }
613 
614     @Test(expected = IllegalArgumentException.class)
testGetBounds_index_out_of_bounds_end_offset()615     public void testGetBounds_index_out_of_bounds_end_offset() {
616         final Rect rect = new Rect();
617         final Params param = new Params.Builder(PAINT).build();
618         PrecomputedText.create("a", param).getBounds(0, 2, rect);
619     }
620 
621     @Test(expected = IllegalArgumentException.class)
testGetBounds_reverse_offset()622     public void testGetBounds_reverse_offset() {
623         final Rect rect = new Rect();
624         final Params param = new Params.Builder(PAINT).build();
625         PrecomputedText.create("a", param).getBounds(1, 0, rect);
626     }
627 
628     @Test(expected = NullPointerException.class)
testGetBounds_null_rect()629     public void testGetBounds_null_rect() {
630         final Params param = new Params.Builder(PAINT).build();
631         PrecomputedText.create("a", param).getBounds(0, 1, null);
632     }
633 
634     @Test(expected = IllegalArgumentException.class)
testGetBounds_across_paragraph_boundary()635     public void testGetBounds_across_paragraph_boundary() {
636         final Rect rect = new Rect();
637         final Params param = new Params.Builder(PAINT).build();
638         PrecomputedText.create("a\nb", param).getBounds(0, 3, rect);
639     }
640 
drawToBitmap(@onNull CharSequence cs, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @IntRange(from = 0) int ctxStart, @IntRange(from = 0) int ctxEnd, @NonNull TextPaint paint)641     private static Bitmap drawToBitmap(@NonNull CharSequence cs,
642             @IntRange(from = 0) int start, @IntRange(from = 0) int end,
643             @IntRange(from = 0) int ctxStart, @IntRange(from = 0) int ctxEnd,
644             @NonNull TextPaint paint) {
645 
646         Rect rect = new Rect();
647         paint.getTextBounds(cs.toString(), start, end, rect);
648         final Bitmap bmp = Bitmap.createBitmap(rect.width(),
649                 rect.height(), Bitmap.Config.ARGB_8888);
650         final Canvas c = new Canvas(bmp);
651         c.save();
652         c.translate(0, 0);
653         c.drawTextRun(cs, start, end, ctxStart, ctxEnd, 0, 0, false /* isRtl */, paint);
654         c.restore();
655         return bmp;
656     }
657 
assertSameOutput(@onNull CharSequence cs, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @IntRange(from = 0) int ctxStart, @IntRange(from = 0) int ctxEnd, @NonNull TextPaint paint)658     private static void assertSameOutput(@NonNull CharSequence cs,
659             @IntRange(from = 0) int start, @IntRange(from = 0) int end,
660             @IntRange(from = 0) int ctxStart, @IntRange(from = 0) int ctxEnd,
661             @NonNull TextPaint paint) {
662         final Params params = new Params.Builder(paint).build();
663         final PrecomputedText pt = PrecomputedText.create(cs, params);
664 
665         final Params rtlParams = new Params.Builder(paint)
666                 .setTextDirection(TextDirectionHeuristics.RTL).build();
667         final PrecomputedText rtlPt = PrecomputedText.create(cs, rtlParams);
668         // FIRSTSTRONG_LTR is the default direction.
669         final PrecomputedText ptFromRtl = PrecomputedText.create(rtlPt,
670                 new Params.Builder(params).setTextDirection(
671                         TextDirectionHeuristics.FIRSTSTRONG_LTR).build());
672 
673         final Bitmap originalDrawOutput = drawToBitmap(cs, start, end, ctxStart, ctxEnd, paint);
674         final Bitmap precomputedDrawOutput = drawToBitmap(pt, start, end, ctxStart, ctxEnd, paint);
675         final Bitmap precomputedFromDifferentDirectionDrawOutput =
676                 drawToBitmap(pt, start, end, ctxStart, ctxEnd, paint);
677         assertTrue(originalDrawOutput.sameAs(precomputedDrawOutput));
678         assertTrue(originalDrawOutput.sameAs(precomputedFromDifferentDirectionDrawOutput));
679     }
680 
681     @Test
testDrawText()682     public void testDrawText() {
683         final TextPaint paint = new TextPaint();
684         paint.setTextSize(32.0f);
685 
686         final SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World");
687         assertSameOutput(ssb, 0, ssb.length(), 0, ssb.length(), paint);
688         assertSameOutput(ssb, 3, ssb.length() - 3, 0, ssb.length(), paint);
689         assertSameOutput(ssb, 5, ssb.length() - 5, 2, ssb.length() - 2, paint);
690     }
691 
692     @Test
testDrawText_MultiStyle()693     public void testDrawText_MultiStyle() {
694         final TextPaint paint = new TextPaint();
695         paint.setTextSize(32.0f);
696 
697         final SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World");
698         ssb.setSpan(new TypefaceSpan("serif"), 0, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
699         assertSameOutput(ssb, 0, ssb.length(), 0, ssb.length(), paint);
700         assertSameOutput(ssb, 3, ssb.length() - 3, 0, ssb.length(), paint);
701         assertSameOutput(ssb, 5, ssb.length() - 5, 2, ssb.length() - 2, paint);
702     }
703 
704     @Test
testDrawText_MultiParagraph()705     public void testDrawText_MultiParagraph() {
706         final TextPaint paint = new TextPaint();
707         paint.setTextSize(32.0f);
708 
709         final SpannableStringBuilder ssb = new SpannableStringBuilder(
710                 "Hello, World\nHello, Android");
711 
712         // The first line
713         final int firstLineLen = "Hello, World\n".length();
714         assertSameOutput(ssb, 0, firstLineLen, 0, firstLineLen, paint);
715         assertSameOutput(ssb, 3, firstLineLen - 3, 0, firstLineLen, paint);
716         assertSameOutput(ssb, 3, firstLineLen - 3, 2, firstLineLen - 2, paint);
717 
718         // The second line.
719         assertSameOutput(ssb, firstLineLen, ssb.length(), firstLineLen, ssb.length(), paint);
720         assertSameOutput(ssb, firstLineLen + 3, ssb.length() - 3,
721                 firstLineLen, ssb.length(), paint);
722         assertSameOutput(ssb, firstLineLen + 5, ssb.length() - 5,
723                 firstLineLen + 2, ssb.length() - 2, paint);
724 
725         // Across the paragraph
726         assertSameOutput(ssb, 0, ssb.length(), 0, ssb.length(), paint);
727         assertSameOutput(ssb, 3, firstLineLen - 3, 0, ssb.length(), paint);
728         assertSameOutput(ssb, 3, firstLineLen - 3, 2, ssb.length() - 2, paint);
729     }
730 }
731