| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.text.cts; |
| |
| import android.test.AndroidTestCase; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.StaticLayout; |
| import android.text.TextDirectionHeuristics; |
| import android.text.TextPaint; |
| import android.text.Layout.Alignment; |
| import android.text.style.MetricAffectingSpan; |
| import android.util.Log; |
| |
| public class StaticLayoutLineBreakingTest extends AndroidTestCase { |
| // Span test are currently not supported because text measurement uses the MeasuredText |
| // internal mWorkPaint instead of the provided MockTestPaint. |
| private static final boolean SPAN_TESTS_SUPPORTED = false; |
| private static final boolean DEBUG = false; |
| |
| private static final float SPACE_MULTI = 1.0f; |
| private static final float SPACE_ADD = 0.0f; |
| private static final int WIDTH = 100; |
| private static final Alignment ALIGN = Alignment.ALIGN_LEFT; |
| |
| final static char SURR_FIRST = '\uD800'; |
| final static char SURR_SECOND = '\uDF31'; |
| |
| private static final int[] NO_BREAK = new int[] {}; |
| |
| private static final TextPaint mTextPaint = new MockTextPaint(); |
| |
| private static class MockTextPaint extends TextPaint { |
| |
| @Override |
| public float getTextRunAdvances(char[] chars, int index, int count, |
| int contextIndex, int contextCount, int flags, float[] advances, |
| int advancesIndex, int reserved) { |
| |
| // Conditions copy pasted from Paint |
| if (chars == null) { |
| throw new IllegalArgumentException("text cannot be null"); |
| } |
| |
| if (flags != DIRECTION_LTR && flags != DIRECTION_RTL) { |
| throw new IllegalArgumentException("unknown flags value: " + flags); |
| } |
| |
| if ((index | count | contextIndex | contextCount | advancesIndex |
| | (index - contextIndex) | (contextCount - count) |
| | ((contextIndex + contextCount) - (index + count)) |
| | (chars.length - (contextIndex + contextCount)) |
| | (advances == null ? 0 : |
| (advances.length - (advancesIndex + count)))) < 0) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| float res = 0.0f; |
| |
| if (advances != null) { |
| for (int i = 0; i < count; i++) { |
| float width = getCharWidth(chars[index + i]); |
| advances[advancesIndex + i] = width; |
| res += width; |
| } |
| } |
| |
| return res; |
| } |
| } |
| |
| private static float getCharWidth(char c) { |
| switch (Character.toUpperCase(c)) { |
| // Roman figures |
| case 'I': return 1.0f; |
| case 'V': return 5.0f; |
| case 'X': return 10.0f; |
| case 'L': return 50.0f; |
| case 'C': return 100.0f; // equals to WIDTH |
| case ' ': return 10.0f; |
| case '_': return 0.0f; // 0-width character |
| case SURR_FIRST: return 7.0f; |
| case SURR_SECOND: return 3.0f; // Sum of SURR_FIRST-SURR_SECOND is 10 |
| default: return 10.0f; |
| } |
| } |
| |
| private static StaticLayout getStaticLayout(CharSequence source, int width) { |
| return new StaticLayout(source, mTextPaint, width, ALIGN, SPACE_MULTI, SPACE_ADD, false); |
| } |
| |
| private static int[] getBreaks(CharSequence source) { |
| return getBreaks(source, WIDTH); |
| } |
| |
| private static int[] getBreaks(CharSequence source, int width) { |
| StaticLayout staticLayout = getStaticLayout(source, width); |
| |
| int[] breaks = new int[staticLayout.getLineCount() - 1]; |
| for (int line = 0; line < breaks.length; line++) { |
| breaks[line] = staticLayout.getLineEnd(line); |
| } |
| return breaks; |
| } |
| |
| private static void debugLayout(CharSequence source, StaticLayout staticLayout) { |
| if (DEBUG) { |
| int count = staticLayout.getLineCount(); |
| Log.i("StaticLayoutLineBreakingTest", "\"" + source.toString() + "\": " + |
| count + " lines"); |
| for (int line = 0; line < count; line++) { |
| int lineStart = staticLayout.getLineStart(line); |
| int lineEnd = staticLayout.getLineEnd(line); |
| Log.i("StaticLayoutLineBreakingTest", "Line " + line + " [" + lineStart + ".." + |
| lineEnd + "]\t" + source.subSequence(lineStart, lineEnd)); |
| } |
| } |
| } |
| |
| private static void layout(CharSequence source, int[] breaks) { |
| layout(source, breaks, WIDTH); |
| } |
| |
| private static void layout(CharSequence source, int[] breaks, int width) { |
| StaticLayout staticLayout = getStaticLayout(source, width); |
| |
| debugLayout(source, staticLayout); |
| |
| int lineCount = breaks.length + 1; |
| assertEquals("Number of lines", lineCount, staticLayout.getLineCount()); |
| |
| for (int line = 0; line < lineCount; line++) { |
| int lineStart = staticLayout.getLineStart(line); |
| int lineEnd = staticLayout.getLineEnd(line); |
| |
| if (line == 0) { |
| assertEquals("Line start for first line", 0, lineStart); |
| } else { |
| assertEquals("Line start for line " + line, breaks[line - 1], lineStart); |
| } |
| |
| if (line == lineCount - 1) { |
| assertEquals("Line end for last line", source.length(), lineEnd); |
| } else { |
| assertEquals("Line end for line " + line, breaks[line], lineEnd); |
| } |
| } |
| } |
| |
| private static void layoutMaxLines(CharSequence source, int[] breaks, int maxLines) { |
| StaticLayout staticLayout = new StaticLayout(source, 0, source.length(), mTextPaint, WIDTH, |
| ALIGN, TextDirectionHeuristics.LTR, SPACE_MULTI, SPACE_ADD, false /* includePad */, |
| null, WIDTH, maxLines); |
| |
| debugLayout(source, staticLayout); |
| |
| int lineCount = staticLayout.getLineCount(); |
| assertTrue("Number of lines", lineCount <= maxLines); |
| |
| for (int line = 0; line < lineCount; line++) { |
| int lineStart = staticLayout.getLineStart(line); |
| int lineEnd = staticLayout.getLineEnd(line); |
| |
| if (line == 0) { |
| assertEquals("Line start for first line", 0, lineStart); |
| } else { |
| assertEquals("Line start for line " + line, breaks[line - 1], lineStart); |
| } |
| |
| if (line == lineCount - 1 && line != breaks.length - 1) { |
| assertEquals("Line end for last line", source.length(), lineEnd); |
| } else { |
| assertEquals("Line end for line " + line, breaks[line], lineEnd); |
| } |
| } |
| } |
| |
| final static int MAX_SPAN_COUNT = 10; |
| final static int[] spanStarts = new int[MAX_SPAN_COUNT]; |
| final static int[] spanEnds = new int[MAX_SPAN_COUNT]; |
| |
| private static MetricAffectingSpan getMetricAffectingSpan() { |
| return new MetricAffectingSpan() { |
| @Override |
| public void updateDrawState(TextPaint tp) { /* empty */ } |
| |
| @Override |
| public void updateMeasureState(TextPaint p) { /* empty */ } |
| }; |
| } |
| |
| /** |
| * Replaces the "<...>" blocks by spans, assuming non overlapping, correctly defined spans |
| * @param text |
| * @return A CharSequence with '<' '>' replaced by MetricAffectingSpan |
| */ |
| private static CharSequence spanify(String text) { |
| int startIndex = text.indexOf('<'); |
| if (startIndex < 0) return text; |
| |
| int spanCount = 0; |
| do { |
| int endIndex = text.indexOf('>'); |
| if (endIndex < 0) throw new IllegalArgumentException("Unbalanced span markers"); |
| |
| text = text.substring(0, startIndex) + text.substring(startIndex + 1, endIndex) + |
| text.substring(endIndex + 1); |
| |
| spanStarts[spanCount] = startIndex; |
| spanEnds[spanCount] = endIndex - 2; |
| spanCount++; |
| |
| startIndex = text.indexOf('<'); |
| } while (startIndex >= 0); |
| |
| SpannableStringBuilder result = new SpannableStringBuilder(text); |
| for (int i = 0; i < spanCount; i++) { |
| result.setSpan(getMetricAffectingSpan(), spanStarts[i], spanEnds[i], |
| Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| } |
| return result; |
| } |
| |
| public void testNoLineBreak() { |
| // Width lower than WIDTH |
| layout("", NO_BREAK); |
| layout("I", NO_BREAK); |
| layout("V", NO_BREAK); |
| layout("X", NO_BREAK); |
| layout("L", NO_BREAK); |
| layout("I VILI", NO_BREAK); |
| layout("XXXX", NO_BREAK); |
| layout("LXXXX", NO_BREAK); |
| |
| // Width equal to WIDTH |
| layout("C", NO_BREAK); |
| layout("LL", NO_BREAK); |
| layout("L XXXX", NO_BREAK); |
| layout("XXXXXXXXXX", NO_BREAK); |
| layout("XXX XXXXXX", NO_BREAK); |
| layout("XXX XXXX X", NO_BREAK); |
| layout("XXX XXXXX ", NO_BREAK); |
| layout(" XXXXXXXX ", NO_BREAK); |
| layout(" XX XXX ", NO_BREAK); |
| // 0123456789 |
| |
| // Width greater than WIDTH, but no break |
| layout(" XX XXX ", NO_BREAK); |
| layout("XX XXX XXX ", NO_BREAK); |
| layout("XX XXX XXX ", NO_BREAK); |
| layout("XXXXXXXXXX ", NO_BREAK); |
| // 01234567890 |
| } |
| |
| public void testOneLineBreak() { |
| // 01234567890 |
| layout("XX XXX XXXX", new int[] {7}); |
| layout("XX XXXX XXX", new int[] {8}); |
| layout("XX XXXXX XX", new int[] {9}); |
| layout("XX XXXXXX X", new int[] {10}); |
| // 01234567890 |
| layout("XXXXXXXXXXX", new int[] {10}); |
| layout("XXXXXXXXX X", new int[] {10}); |
| layout("XXXXXXXX XX", new int[] {9}); |
| layout("XXXXXXX XXX", new int[] {8}); |
| layout("XXXXXX XXXX", new int[] {7}); |
| // 01234567890 |
| layout("LL LL", new int[] {3}); |
| layout("LLLL", new int[] {2}); |
| layout("C C", new int[] {2}); |
| layout("CC", new int[] {1}); |
| } |
| |
| public void testSpaceAtBreak() { |
| // 0123456789012 |
| layout("XXXX XXXXX X", new int[] {11}); |
| layout("XXXXXXXXXX X", new int[] {11}); |
| layout("XXXXXXXXXV X", new int[] {11}); |
| layout("C X", new int[] {2}); |
| } |
| |
| public void testMultipleSpacesAtBreak() { |
| // 0123456789012 |
| layout("LXX XXXX", new int[] {4}); |
| layout("LXX XXXX", new int[] {5}); |
| layout("LXX XXXX", new int[] {6}); |
| layout("LXX XXXX", new int[] {7}); |
| layout("LXX XXXX", new int[] {8}); |
| } |
| |
| public void testZeroWidthCharacters() { |
| // 0123456789012345678901234 |
| layout("X_X_X_X_X_X_X_X_X_X", NO_BREAK); |
| layout("___X_X_X_X_X_X_X_X_X_X___", NO_BREAK); |
| layout("C_X", new int[] {2}); |
| layout("C__X", new int[] {3}); |
| } |
| |
| /** |
| * Note that when the text has spans, StaticLayout does not use the provided TextPaint to |
| * measure text runs anymore. This is probably a bug. |
| * To be able to use the fake mTextPaint and make this test pass, use mPaint instead of |
| * mWorkPaint in MeasuredText#addStyleRun |
| */ |
| public void testWithSpans() { |
| if (!SPAN_TESTS_SUPPORTED) return; |
| |
| layout(spanify("<012 456 89>"), NO_BREAK); |
| layout(spanify("012 <456> 89"), NO_BREAK); |
| layout(spanify("<012> <456>< 89>"), NO_BREAK); |
| layout(spanify("<012> <456> <89>"), NO_BREAK); |
| |
| layout(spanify("<012> <456> <89>012"), new int[] {8}); |
| layout(spanify("<012> <456> 89<012>"), new int[] {8}); |
| layout(spanify("<012> <456> <89><012>"), new int[] {8}); |
| layout(spanify("<012> <456> 89 <123>"), new int[] {11}); |
| layout(spanify("<012> <456> 89< 123>"), new int[] {11}); |
| layout(spanify("<012> <456> <89> <123>"), new int[] {11}); |
| layout(spanify("012 456 89 <LXX> XX XX"), new int[] {11, 18}); |
| } |
| |
| /* |
| * Adding a span to the string should not change the layout, since the metrics are unchanged. |
| */ |
| public void testWithOneSpan() { |
| if (!SPAN_TESTS_SUPPORTED) return; |
| |
| String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", |
| "012 456 89012 456 89012", "0123456789012" }; |
| |
| MetricAffectingSpan metricAffectingSpan = getMetricAffectingSpan(); |
| |
| for (String text : texts) { |
| // Get the line breaks without any span |
| int[] breaks = getBreaks(text); |
| |
| // Add spans on all possible offsets |
| for (int spanStart = 0; spanStart < text.length(); spanStart++) { |
| for (int spanEnd = spanStart; spanEnd < text.length(); spanEnd++) { |
| SpannableStringBuilder ssb = new SpannableStringBuilder(text); |
| ssb.setSpan(metricAffectingSpan, spanStart, spanEnd, |
| Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| layout(ssb, breaks); |
| } |
| } |
| } |
| } |
| |
| public void testWithTwoSpans() { |
| if (!SPAN_TESTS_SUPPORTED) return; |
| |
| String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", |
| "012 456 89012 456 89012", "0123456789012" }; |
| |
| MetricAffectingSpan metricAffectingSpan1 = getMetricAffectingSpan(); |
| MetricAffectingSpan metricAffectingSpan2 = getMetricAffectingSpan(); |
| |
| for (String text : texts) { |
| // Get the line breaks without any span |
| int[] breaks = getBreaks(text); |
| |
| // Add spans on all possible offsets |
| for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) { |
| for (int spanEnd1 = spanStart1; spanEnd1 < text.length(); spanEnd1++) { |
| SpannableStringBuilder ssb = new SpannableStringBuilder(text); |
| ssb.setSpan(metricAffectingSpan1, spanStart1, spanEnd1, |
| Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| |
| for (int spanStart2 = 0; spanStart2 < text.length(); spanStart2++) { |
| for (int spanEnd2 = spanStart2; spanEnd2 < text.length(); spanEnd2++) { |
| ssb.setSpan(metricAffectingSpan2, spanStart2, spanEnd2, |
| Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| layout(ssb, breaks); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| public static String replace(String string, char c, char r) { |
| return string.replaceAll(String.valueOf(c), String.valueOf(r)); |
| } |
| |
| public void testClassIS() { |
| char[] classISCharacters = new char[] {'.', ',', ':', ';'}; |
| char[] digitCharacters = new char[] {'0', '\u0660', '\u06F0', '\u0966', '\uFF10'}; |
| |
| for (char c : classISCharacters) { |
| // .,:; are class IS breakpoints... (but still shouldn't break alphabetic chars) |
| // 01234567 |
| layout(replace("L XXX#X", '#', c), new int[] {2}); |
| layout(replace("L XXXX#X", '#', c), new int[] {2}); |
| |
| // ...except when adjacent to digits |
| for (char d : digitCharacters) { |
| // 01234567 |
| layout(replace(replace("L XX0#X", '#', c), '0', d), new int[] {2}); |
| layout(replace(replace("L XXX#0", '#', c), '0', d), new int[] {2}); |
| layout(replace(replace("L XXX0#X", '#', c), '0', d), new int[] {2}); |
| layout(replace(replace("L XXXX#0", '#', c), '0', d), new int[] {2}); |
| } |
| } |
| } |
| |
| public void testClassSYandHY() { |
| char[] classSYorHYCharacters = new char[] {'/', '-'}; |
| char[] digitCharacters = new char[] {'0', '\u0660', '\u06F0', '\u0966', '\uFF10'}; |
| |
| for (char c : classSYorHYCharacters) { |
| // / is a class SY breakpoint, - a class HY... |
| // 01234567 |
| layout(replace("L XXX#X", '#', c), new int[] {6}); |
| layout(replace("L XXXX#X", '#', c), new int[] {2}); |
| |
| // ...except when followed by a digits |
| for (char d : digitCharacters) { |
| // 01234567 |
| layout(replace(replace("L XX0#X", '#', c), '0', d), new int[] {6}); |
| layout(replace(replace("L XXX#0", '#', c), '0', d), new int[] {2}); |
| layout(replace(replace("L XXX0#X", '#', c), '0', d), new int[] {2}); |
| layout(replace(replace("L XXXX#0", '#', c), '0', d), new int[] {2}); |
| } |
| } |
| } |
| |
| public void testClassID() { |
| char ideographic = '\u8a9e'; // regular ideographic character |
| char hyphen = '\u30A0'; // KATAKANA-HIRAGANA DOUBLE HYPHEN, ideographic but non starter |
| |
| // Single ideographs are normal characters |
| layout("L XXX" + ideographic, NO_BREAK); |
| layout("L XXX" + ideographic + "X", new int[] {2}); |
| layout("L XXXX" + ideographic, new int[] {2}); |
| layout("L XXXX" + ideographic + "X", new int[] {2}); |
| |
| // Two adjacent ideographs create a possible breakpoint |
| layout("L X" + ideographic + ideographic + "X", NO_BREAK); |
| layout("L X" + ideographic + ideographic + "XX", new int[] {4}); |
| layout("L XX" + ideographic + ideographic + "XX", new int[] {5}); |
| layout("L XXX" + ideographic + ideographic + "X", new int[] {6}); |
| layout("L XXXX" + ideographic + ideographic + "X", new int[] {2}); |
| |
| // Except when the second one is a non starter |
| layout("L X" + ideographic + hyphen + "X", NO_BREAK); |
| layout("L X" + ideographic + hyphen + "XX", new int[] {2}); |
| layout("L XX" + ideographic + hyphen + "XX", new int[] {2}); |
| layout("L XXX" + ideographic + hyphen + "X", new int[] {2}); |
| layout("L XXXX" + ideographic + hyphen + "X", new int[] {2}); |
| |
| // When the non-starter is first, a pair of ideographic characters is a line break |
| layout("L X" + hyphen + ideographic + "X", NO_BREAK); |
| layout("L X" + hyphen + ideographic + "XX", new int[] {4}); |
| layout("L XX" + hyphen + ideographic + "XX", new int[] {5}); |
| layout("L XXX" + hyphen + ideographic + "X", new int[] {6}); |
| layout("L XXXX" + hyphen + ideographic + "X", new int[] {2}); |
| } |
| |
| public void testReplacementSpan() { |
| // Add ReplacementSpan to the string |
| } |
| |
| public void testParagraphs() { |
| // Add \n to the text |
| } |
| |
| public void testWithEmoji() { |
| // Surrogate emoji characters get replaced by a bitmap |
| } |
| |
| public void testWithSurrogate() { |
| layout("LX" + SURR_FIRST + SURR_SECOND, NO_BREAK); |
| layout("LXXXX" + SURR_FIRST + SURR_SECOND, NO_BREAK); |
| // LXXXXI (91) + SURR_FIRST (7) fits. But we should not break the surrogate pair |
| // Bug: surrogate pair is broken, should be 6 (breaking after the 'V') |
| // Maybe not: may be ok if the second character of a pair always has a 0-width |
| layout("LXXXXI" + SURR_FIRST + SURR_SECOND, new int[] {7}); |
| |
| // LXXXXI (95) + SURR_SECOND (3) fits, but this is not a valid surrogate pair, breaking it |
| layout("LXXXXV" + SURR_SECOND + SURR_FIRST, new int[] {7}); |
| |
| layout("C" + SURR_FIRST + SURR_SECOND, new int[] {1}); |
| } |
| |
| public void testNarrowWidth() { |
| int[] widths = new int[] { 0, 4, 10 }; |
| String[] texts = new String[] { "", "X", " ", "XX", " X", "XXX" }; |
| |
| for (String text: texts) { |
| // 15 is such that only one character will fit |
| int[] breaks = getBreaks(text, 15); |
| |
| // Width under 15 should all lead to the same line break |
| for (int width: widths) { |
| layout(text, breaks, width); |
| } |
| } |
| } |
| |
| public void testNarrowWidthWithSpace() { |
| int[] widths = new int[] { 0, 4 }; |
| for (int width: widths) { |
| layout("X ", new int[] {1}, width); |
| layout("X ", new int[] {1}, width); |
| layout("XX ", new int[] {1, 2}, width); |
| layout("XX ", new int[] {1, 2}, width); |
| layout("X X", new int[] {1, 3}, width); |
| layout("X X", new int[] {1, 2}, width); |
| |
| layout(" ", NO_BREAK, width); |
| layout(" X", new int[] {1}, width); |
| layout(" ", NO_BREAK, width); |
| layout(" X X", new int[] {1, 2, 3}, width); |
| layout(" X", new int[] {2}, width); |
| } |
| } |
| |
| public void testNarrowWidthZeroWidth() { |
| int[] widths = new int[] { 0, 4 }; |
| for (int width: widths) { |
| layout("X.", new int[] {1}, width); |
| layout("X__", new int[] {1}, width); |
| layout("X__X", new int[] {1, 3}, width); // Could be {1} |
| layout("X__X_", new int[] {1, 3, 4}, width); // Could be {1, 4} |
| |
| layout("_", NO_BREAK, width); |
| layout("__", NO_BREAK, width); |
| layout("_X", new int[] {1}, width); // Could be NO_BREAK |
| layout("_X_", new int[] {1, 2}, width); // Could be {2} |
| layout("_X__", new int[] {1, 2}, width); // Could be {2} |
| } |
| } |
| |
| public void testMaxLines() { |
| layoutMaxLines("C", NO_BREAK, 1); |
| layoutMaxLines("C C", new int[] {2}, 1); |
| layoutMaxLines("C C", new int[] {2}, 2); |
| layoutMaxLines("CC", new int[] {1}, 1); |
| layoutMaxLines("CC", new int[] {1}, 2); |
| } |
| } |