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; 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.assertTrue; 23 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Typeface; 27 import android.platform.test.annotations.Presubmit; 28 import android.text.Layout.TabStops; 29 import android.text.style.ReplacementSpan; 30 import android.text.style.TabStopSpan; 31 32 import androidx.test.InstrumentationRegistry; 33 import androidx.test.filters.SmallTest; 34 import androidx.test.filters.Suppress; 35 import androidx.test.runner.AndroidJUnit4; 36 37 import org.junit.Test; 38 import org.junit.runner.RunWith; 39 40 import java.util.Arrays; 41 42 @Presubmit 43 @SmallTest 44 @RunWith(AndroidJUnit4.class) 45 public class TextLineTest { stretchesToFullWidth(CharSequence line)46 private boolean stretchesToFullWidth(CharSequence line) { 47 final TextPaint paint = new TextPaint(); 48 final TextLine tl = TextLine.obtain(); 49 tl.set(paint, line, 0, line.length(), Layout.DIR_LEFT_TO_RIGHT, 50 Layout.DIRS_ALL_LEFT_TO_RIGHT, false /* hasTabs */, null /* tabStops */, 51 0, 0 /* no ellipsis */); 52 final float originalWidth = tl.metrics(null); 53 final float expandedWidth = 2 * originalWidth; 54 55 tl.justify(expandedWidth); 56 final float newWidth = tl.metrics(null); 57 TextLine.recycle(tl); 58 return Math.abs(newWidth - expandedWidth) < 0.5; 59 } 60 61 @Test testJustify_spaces()62 public void testJustify_spaces() { 63 // There are no spaces to stretch. 64 assertFalse(stretchesToFullWidth("text")); 65 66 assertTrue(stretchesToFullWidth("one space")); 67 assertTrue(stretchesToFullWidth("exactly two spaces")); 68 assertTrue(stretchesToFullWidth("up to three spaces")); 69 } 70 71 // NBSP should also stretch when it's not used as a base for a combining mark. This doesn't work 72 // yet (b/68204709). 73 @Suppress disabledTestJustify_NBSP()74 public void disabledTestJustify_NBSP() { 75 final char nbsp = '\u00A0'; 76 assertTrue(stretchesToFullWidth("non-breaking" + nbsp + "space")); 77 assertTrue(stretchesToFullWidth("mix" + nbsp + "and match")); 78 79 final char combining_acute = '\u0301'; 80 assertFalse(stretchesToFullWidth("combining" + nbsp + combining_acute + "acute")); 81 } 82 83 // The test font has following coverage and width. 84 // U+0020: 10em 85 // U+002E (.): 10em 86 // U+0043 (C): 100em 87 // U+0049 (I): 1em 88 // U+004C (L): 50em 89 // U+0056 (V): 5em 90 // U+0058 (X): 10em 91 // U+005F (_): 0em 92 // U+05D0 : 1em // HEBREW LETTER ALEF 93 // U+05D1 : 5em // HEBREW LETTER BET 94 // U+FFFD (invalid surrogate will be replaced to this): 7em 95 // U+10331 (\uD800\uDF31): 10em 96 private static final Typeface TYPEFACE = Typeface.createFromAsset( 97 InstrumentationRegistry.getInstrumentation().getTargetContext().getAssets(), 98 "fonts/StaticLayoutLineBreakingTestFont.ttf"); 99 getTextLine(String str, TextPaint paint, TabStops tabStops)100 private TextLine getTextLine(String str, TextPaint paint, TabStops tabStops) { 101 Layout layout = 102 StaticLayout.Builder.obtain(str, 0, str.length(), paint, Integer.MAX_VALUE) 103 .build(); 104 TextLine tl = TextLine.obtain(); 105 tl.set(paint, str, 0, str.length(), 106 TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(str, 0, str.length()) ? -1 : 1, 107 layout.getLineDirections(0), tabStops != null, tabStops, 108 0, 0 /* no ellipsis */); 109 return tl; 110 } 111 getTextLine(String str, TextPaint paint)112 private TextLine getTextLine(String str, TextPaint paint) { 113 return getTextLine(str, paint, null); 114 } 115 assertMeasurements(final TextLine tl, final int length, boolean trailing, final float[] expected)116 private void assertMeasurements(final TextLine tl, final int length, boolean trailing, 117 final float[] expected) { 118 for (int offset = 0; offset <= length; ++offset) { 119 assertEquals(expected[offset], tl.measure(offset, trailing, null), 0.0f); 120 } 121 122 final boolean[] trailings = new boolean[length + 1]; 123 Arrays.fill(trailings, trailing); 124 final float[] allMeasurements = tl.measureAllOffsets(trailings, null); 125 assertArrayEquals(expected, allMeasurements, 0.0f); 126 } 127 128 @Test testMeasure_LTR()129 public void testMeasure_LTR() { 130 final TextPaint paint = new TextPaint(); 131 paint.setTypeface(TYPEFACE); 132 paint.setTextSize(10.0f); // make 1em = 10px 133 134 TextLine tl = getTextLine("IIIIIV", paint); 135 assertMeasurements(tl, 6, false, 136 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, 100.0f}); 137 assertMeasurements(tl, 6, true, 138 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, 100.0f}); 139 } 140 141 @Test testMeasure_RTL()142 public void testMeasure_RTL() { 143 final TextPaint paint = new TextPaint(); 144 paint.setTypeface(TYPEFACE); 145 paint.setTextSize(10.0f); // make 1em = 10px 146 147 TextLine tl = getTextLine("\u05D0\u05D0\u05D0\u05D0\u05D0\u05D1", paint); 148 assertMeasurements(tl, 6, false, 149 new float[]{0.0f, -10.0f, -20.0f, -30.0f, -40.0f, -50.0f, -100.0f}); 150 assertMeasurements(tl, 6, true, 151 new float[]{0.0f, -10.0f, -20.0f, -30.0f, -40.0f, -50.0f, -100.0f}); 152 } 153 154 @Test testMeasure_BiDi()155 public void testMeasure_BiDi() { 156 final TextPaint paint = new TextPaint(); 157 paint.setTypeface(TYPEFACE); 158 paint.setTextSize(10.0f); // make 1em = 10px 159 160 TextLine tl = getTextLine("II\u05D0\u05D0II", paint); 161 assertMeasurements(tl, 6, false, 162 new float[]{0.0f, 10.0f, 40.0f, 30.0f, 40.0f, 50.0f, 60.0f}); 163 assertMeasurements(tl, 6, true, 164 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 20.0f, 50.0f, 60.0f}); 165 } 166 167 private static final String LRI = "\u2066"; // LEFT-TO-RIGHT ISOLATE 168 private static final String RLI = "\u2067"; // RIGHT-TO-LEFT ISOLATE 169 private static final String PDI = "\u2069"; // POP DIRECTIONAL ISOLATE 170 171 @Test testMeasure_BiDi2()172 public void testMeasure_BiDi2() { 173 final TextPaint paint = new TextPaint(); 174 paint.setTypeface(TYPEFACE); 175 paint.setTextSize(10.0f); // make 1em = 10px 176 177 TextLine tl = getTextLine("I" + RLI + "I\u05D0\u05D0" + PDI + "I", paint); 178 assertMeasurements(tl, 7, false, 179 new float[]{0.0f, 10.0f, 30.0f, 30.0f, 20.0f, 40.0f, 40.0f, 50.0f}); 180 assertMeasurements(tl, 7, true, 181 new float[]{0.0f, 10.0f, 10.0f, 40.0f, 20.0f, 10.0f, 40.0f, 50.0f}); 182 } 183 184 @Test testMeasure_BiDi3()185 public void testMeasure_BiDi3() { 186 final TextPaint paint = new TextPaint(); 187 paint.setTypeface(TYPEFACE); 188 paint.setTextSize(10.0f); // make 1em = 10px 189 190 TextLine tl = getTextLine("\u05D0" + LRI + "\u05D0II" + PDI + "\u05D0", paint); 191 assertMeasurements(tl, 7, false, 192 new float[]{0.0f, -10.0f, -30.0f, -30.0f, -20.0f, -40.0f, -40.0f, -50.0f}); 193 assertMeasurements(tl, 7, true, 194 new float[]{0.0f, -10.0f, -10.0f, -40.0f, -20.0f, -10.0f, -40.0f, -50.0f}); 195 } 196 197 @Test testMeasure_Tab_LTR()198 public void testMeasure_Tab_LTR() { 199 final Object[] spans = { new TabStopSpan.Standard(100) }; 200 final TabStops stops = new TabStops(100, spans); 201 final TextPaint paint = new TextPaint(); 202 paint.setTypeface(TYPEFACE); 203 paint.setTextSize(10.0f); // make 1em = 10px 204 205 TextLine tl = getTextLine("II\tII", paint, stops); 206 assertMeasurements(tl, 5, false, 207 new float[]{0.0f, 10.0f, 20.0f, 100.0f, 110.0f, 120.0f}); 208 assertMeasurements(tl, 5, true, 209 new float[]{0.0f, 10.0f, 20.0f, 100.0f, 110.0f, 120.0f}); 210 } 211 212 @Test testMeasure_Tab_RTL()213 public void testMeasure_Tab_RTL() { 214 final Object[] spans = { new TabStopSpan.Standard(100) }; 215 final TabStops stops = new TabStops(100, spans); 216 final TextPaint paint = new TextPaint(); 217 paint.setTypeface(TYPEFACE); 218 paint.setTextSize(10.0f); // make 1em = 10px 219 220 TextLine tl = getTextLine("\u05D0\u05D0\t\u05D0\u05D0", paint, stops); 221 assertMeasurements(tl, 5, false, 222 new float[]{0.0f, -10.0f, -20.0f, -100.0f, -110.0f, -120.0f}); 223 assertMeasurements(tl, 5, true, 224 new float[]{0.0f, -10.0f, -20.0f, -100.0f, -110.0f, -120.0f}); 225 } 226 227 @Test testMeasure_Tab_BiDi()228 public void testMeasure_Tab_BiDi() { 229 final Object[] spans = { new TabStopSpan.Standard(100) }; 230 final TabStops stops = new TabStops(100, spans); 231 final TextPaint paint = new TextPaint(); 232 paint.setTypeface(TYPEFACE); 233 paint.setTextSize(10.0f); // make 1em = 10px 234 235 TextLine tl = getTextLine("I\u05D0\tI\u05D0", paint, stops); 236 assertMeasurements(tl, 5, false, 237 new float[]{0.0f, 20.0f, 20.0f, 100.0f, 120.0f, 120.0f}); 238 assertMeasurements(tl, 5, true, 239 new float[]{0.0f, 10.0f, 10.0f, 100.0f, 110.0f, 110.0f}); 240 } 241 242 @Test testMeasure_Tab_BiDi2()243 public void testMeasure_Tab_BiDi2() { 244 final Object[] spans = { new TabStopSpan.Standard(100) }; 245 final TabStops stops = new TabStops(100, spans); 246 final TextPaint paint = new TextPaint(); 247 paint.setTypeface(TYPEFACE); 248 paint.setTextSize(10.0f); // make 1em = 10px 249 250 TextLine tl = getTextLine("\u05D0I\t\u05D0I", paint, stops); 251 assertMeasurements(tl, 5, false, 252 new float[]{0.0f, -20.0f, -20.0f, -100.0f, -120.0f, -120.0f}); 253 assertMeasurements(tl, 5, true, 254 new float[]{0.0f, -10.0f, -10.0f, -100.0f, -110.0f, -110.0f}); 255 } 256 257 @Test testMeasure_wordSpacing()258 public void testMeasure_wordSpacing() { 259 final TextPaint paint = new TextPaint(); 260 paint.setTypeface(TYPEFACE); 261 paint.setTextSize(10.0f); // make 1em = 10px 262 paint.setWordSpacing(10.0f); 263 264 TextLine tl = getTextLine("I I", paint); 265 assertMeasurements(tl, 3, false, 266 new float[]{0.0f, 10.0f, 120.0f, 130.0f}); 267 } 268 269 @Test testHandleRun_ellipsizedReplacementSpan_isSkipped()270 public void testHandleRun_ellipsizedReplacementSpan_isSkipped() { 271 final Spannable text = new SpannableStringBuilder("This is a... text"); 272 273 // Setup a replacement span that the measurement should not interact with. 274 final TestReplacementSpan span = new TestReplacementSpan(); 275 text.setSpan(span, 9, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 276 277 final TextLine tl = TextLine.obtain(); 278 tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT, 279 false /* hasTabs */, null /* tabStops */, 9, 12); 280 tl.measure(text.length(), false /* trailing */, null /* fmi */); 281 282 assertFalse(span.mIsUsed); 283 } 284 285 @Test testHandleRun_notEllipsizedReplacementSpan_isNotSkipped()286 public void testHandleRun_notEllipsizedReplacementSpan_isNotSkipped() { 287 final Spannable text = new SpannableStringBuilder("This is a... text"); 288 289 // Setup a replacement span that the measurement should not interact with. 290 final TestReplacementSpan span = new TestReplacementSpan(); 291 text.setSpan(span, 1, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 292 293 final TextLine tl = TextLine.obtain(); 294 tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT, 295 false /* hasTabs */, null /* tabStops */, 9, 12); 296 tl.measure(text.length(), false /* trailing */, null /* fmi */); 297 298 assertTrue(span.mIsUsed); 299 } 300 301 @Test testHandleRun_halfEllipsizedReplacementSpan_isNotSkipped()302 public void testHandleRun_halfEllipsizedReplacementSpan_isNotSkipped() { 303 final Spannable text = new SpannableStringBuilder("This is a... text"); 304 305 // Setup a replacement span that the measurement should not interact with. 306 final TestReplacementSpan span = new TestReplacementSpan(); 307 text.setSpan(span, 7, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 308 309 final TextLine tl = TextLine.obtain(); 310 tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT, 311 false /* hasTabs */, null /* tabStops */, 9, 12); 312 tl.measure(text.length(), false /* trailing */, null /* fmi */); 313 assertTrue(span.mIsUsed); 314 } 315 316 private static class TestReplacementSpan extends ReplacementSpan { 317 boolean mIsUsed; 318 319 @Override getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)320 public int getSize(Paint paint, CharSequence text, int start, int end, 321 Paint.FontMetricsInt fm) { 322 mIsUsed = true; 323 return 0; 324 } 325 326 @Override draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)327 public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, 328 int y, 329 int bottom, Paint paint) { 330 mIsUsed = true; 331 } 332 } 333 } 334