Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package android.text;
     18 
     19 import android.graphics.Paint.FontMetricsInt;
     20 import android.test.suitebuilder.annotation.SmallTest;
     21 import android.text.Layout.Alignment;
     22 import static android.text.Layout.Alignment.*;
     23 import android.text.TextPaint;
     24 import android.text.method.EditorState;
     25 import android.util.Log;
     26 
     27 import junit.framework.TestCase;
     28 
     29 /**
     30  * Tests StaticLayout vertical metrics behavior.
     31  *
     32  * Requires disabling access checks in the vm since this calls package-private
     33  * APIs.
     34  *
     35  * @Suppress
     36  */
     37 public class StaticLayoutTest extends TestCase {
     38     private static final int DEFAULT_OUTER_WIDTH = 150;
     39     private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
     40     private static final float SPACE_MULTI = 1.0f;
     41     private static final float SPACE_ADD = 0.0f;
     42 
     43     /**
     44      * Basic test showing expected behavior and relationship between font
     45      * metrics and line metrics.
     46      */
     47     //@SmallTest
     48     public void testGetters1() {
     49         LayoutBuilder b = builder();
     50         FontMetricsInt fmi = b.paint.getFontMetricsInt();
     51 
     52         // check default paint
     53         Log.i("TG1:paint", fmi.toString());
     54 
     55         Layout l = b.build();
     56         assertVertMetrics(l, 0, 0,
     57                 fmi.ascent, fmi.descent);
     58 
     59         // other quick metrics
     60         assertEquals(0, l.getLineStart(0));
     61         assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0));
     62         assertEquals(false, l.getLineContainsTab(0));
     63         assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0));
     64         assertEquals(0, l.getEllipsisCount(0));
     65         assertEquals(0, l.getEllipsisStart(0));
     66         assertEquals(b.width, l.getEllipsizedWidth());
     67     }
     68 
     69     /**
     70      * Basic test showing effect of includePad = true with 1 line.
     71      * Top and bottom padding are affected, as is the line descent and height.
     72      */
     73     //@SmallTest
     74     public void testGetters2() {
     75         LayoutBuilder b = builder()
     76             .setIncludePad(true);
     77         FontMetricsInt fmi = b.paint.getFontMetricsInt();
     78 
     79         Layout l = b.build();
     80         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
     81                 fmi.top, fmi.bottom);
     82     }
     83 
     84     /**
     85      * Basic test showing effect of includePad = true wrapping to 2 lines.
     86      * Ascent of top line and descent of bottom line are affected.
     87      */
     88     //@SmallTest
     89     public void testGetters3() {
     90         LayoutBuilder b = builder()
     91             .setIncludePad(true)
     92             .setWidth(50);
     93         FontMetricsInt fmi = b.paint.getFontMetricsInt();
     94 
     95         Layout l =  b.build();
     96         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
     97             fmi.top, fmi.descent,
     98             fmi.ascent, fmi.bottom);
     99     }
    100 
    101     /**
    102      * Basic test showing effect of includePad = true wrapping to 3 lines.
    103      * First line ascent is top, bottom line descent is bottom.
    104      */
    105     //@SmallTest
    106     public void testGetters4() {
    107         LayoutBuilder b = builder()
    108             .setText("This is a longer test")
    109             .setIncludePad(true)
    110             .setWidth(50);
    111         FontMetricsInt fmi = b.paint.getFontMetricsInt();
    112 
    113         Layout l = b.build();
    114         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
    115                 fmi.top, fmi.descent,
    116                 fmi.ascent, fmi.descent,
    117                 fmi.ascent, fmi.bottom);
    118     }
    119 
    120     /**
    121      * Basic test showing effect of includePad = true wrapping to 3 lines and
    122      * large text. See effect of leading. Currently, we don't expect there to
    123      * even be non-zero leading.
    124      */
    125     //@SmallTest
    126     public void testGetters5() {
    127         LayoutBuilder b = builder()
    128             .setText("This is a longer test")
    129             .setIncludePad(true)
    130             .setWidth(150);
    131         b.paint.setTextSize(36);
    132         FontMetricsInt fmi = b.paint.getFontMetricsInt();
    133 
    134         if (fmi.leading == 0) { // nothing to test
    135             Log.i("TG5", "leading is 0, skipping test");
    136             return;
    137         }
    138 
    139         // So far, leading is not used, so this is the same as TG4.  If we start
    140         // using leading, this will fail.
    141         Layout l = b.build();
    142         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
    143                 fmi.top, fmi.descent,
    144                 fmi.ascent, fmi.descent,
    145                 fmi.ascent, fmi.bottom);
    146     }
    147 
    148     /**
    149      * Basic test showing effect of includePad = true, spacingAdd = 2, wrapping
    150      * to 3 lines.
    151      */
    152     //@SmallTest
    153     public void testGetters6() {
    154         int spacingAdd = 2; // int so expressions return int
    155         LayoutBuilder b = builder()
    156             .setText("This is a longer test")
    157             .setIncludePad(true)
    158             .setWidth(50)
    159             .setSpacingAdd(spacingAdd);
    160         FontMetricsInt fmi = b.paint.getFontMetricsInt();
    161 
    162         Layout l = b.build();
    163         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
    164                 fmi.top, fmi.descent + spacingAdd,
    165                 fmi.ascent, fmi.descent + spacingAdd,
    166                 fmi.ascent, fmi.bottom + spacingAdd);
    167     }
    168 
    169     /**
    170      * Basic test showing effect of includePad = true, spacingAdd = 2,
    171      * spacingMult = 1.5, wrapping to 3 lines.
    172      */
    173     //@SmallTest
    174     public void testGetters7() {
    175         LayoutBuilder b = builder()
    176             .setText("This is a longer test")
    177             .setIncludePad(true)
    178             .setWidth(50)
    179             .setSpacingAdd(2)
    180             .setSpacingMult(1.5f);
    181         FontMetricsInt fmi = b.paint.getFontMetricsInt();
    182         Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
    183 
    184         Layout l = b.build();
    185         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
    186                 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
    187                 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
    188                 fmi.ascent, fmi.bottom + s.scale(fmi.bottom - fmi.ascent));
    189     }
    190 
    191     /**
    192      * Basic test showing effect of includePad = true, spacingAdd = 0,
    193      * spacingMult = 0.8 when wrapping to 3 lines.
    194      */
    195     //@SmallTest
    196     public void testGetters8() {
    197         LayoutBuilder b = builder()
    198             .setText("This is a longer test")
    199             .setIncludePad(true)
    200             .setWidth(50)
    201             .setSpacingAdd(2)
    202             .setSpacingMult(.8f);
    203         FontMetricsInt fmi = b.paint.getFontMetricsInt();
    204         Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
    205 
    206         Layout l = b.build();
    207         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
    208                 fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
    209                 fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
    210                 fmi.ascent, fmi.bottom + s.scale(fmi.bottom - fmi.ascent));
    211     }
    212 
    213     // ----- test utility classes and methods -----
    214 
    215     // Models the effect of the scale and add parameters.  I think the current
    216     // implementation misbehaves.
    217     private static class Scaler {
    218         private final float sMult;
    219         private final float sAdd;
    220 
    221         Scaler(float sMult, float sAdd) {
    222             this.sMult = sMult - 1;
    223             this.sAdd = sAdd;
    224         }
    225 
    226         public int scale(float height) {
    227             int altVal = (int)(height * sMult + sAdd + 0.5);
    228             int rndVal = Math.round(height * sMult + sAdd);
    229             if (altVal != rndVal) {
    230                 Log.i("Scale", "expected scale: " + rndVal +
    231                         " != returned scale: " + altVal);
    232             }
    233             return rndVal;
    234         }
    235     }
    236 
    237     /* package */ static LayoutBuilder builder() {
    238         return new LayoutBuilder();
    239     }
    240 
    241     /* package */ static class LayoutBuilder {
    242         String text = "This is a test";
    243         TextPaint paint = new TextPaint(); // default
    244         int width = 100;
    245         Alignment align = ALIGN_NORMAL;
    246         float spacingMult = 1;
    247         float spacingAdd = 0;
    248         boolean includePad = false;
    249 
    250         LayoutBuilder setText(String text) {
    251             this.text = text;
    252             return this;
    253         }
    254 
    255         LayoutBuilder setPaint(TextPaint paint) {
    256             this.paint = paint;
    257             return this;
    258         }
    259 
    260         LayoutBuilder setWidth(int width) {
    261             this.width = width;
    262             return this;
    263         }
    264 
    265         LayoutBuilder setAlignment(Alignment align) {
    266             this.align = align;
    267             return this;
    268         }
    269 
    270         LayoutBuilder setSpacingMult(float spacingMult) {
    271             this.spacingMult = spacingMult;
    272             return this;
    273         }
    274 
    275         LayoutBuilder setSpacingAdd(float spacingAdd) {
    276             this.spacingAdd = spacingAdd;
    277             return this;
    278         }
    279 
    280         LayoutBuilder setIncludePad(boolean includePad) {
    281             this.includePad = includePad;
    282             return this;
    283         }
    284 
    285        Layout build() {
    286             return  new StaticLayout(text, paint, width, align, spacingMult,
    287                 spacingAdd, includePad);
    288         }
    289     }
    290 
    291     private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) {
    292         assertTopBotPadding(l, topPad, botPad);
    293         assertLinesMetrics(l, values);
    294     }
    295 
    296     private void assertLinesMetrics(Layout l, int... values) {
    297         // sanity check
    298         if ((values.length & 0x1) != 0) {
    299             throw new IllegalArgumentException(String.valueOf(values.length));
    300         }
    301 
    302         int lines = values.length >> 1;
    303         assertEquals(lines, l.getLineCount());
    304 
    305         int t = 0;
    306         for (int i = 0, n = 0; i < lines; ++i, n += 2) {
    307             int a = values[n];
    308             int d = values[n+1];
    309             int h = -a + d;
    310             assertLineMetrics(l, i, t, a, d, h);
    311             t += h;
    312         }
    313 
    314         assertEquals(t, l.getHeight());
    315     }
    316 
    317     private void assertLineMetrics(Layout l, int line,
    318             int top, int ascent, int descent, int height) {
    319         String info = "line " + line;
    320         assertEquals(info, top, l.getLineTop(line));
    321         assertEquals(info, ascent, l.getLineAscent(line));
    322         assertEquals(info, descent, l.getLineDescent(line));
    323         assertEquals(info, height, l.getLineBottom(line) - top);
    324     }
    325 
    326     private void assertTopBotPadding(Layout l, int topPad, int botPad) {
    327         assertEquals(topPad, l.getTopPadding());
    328         assertEquals(botPad, l.getBottomPadding());
    329     }
    330 
    331     private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) {
    332         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
    333         final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
    334         final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
    335         state.mSelectionStart = state.mSelectionEnd = newOffset;
    336     }
    337 
    338     private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) {
    339         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
    340         final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
    341         final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
    342         state.mSelectionStart = state.mSelectionEnd = newOffset;
    343     }
    344 
    345     /**
    346      * Tests for keycap, variation selectors, flags are in CTS.
    347      * See {@link android.text.cts.StaticLayoutTest}.
    348      */
    349     public void testEmojiOffset() {
    350         EditorState state = new EditorState();
    351         TextPaint paint = new TextPaint();
    352 
    353         // Odd numbered regional indicator symbols.
    354         // U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL
    355         // LETTER C.
    356         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
    357         moveCursorToRightCursorableOffset(state, paint);
    358         state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
    359         moveCursorToRightCursorableOffset(state, paint);
    360         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
    361         moveCursorToRightCursorableOffset(state, paint);
    362         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
    363         moveCursorToRightCursorableOffset(state, paint);
    364         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
    365         moveCursorToLeftCursorableOffset(state, paint);
    366         state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
    367         moveCursorToLeftCursorableOffset(state, paint);
    368         state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
    369         moveCursorToLeftCursorableOffset(state, paint);
    370         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
    371         moveCursorToLeftCursorableOffset(state, paint);
    372         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
    373         moveCursorToLeftCursorableOffset(state, paint);
    374 
    375         // Zero width sequence
    376         final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468";
    377         state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
    378         moveCursorToRightCursorableOffset(state, paint);
    379         state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
    380         moveCursorToRightCursorableOffset(state, paint);
    381         state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
    382         moveCursorToRightCursorableOffset(state, paint);
    383         state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
    384         moveCursorToRightCursorableOffset(state, paint);
    385         state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
    386         moveCursorToLeftCursorableOffset(state, paint);
    387         state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
    388         moveCursorToLeftCursorableOffset(state, paint);
    389         state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
    390         moveCursorToLeftCursorableOffset(state, paint);
    391         state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
    392         moveCursorToLeftCursorableOffset(state, paint);
    393         state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
    394         moveCursorToLeftCursorableOffset(state, paint);
    395 
    396         // Emoji modifiers
    397         // U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
    398         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
    399         moveCursorToRightCursorableOffset(state, paint);
    400         state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
    401         moveCursorToRightCursorableOffset(state, paint);
    402         state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
    403         moveCursorToRightCursorableOffset(state, paint);
    404         state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
    405         moveCursorToRightCursorableOffset(state, paint);
    406         state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
    407         moveCursorToLeftCursorableOffset(state, paint);
    408         state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
    409         moveCursorToLeftCursorableOffset(state, paint);
    410         state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
    411         moveCursorToLeftCursorableOffset(state, paint);
    412         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
    413         moveCursorToLeftCursorableOffset(state, paint);
    414         state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
    415         moveCursorToLeftCursorableOffset(state, paint);
    416     }
    417 }
    418