Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2012 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 org.junit.Assert.assertEquals;
     20 
     21 import android.content.Context;;
     22 import android.graphics.Typeface;
     23 import android.support.test.InstrumentationRegistry;
     24 import android.support.test.filters.SmallTest;
     25 import android.support.test.runner.AndroidJUnit4;
     26 import android.text.Layout;
     27 import android.text.Layout.Alignment;
     28 import android.text.SpannableStringBuilder;
     29 import android.text.Spanned;
     30 import android.text.StaticLayout;
     31 import android.text.TextDirectionHeuristics;
     32 import android.text.TextPaint;
     33 import android.text.style.MetricAffectingSpan;
     34 import android.util.Log;
     35 
     36 import org.junit.Test;
     37 import org.junit.runner.RunWith;
     38 
     39 @SmallTest
     40 @RunWith(AndroidJUnit4.class)
     41 public class StaticLayoutLineBreakingTest {
     42     // Span test are currently not supported because text measurement uses the MeasuredText
     43     // internal mWorkPaint instead of the provided MockTestPaint.
     44     private static final boolean SPAN_TESTS_SUPPORTED = false;
     45     private static final boolean DEBUG = false;
     46 
     47     private static final float SPACE_MULTI = 1.0f;
     48     private static final float SPACE_ADD = 0.0f;
     49     private static final int WIDTH = 100;
     50     private static final Alignment ALIGN = Alignment.ALIGN_NORMAL;
     51 
     52     private static final char SURR_FIRST = '\uD800';
     53     private static final char SURR_SECOND = '\uDF31';
     54 
     55     private static final int[] NO_BREAK = new int[] {};
     56 
     57     private static final TextPaint sTextPaint = new TextPaint();
     58 
     59     static {
     60         // The test font has following coverage and width.
     61         // U+0020: 10em
     62         // U+002E (.): 10em
     63         // U+0043 (C): 100em
     64         // U+0049 (I): 1em
     65         // U+004C (L): 50em
     66         // U+0056 (V): 5em
     67         // U+0058 (X): 10em
     68         // U+005F (_): 0em
     69         // U+FFFD (invalid surrogate will be replaced to this): 7em
     70         // U+10331 (\uD800\uDF31): 10em
     71         Context context = InstrumentationRegistry.getTargetContext();
     72         sTextPaint.setTypeface(Typeface.createFromAsset(context.getAssets(),
     73                   "fonts/StaticLayoutLineBreakingTestFont.ttf"));
     74         sTextPaint.setTextSize(1.0f);  // Make 1em == 1px.
     75     }
     76 
     77     private static StaticLayout getStaticLayout(CharSequence source, int width,
     78             int breakStrategy) {
     79         return StaticLayout.Builder.obtain(source, 0, source.length(), sTextPaint, width)
     80                 .setAlignment(ALIGN)
     81                 .setLineSpacing(SPACE_ADD, SPACE_MULTI)
     82                 .setIncludePad(false)
     83                 .setBreakStrategy(breakStrategy)
     84                 .build();
     85     }
     86 
     87     private static int[] getBreaks(CharSequence source) {
     88         return getBreaks(source, WIDTH, Layout.BREAK_STRATEGY_SIMPLE);
     89     }
     90 
     91     private static int[] getBreaks(CharSequence source, int width, int breakStrategy) {
     92         final StaticLayout staticLayout = getStaticLayout(source, width, breakStrategy);
     93 
     94         final int[] breaks = new int[staticLayout.getLineCount() - 1];
     95         for (int line = 0; line < breaks.length; line++) {
     96             breaks[line] = staticLayout.getLineEnd(line);
     97         }
     98         return breaks;
     99     }
    100 
    101     private static void debugLayout(CharSequence source, StaticLayout staticLayout) {
    102         if (DEBUG) {
    103             int count = staticLayout.getLineCount();
    104             Log.i("SLLBTest", "\"" + source.toString() + "\": "
    105                     + count + " lines");
    106             for (int line = 0; line < count; line++) {
    107                 int lineStart = staticLayout.getLineStart(line);
    108                 int lineEnd = staticLayout.getLineEnd(line);
    109                 Log.i("SLLBTest", "Line " + line + " [" + lineStart + ".."
    110                         + lineEnd + "]\t" + source.subSequence(lineStart, lineEnd));
    111             }
    112         }
    113     }
    114 
    115     private static void layout(CharSequence source, int[] breaks) {
    116         layout(source, breaks, WIDTH);
    117     }
    118 
    119     private static void layout(CharSequence source, int[] breaks, int width) {
    120         final int[] breakStrategies = {Layout.BREAK_STRATEGY_SIMPLE,
    121                 Layout.BREAK_STRATEGY_HIGH_QUALITY};
    122         for (int breakStrategy : breakStrategies) {
    123             final StaticLayout staticLayout = getStaticLayout(source, width, breakStrategy);
    124 
    125             debugLayout(source, staticLayout);
    126 
    127             final int lineCount = breaks.length + 1;
    128             assertEquals("Number of lines", lineCount, staticLayout.getLineCount());
    129 
    130             for (int line = 0; line < lineCount; line++) {
    131                 final int lineStart = staticLayout.getLineStart(line);
    132                 final int lineEnd = staticLayout.getLineEnd(line);
    133 
    134                 if (line == 0) {
    135                     assertEquals("Line start for first line", 0, lineStart);
    136                 } else {
    137                     assertEquals("Line start for line " + line, breaks[line - 1], lineStart);
    138                 }
    139 
    140                 if (line == lineCount - 1) {
    141                     assertEquals("Line end for last line", source.length(), lineEnd);
    142                 } else {
    143                     assertEquals("Line end for line " + line, breaks[line], lineEnd);
    144                 }
    145             }
    146         }
    147     }
    148 
    149     private static void layoutMaxLines(CharSequence source, int[] breaks, int maxLines) {
    150         final StaticLayout staticLayout = StaticLayout.Builder
    151                 .obtain(source, 0, source.length(), sTextPaint, WIDTH)
    152                 .setAlignment(ALIGN)
    153                 .setTextDirection(TextDirectionHeuristics.LTR)
    154                 .setLineSpacing(SPACE_ADD, SPACE_MULTI)
    155                 .setIncludePad(false)
    156                 .setMaxLines(maxLines)
    157                 .build();
    158 
    159         debugLayout(source, staticLayout);
    160 
    161         final int lineCount = staticLayout.getLineCount();
    162 
    163         for (int line = 0; line < lineCount; line++) {
    164             int lineStart = staticLayout.getLineStart(line);
    165             int lineEnd = staticLayout.getLineEnd(line);
    166 
    167             if (line == 0) {
    168                 assertEquals("Line start for first line", 0, lineStart);
    169             } else {
    170                 assertEquals("Line start for line " + line, breaks[line - 1], lineStart);
    171             }
    172 
    173             if (line == lineCount - 1 && line != breaks.length - 1) {
    174                 assertEquals("Line end for last line", source.length(), lineEnd);
    175             } else {
    176                 assertEquals("Line end for line " + line, breaks[line], lineEnd);
    177             }
    178         }
    179     }
    180 
    181     private static final int MAX_SPAN_COUNT = 10;
    182     private static final int[] sSpanStarts = new int[MAX_SPAN_COUNT];
    183     private static final int[] sSpanEnds = new int[MAX_SPAN_COUNT];
    184 
    185     private static MetricAffectingSpan getMetricAffectingSpan() {
    186         return new MetricAffectingSpan() {
    187             @Override
    188             public void updateDrawState(TextPaint tp) { /* empty */ }
    189 
    190             @Override
    191             public void updateMeasureState(TextPaint p) { /* empty */ }
    192         };
    193     }
    194 
    195     /**
    196      * Replaces the "<...>" blocks by spans, assuming non overlapping, correctly defined spans
    197      * @param text
    198      * @return A CharSequence with '<' '>' replaced by MetricAffectingSpan
    199      */
    200     private static CharSequence spanify(String text) {
    201         int startIndex = text.indexOf('<');
    202         if (startIndex < 0) return text;
    203 
    204         int spanCount = 0;
    205         do {
    206             int endIndex = text.indexOf('>');
    207             if (endIndex < 0) throw new IllegalArgumentException("Unbalanced span markers");
    208 
    209             text = text.substring(0, startIndex) + text.substring(startIndex + 1, endIndex)
    210                     + text.substring(endIndex + 1);
    211 
    212             sSpanStarts[spanCount] = startIndex;
    213             sSpanEnds[spanCount] = endIndex - 2;
    214             spanCount++;
    215 
    216             startIndex = text.indexOf('<');
    217         } while (startIndex >= 0);
    218 
    219         SpannableStringBuilder result = new SpannableStringBuilder(text);
    220         for (int i = 0; i < spanCount; i++) {
    221             result.setSpan(getMetricAffectingSpan(), sSpanStarts[i], sSpanEnds[i],
    222                     Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    223         }
    224         return result;
    225     }
    226 
    227     @Test
    228     public void testNoLineBreak() {
    229         // Width lower than WIDTH
    230         layout("", NO_BREAK);
    231         layout("I", NO_BREAK);
    232         layout("V", NO_BREAK);
    233         layout("X", NO_BREAK);
    234         layout("L", NO_BREAK);
    235         layout("I VILI", NO_BREAK);
    236         layout("XXXX", NO_BREAK);
    237         layout("LXXXX", NO_BREAK);
    238 
    239         // Width equal to WIDTH
    240         layout("C", NO_BREAK);
    241         layout("LL", NO_BREAK);
    242         layout("L XXXX", NO_BREAK);
    243         layout("XXXXXXXXXX", NO_BREAK);
    244         layout("XXX XXXXXX", NO_BREAK);
    245         layout("XXX XXXX X", NO_BREAK);
    246         layout("XXX XXXXX ", NO_BREAK);
    247         layout(" XXXXXXXX ", NO_BREAK);
    248         layout("  XX  XXX ", NO_BREAK);
    249         //      0123456789
    250 
    251         // Width greater than WIDTH, but no break
    252         layout("  XX  XXX  ", NO_BREAK);
    253         layout("XX XXX XXX ", NO_BREAK);
    254         layout("XX XXX XXX     ", NO_BREAK);
    255         layout("XXXXXXXXXX     ", NO_BREAK);
    256         //      01234567890
    257     }
    258 
    259     @Test
    260     public void testOneLineBreak() {
    261         //      01234567890
    262         layout("XX XXX XXXX", new int[] {7});
    263         layout("XX XXXX XXX", new int[] {8});
    264         layout("XX XXXXX XX", new int[] {9});
    265         layout("XX XXXXXX X", new int[] {10});
    266         //      01234567890
    267         layout("XXXXXXXXXXX", new int[] {10});
    268         layout("XXXXXXXXX X", new int[] {10});
    269         layout("XXXXXXXX XX", new int[] {9});
    270         layout("XXXXXXX XXX", new int[] {8});
    271         layout("XXXXXX XXXX", new int[] {7});
    272         //      01234567890
    273         layout("LL LL", new int[] {3});
    274         layout("LLLL", new int[] {2});
    275         layout("C C", new int[] {2});
    276         layout("CC", new int[] {1});
    277     }
    278 
    279     @Test
    280     public void testSpaceAtBreak() {
    281         //      0123456789012
    282         layout("XXXX XXXXX X", new int[] {11});
    283         layout("XXXXXXXXXX X", new int[] {11});
    284         layout("XXXXXXXXXV X", new int[] {11});
    285         layout("C X", new int[] {2});
    286     }
    287 
    288     @Test
    289     public void testMultipleSpacesAtBreak() {
    290         //      0123456789012
    291         layout("LXX XXXX", new int[] {4});
    292         layout("LXX  XXXX", new int[] {5});
    293         layout("LXX   XXXX", new int[] {6});
    294         layout("LXX    XXXX", new int[] {7});
    295         layout("LXX     XXXX", new int[] {8});
    296     }
    297 
    298     @Test
    299     public void testZeroWidthCharacters() {
    300         //      0123456789012345678901234
    301         layout("X_X_X_X_X_X_X_X_X_X", NO_BREAK);
    302         layout("___X_X_X_X_X_X_X_X_X_X___", NO_BREAK);
    303         layout("C_X", new int[] {2});
    304         layout("C__X", new int[] {3});
    305     }
    306 
    307     /**
    308      * Note that when the text has spans, StaticLayout does not use the provided TextPaint to
    309      * measure text runs anymore. This is probably a bug.
    310      * To be able to use the fake sTextPaint and make this test pass, use mPaint instead of
    311      * mWorkPaint in MeasuredText#addStyleRun
    312      */
    313     @Test
    314     public void testWithSpans() {
    315         if (!SPAN_TESTS_SUPPORTED) return;
    316 
    317         layout(spanify("<012 456 89>"), NO_BREAK);
    318         layout(spanify("012 <456> 89"), NO_BREAK);
    319         layout(spanify("<012> <456>< 89>"), NO_BREAK);
    320         layout(spanify("<012> <456> <89>"), NO_BREAK);
    321 
    322         layout(spanify("<012> <456> <89>012"), new int[] {8});
    323         layout(spanify("<012> <456> 89<012>"), new int[] {8});
    324         layout(spanify("<012> <456> <89><012>"), new int[] {8});
    325         layout(spanify("<012> <456> 89 <123>"), new int[] {11});
    326         layout(spanify("<012> <456> 89< 123>"), new int[] {11});
    327         layout(spanify("<012> <456> <89> <123>"), new int[] {11});
    328         layout(spanify("012 456 89 <LXX> XX XX"), new int[] {11, 18});
    329     }
    330 
    331     /*
    332      * Adding a span to the string should not change the layout, since the metrics are unchanged.
    333      */
    334     @Test
    335     public void testWithOneSpan() {
    336         if (!SPAN_TESTS_SUPPORTED) return;
    337 
    338         String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012",
    339                 "012 456 89012 456 89012", "0123456789012" };
    340 
    341         MetricAffectingSpan metricAffectingSpan = getMetricAffectingSpan();
    342 
    343         for (String text : texts) {
    344             // Get the line breaks without any span
    345             int[] breaks = getBreaks(text);
    346 
    347             // Add spans on all possible offsets
    348             for (int spanStart = 0; spanStart < text.length(); spanStart++) {
    349                 for (int spanEnd = spanStart; spanEnd < text.length(); spanEnd++) {
    350                     SpannableStringBuilder ssb = new SpannableStringBuilder(text);
    351                     ssb.setSpan(metricAffectingSpan, spanStart, spanEnd,
    352                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    353                     layout(ssb, breaks);
    354                 }
    355             }
    356         }
    357     }
    358 
    359     @Test
    360     public void testWithTwoSpans() {
    361         if (!SPAN_TESTS_SUPPORTED) return;
    362 
    363         String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012",
    364                 "012 456 89012 456 89012", "0123456789012" };
    365 
    366         MetricAffectingSpan metricAffectingSpan1 = getMetricAffectingSpan();
    367         MetricAffectingSpan metricAffectingSpan2 = getMetricAffectingSpan();
    368 
    369         for (String text : texts) {
    370             // Get the line breaks without any span
    371             int[] breaks = getBreaks(text);
    372 
    373             // Add spans on all possible offsets
    374             for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) {
    375                 for (int spanEnd1 = spanStart1; spanEnd1 < text.length(); spanEnd1++) {
    376                     SpannableStringBuilder ssb = new SpannableStringBuilder(text);
    377                     ssb.setSpan(metricAffectingSpan1, spanStart1, spanEnd1,
    378                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    379 
    380                     for (int spanStart2 = 0; spanStart2 < text.length(); spanStart2++) {
    381                         for (int spanEnd2 = spanStart2; spanEnd2 < text.length(); spanEnd2++) {
    382                             ssb.setSpan(metricAffectingSpan2, spanStart2, spanEnd2,
    383                                     Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    384                             layout(ssb, breaks);
    385                         }
    386                     }
    387                 }
    388             }
    389         }
    390     }
    391 
    392     public static String replace(String string, char c, char r) {
    393         return string.replaceAll(String.valueOf(c), String.valueOf(r));
    394     }
    395 
    396     @Test
    397     public void testWithSurrogate() {
    398         layout("LX" + SURR_FIRST + SURR_SECOND, NO_BREAK);
    399         layout("LXXXX" + SURR_FIRST + SURR_SECOND, NO_BREAK);
    400         // LXXXXI (91) + SURR_FIRST + SURR_SECOND (10). Do not break in the middle point of
    401         // surrogatge pair.
    402         layout("LXXXXI" + SURR_FIRST + SURR_SECOND, new int[] {6});
    403 
    404         // LXXXXI (91) + SURR_SECOND (replaced with REPLACEMENT CHARACTER. width is 7px) fits.
    405         // Break just after invalid trailing surrogate.
    406         layout("LXXXXI" + SURR_SECOND + SURR_FIRST, new int[] {7});
    407 
    408         layout("C" + SURR_FIRST + SURR_SECOND, new int[] {1});
    409     }
    410 
    411     @Test
    412     public void testNarrowWidth() {
    413         int[] widths = new int[] { 0, 4, 10 };
    414         String[] texts = new String[] { "", "X", " ", "XX", " X", "XXX" };
    415 
    416         for (String text: texts) {
    417             // 15 is such that only one character will fit
    418             int[] breaks = getBreaks(text, 15, Layout.BREAK_STRATEGY_SIMPLE);
    419 
    420             // Width under 15 should all lead to the same line break
    421             for (int width: widths) {
    422                 layout(text, breaks, width);
    423             }
    424         }
    425     }
    426 
    427     @Test
    428     public void testNarrowWidthZeroWidth() {
    429         int[] widths = new int[] { 1, 4 };
    430         for (int width: widths) {
    431             layout("X.", new int[] {1}, width);
    432             layout("X__", NO_BREAK, width);
    433             layout("X__X", new int[] {3}, width);
    434             layout("X__X_", new int[] {3}, width);
    435 
    436             layout("_", NO_BREAK, width);
    437             layout("__", NO_BREAK, width);
    438 
    439             // TODO: The line breaking algorithms break the line too frequently in the presence of
    440             // zero-width characters. The following cases document how line-breaking should behave
    441             // in some cases, where the current implementation does not seem reasonable. (Breaking
    442             // between a zero-width character that start the line and a character with positive
    443             // width does not make sense.) Line-breaking should be fixed so that all the following
    444             // tests end up on one line, with no breaks.
    445             // layout("_X", NO_BREAK, width);
    446             // layout("_X_", NO_BREAK, width);
    447             // layout("__X__", NO_BREAK, width);
    448         }
    449     }
    450 
    451     @Test
    452     public void testMaxLines() {
    453         layoutMaxLines("C", NO_BREAK, 1);
    454         layoutMaxLines("C C", new int[] {2}, 1);
    455         layoutMaxLines("C C", new int[] {2}, 2);
    456         layoutMaxLines("CC", new int[] {1}, 1);
    457         layoutMaxLines("CC", new int[] {1}, 2);
    458     }
    459 }
    460