Enable nested bidi levels in a paragraph.

Changes the internal representation of direction information in the Directions object to be a visually-ordered list of start/length+direction pairs instead of a list of directionality inversion offsets.

Rewrite Layout.getOffsetToLeft/RightOf to use run information instead of width metrics.

Remove java Bidi, use native.  Switch bidi tests to test native, expect levels instead of dirs.

Add test of directionality.  Leave in switch to turn new code off and restore previous behavior for now.

Change-Id: Iea8bb46c678a18820e237c90f76007a084c83051
This commit is contained in:
Doug Felt
2010-03-01 12:45:56 -08:00
committed by Kenny Root
parent 5833ae3c9d
commit 9f7a4442b8
6 changed files with 724 additions and 616 deletions

View File

@@ -310,7 +310,6 @@ extends Layout
Directions[] objects = new Directions[1];
for (int i = 0; i < n; i++) {
ints[START] = reflowed.getLineStart(i) |
(reflowed.getParagraphDirection(i) << DIR_SHIFT) |

File diff suppressed because it is too large Load Diff

View File

@@ -224,7 +224,7 @@ extends Layout
boolean easy = true;
boolean altered = false;
int dir = DEFAULT_DIR; // XXX
int dir = DEFAULT_DIR; // XXX pass value in
for (int i = 0; i < n; i++) {
if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
@@ -253,7 +253,8 @@ extends Layout
if (!easy) {
// XXX put override flags, etc. into chdirs
dir = bidi(dir, chs, chdirs, n, false);
// XXX supply dir rather than force
dir = AndroidBidi.bidi(DIR_REQUEST_DEFAULT_LTR, chs, chdirs, n, false);
// Do mirroring for right-to-left segments
@@ -606,239 +607,6 @@ extends Layout
}
}
/**
* Runs the unicode bidi algorithm on the first n chars in chs, returning
* the char dirs in chInfo and the base line direction of the first
* paragraph.
*
* XXX change result from dirs to levels
*
* @param dir the direction flag, either DIR_REQUEST_LTR,
* DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL.
* @param chs the text to examine
* @param chInfo on input, if hasInfo is true, override and other flags
* representing out-of-band embedding information. On output, the generated
* dirs of the text.
* @param n the length of the text/information in chs and chInfo
* @param hasInfo true if chInfo has input information, otherwise the
* input data in chInfo is ignored.
* @return the resolved direction level of the first paragraph, either
* DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT.
*/
/* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n,
boolean hasInfo) {
AndroidCharacter.getDirectionalities(chs, chInfo, n);
/*
* Determine primary paragraph direction if not specified
*/
if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) {
// set up default
dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT;
for (int j = 0; j < n; j++) {
int d = chInfo[j];
if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
dir = DIR_LEFT_TO_RIGHT;
break;
}
if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
dir = DIR_RIGHT_TO_LEFT;
break;
}
}
}
final byte SOR = dir == DIR_LEFT_TO_RIGHT ?
Character.DIRECTIONALITY_LEFT_TO_RIGHT :
Character.DIRECTIONALITY_RIGHT_TO_LEFT;
/*
* XXX Explicit overrides should go here
*/
/*
* Weak type resolution
*/
// dump(chdirs, n, "initial");
// W1 non spacing marks
for (int j = 0; j < n; j++) {
if (chInfo[j] == Character.NON_SPACING_MARK) {
if (j == 0)
chInfo[j] = SOR;
else
chInfo[j] = chInfo[j - 1];
}
}
// dump(chdirs, n, "W1");
// W2 european numbers
byte cur = SOR;
for (int j = 0; j < n; j++) {
byte d = chInfo[j];
if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
d == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
cur = d;
else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) {
if (cur ==
Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
}
}
// dump(chdirs, n, "W2");
// W3 arabic letters
for (int j = 0; j < n; j++) {
if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
}
// dump(chdirs, n, "W3");
// W4 single separator between numbers
for (int j = 1; j < n - 1; j++) {
byte d = chInfo[j];
byte prev = chInfo[j - 1];
byte next = chInfo[j + 1];
if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) {
if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
} else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) {
if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER &&
next == Character.DIRECTIONALITY_ARABIC_NUMBER)
chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
}
}
// dump(chdirs, n, "W4");
// W5 european number terminators
boolean adjacent = false;
for (int j = 0; j < n; j++) {
byte d = chInfo[j];
if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
adjacent = true;
else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent)
chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
else
adjacent = false;
}
//dump(chdirs, n, "W5");
// W5 european number terminators part 2,
// W6 separators and terminators
adjacent = false;
for (int j = n - 1; j >= 0; j--) {
byte d = chInfo[j];
if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
adjacent = true;
else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) {
if (adjacent)
chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
else
chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
}
else {
adjacent = false;
if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR ||
d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR ||
d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR)
chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
}
}
// dump(chdirs, n, "W6");
// W7 strong direction of european numbers
cur = SOR;
for (int j = 0; j < n; j++) {
byte d = chInfo[j];
if (d == SOR ||
d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
d == Character.DIRECTIONALITY_RIGHT_TO_LEFT)
cur = d;
if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
chInfo[j] = cur;
}
// dump(chdirs, n, "W7");
// N1, N2 neutrals
cur = SOR;
for (int j = 0; j < n; j++) {
byte d = chInfo[j];
if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
cur = d;
} else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
d == Character.DIRECTIONALITY_ARABIC_NUMBER) {
cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
} else {
byte dd = SOR;
int k;
for (k = j + 1; k < n; k++) {
dd = chInfo[k];
if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
break;
}
if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
dd == Character.DIRECTIONALITY_ARABIC_NUMBER) {
dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
break;
}
}
for (int y = j; y < k; y++) {
if (dd == cur)
chInfo[y] = cur;
else
chInfo[y] = SOR;
}
j = k - 1;
}
}
// dump(chdirs, n, "final");
// extra: enforce that all tabs and surrogate characters go the
// primary direction
// TODO: actually do directions right for surrogates
for (int j = 0; j < n; j++) {
char c = chs[j];
if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) {
chInfo[j] = SOR;
}
}
return dir;
}
private static final char FIRST_CJK = '\u2E80';
/**
* Returns true if the specified character is one of those specified
@@ -1062,49 +830,123 @@ extends Layout
if (tab)
lines[off + TAB] |= TAB_MASK;
{
lines[off + DIR] |= dir << DIR_SHIFT;
int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
int count = 0;
if (!easy) {
for (int k = start; k < end; k++) {
if (chdirs[k - pstart] != cur) {
count++;
cur = chdirs[k - pstart];
}
lines[off + DIR] |= dir << DIR_SHIFT;
Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
// easy means all chars < the first RTL, so no emoji, no nothing
// XXX a run with no text or all spaces is easy but might be an empty
// RTL paragraph. Make sure easy is false if this is the case.
if (easy) {
mLineDirections[j] = linedirs;
} else {
int startOff = start - pstart;
int baseLevel = dir == DIR_LEFT_TO_RIGHT ? 0 : 1;
int curLevel = chdirs[startOff];
int minLevel = curLevel;
int runCount = 1;
for (int i = start + 1; i < end; ++i) {
int level = chdirs[i - pstart];
if (level != curLevel) {
curLevel = level;
++runCount;
}
}
// add final run for trailing counter-directional whitespace
int visEnd = end;
if ((curLevel & 1) != (baseLevel & 1)) {
// look for visible end
while (--visEnd >= start) {
char ch = text.charAt(visEnd);
Directions linedirs;
if (ch == '\n') {
--visEnd;
break;
}
if (count == 0) {
linedirs = DIRS_ALL_LEFT_TO_RIGHT;
if (ch != ' ' && ch != '\t') {
break;
}
}
++visEnd;
if (visEnd != end) {
++runCount;
}
}
if (runCount == 1 && minLevel == baseLevel) {
if ((minLevel & 1) != 0) {
linedirs = DIRS_ALL_RIGHT_TO_LEFT;
}
// we're done, only one run on this line
} else {
short[] ld = new short[count + 1];
cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
count = 0;
int here = start;
for (int k = start; k < end; k++) {
if (chdirs[k - pstart] != cur) {
// XXX check to make sure we don't
// overflow short
ld[count++] = (short) (k - here);
cur = chdirs[k - pstart];
here = k;
int[] ld = new int[runCount * 2];
int maxLevel = minLevel;
int levelBits = minLevel << RUN_LEVEL_SHIFT;
{
// Start of first pair is always 0, we write
// length then start at each new run, and the
// last run length after we're done.
int n = 1;
int prev = start;
curLevel = minLevel;
for (int i = start; i < visEnd; ++i) {
int level = chdirs[i - pstart];
if (level != curLevel) {
curLevel = level;
if (level > maxLevel) {
maxLevel = level;
} else if (level < minLevel) {
minLevel = level;
}
// XXX ignore run length limit of 2^RUN_LEVEL_SHIFT
ld[n++] = (i - prev) | levelBits;
ld[n++] = i - start;
levelBits = curLevel << RUN_LEVEL_SHIFT;
prev = i;
}
}
ld[n] = (visEnd - prev) | levelBits;
if (visEnd < end) {
ld[++n] = visEnd - start;
ld[++n] = (end - visEnd) | (baseLevel << RUN_LEVEL_SHIFT);
}
}
ld[count] = (short) (end - here);
if (count == 1 && ld[0] == 0) {
linedirs = DIRS_ALL_RIGHT_TO_LEFT;
// See if we need to swap any runs.
// If the min level run direction doesn't match the base
// direction, we always need to swap (at this point
// we have more than one run).
// Otherwise, we don't need to swap the lowest level.
// Since there are no logically adjacent runs at the same
// level, if the max level is the same as the (new) min
// level, we have a series of alternating levels that
// is already in order, so there's no more to do.
//
boolean swap;
if ((minLevel & 1) == baseLevel) {
minLevel += 1;
swap = maxLevel > minLevel;
} else {
linedirs = new Directions(ld);
swap = runCount > 1;
}
if (swap) {
for (int level = maxLevel - 1; level >= minLevel; --level) {
for (int i = 0; i < ld.length; i += 2) {
if (chdirs[startOff + ld[i]] >= level) {
int e = i + 2;
while (e < ld.length && chdirs[startOff + ld[e]] >= level) {
e += 2;
}
for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) {
int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x;
x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x;
}
i = e + 2;
}
}
}
}
linedirs = new Directions(ld);
}
mLineDirections[j] = linedirs;

View File

@@ -22,7 +22,7 @@ import android.util.Log;
import junit.framework.TestCase;
/**
* Tests StaticLayout bidi implementation.
* Quick check of native bidi implementation.
*/
public class StaticLayoutBidiTest extends TestCase {
@@ -41,73 +41,47 @@ public class StaticLayoutBidiTest extends TestCase {
//@SmallTest
public void testAllLtr() {
expectBidi(REQ_DL, "a test", "000000", L);
expectNativeBidi(REQ_DL, "a test", "000000", L);
}
//@SmallTest
public void testLtrRtl() {
expectBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L);
expectNativeBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L);
}
//@SmallTest
public void testAllRtl() {
expectBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R);
expectNativeBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R);
}
//@SmallTest
public void testRtlLtr() {
expectBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111000", R);
expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
}
//@SmallTest
public void testRAllLtr() {
expectBidi(REQ_R, "a test", "000000", R);
expectNativeBidi(REQ_R, "a test", "222222", R);
}
//@SmallTest
public void testRLtrRtl() {
expectBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "0001111", R);
expectNativeBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "2221111", R);
}
//@SmallTest
public void testLAllRtl() {
expectBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L);
expectNativeBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L);
}
//@SmallTest
public void testLRtlLtr() {
expectBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L);
}
private void expectBidi(int dir, String text,
String expectedLevels, int expectedDir) {
char[] chs = text.toCharArray();
int n = chs.length;
byte[] chInfo = new byte[n];
int resultDir = StaticLayout.bidi(dir, chs, chInfo, n, false);
{
StringBuilder sb = new StringBuilder("info:");
for (int i = 0; i < n; ++i) {
sb.append(" ").append(String.valueOf(chInfo[i]));
}
Log.i("BIDI", sb.toString());
}
char[] resultLevelChars = new char[n];
for (int i = 0; i < n; ++i) {
resultLevelChars[i] = (char)('0' + chInfo[i]);
}
String resultLevels = new String(resultLevelChars);
assertEquals("direction", expectedDir, resultDir);
assertEquals("levels", expectedLevels, resultLevels);
expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
}
//@SmallTest
public void testNativeBidi() {
// native bidi returns levels, not simply directions
expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
expectNativeBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L);
}
private void expectNativeBidi(int dir, String text,

View File

@@ -0,0 +1,233 @@
/*
* Copyright (C) 2010 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;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.Layout.Directions;
import android.text.StaticLayoutTest.LayoutBuilder;
import android.util.Log;
import java.util.Arrays;
import java.util.Formatter;
import junit.framework.TestCase;
public class StaticLayoutDirectionsTest extends TestCase {
private static final char ALEF = '\u05d0';
private static Directions dirs(int ... dirs) {
return new Directions(dirs);
}
private static final int LVL1_1 = 1 | (1 << Layout.RUN_LEVEL_SHIFT);
private static final int LVL2_1 = 1 | (2 << Layout.RUN_LEVEL_SHIFT);
private static final int LVL2_2 = 2 | (2 << Layout.RUN_LEVEL_SHIFT);
private static String[] texts = {
"",
" ",
"a",
"a1",
"aA",
"a1b",
"a1A",
"aA1",
"aAb",
"aA1B",
"aA1B2",
// rtl
"A",
"A1",
"Aa",
"A1B",
"A1a",
"Aa1",
"AaB"
};
// Expected directions are an array of start/length+level pairs,
// in visual order from the leading margin.
private static Directions[] expected = {
Layout.DIRS_ALL_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT,
dirs(0, 1, 1, LVL1_1),
Layout.DIRS_ALL_LEFT_TO_RIGHT,
dirs(0, 2, 2, LVL1_1),
dirs(0, 1, 2, LVL2_1, 1, LVL1_1),
dirs(0, 1, 1, LVL1_1, 2, 1),
dirs(0, 1, 3, LVL1_1, 2, LVL2_1, 1, LVL1_1),
dirs(0, 1, 4, LVL2_1, 3, LVL1_1, 2, LVL2_1, 1, LVL1_1),
// rtl
Layout.DIRS_ALL_RIGHT_TO_LEFT,
dirs(0, LVL1_1, 1, LVL2_1),
dirs(0, LVL1_1, 1, LVL2_1),
dirs(0, LVL1_1, 1, LVL2_1, 2, LVL1_1),
dirs(0, LVL1_1, 1, LVL2_2),
dirs(0, LVL1_1, 1, LVL2_2),
dirs(0, LVL1_1, 1, LVL2_1, 2, LVL1_1),
};
private static String pseudoBidiToReal(String src) {
char[] chars = src.toCharArray();
for (int j = 0; j < chars.length; ++j) {
char c = chars[j];
if (c >= 'A' && c <= 'D') {
chars[j] = (char)(ALEF + c - 'A');
}
}
return new String(chars, 0, chars.length);
}
// @SmallTest
public void testDirections() {
StringBuilder buf = new StringBuilder("\n");
Formatter f = new Formatter(buf);
LayoutBuilder b = StaticLayoutTest.builder();
for (int i = 0; i < texts.length; ++i) {
b.setText(pseudoBidiToReal(texts[i]));
checkDirections(b.build(), i, b.text, expected, f);
}
if (buf.length() > 1) {
fail(buf.toString());
}
}
// @SmallTest
public void testTrailingWhitespace() {
LayoutBuilder b = StaticLayoutTest.builder();
b.setText(pseudoBidiToReal("Ab c"));
float width = b.paint.measureText(b.text, 0, 5); // exclude 'c'
b.setWidth(Math.round(width));
Layout l = b.build();
if (l.getLineCount() != 2) {
throw new RuntimeException("expected 2 lines, got: " + l.getLineCount());
}
Directions result = l.getLineDirections(0);
Directions expected = dirs(0, LVL1_1, 1, LVL2_1, 2, 3 | (1 << Layout.RUN_LEVEL_SHIFT));
expectDirections("split line", expected, result);
}
public void testNextToRightOf() {
LayoutBuilder b = StaticLayoutTest.builder();
b.setText(pseudoBidiToReal("aA1B2"));
// visual a2B1A positions 04321
// 0: |a2B1A, strong is sol, after -> 0
// 1: a|2B1A, strong is a, after ->, 1
// 2: a2|B1A, strong is B, after -> 4
// 3: a2B|1A, strong is B, before -> 3
// 4: a2B1|A, strong is A, after -> 2
// 5: a2B1A|, strong is eol, before -> 5
int[] expected = { 0, 1, 4, 3, 2, 5 };
Layout l = b.build();
int n = 0;
for (int i = 1; i < expected.length; ++i) {
int t = l.getOffsetToRightOf(n);
if (t != expected[i]) {
fail("offset[" + i + "] to right of: " + n + " expected: " +
expected[i] + " got: " + t);
}
n = t;
}
}
public void testNextToLeftOf() {
LayoutBuilder b = StaticLayoutTest.builder();
b.setText(pseudoBidiToReal("aA1B2"));
int[] expected = { 0, 1, 4, 3, 2, 5 };
Layout l = b.build();
int n = 5;
for (int i = expected.length - 1; --i >= 0;) {
int t = l.getOffsetToLeftOf(n);
if (t != expected[i]) {
fail("offset[" + i + "] to left of: " + n + " expected: " +
expected[i] + " got: " + t);
}
n = t;
}
}
// utility, not really a test
public void testMeasureText1() {
LayoutBuilder b = StaticLayoutTest.builder();
String text = "ABC"; // "abAB"
b.setText(pseudoBidiToReal(text));
Layout l = b.build();
Directions directions = l.getLineDirections(0);
TextPaint workPaint = new TextPaint();
int dir = -1; // LEFT_TO_RIGHT
boolean trailing = true;
boolean alt = true;
do {
dir = -dir;
do {
trailing = !trailing;
for (int offset = 0, end = b.text.length(); offset <= end; ++offset) {
float width = Layout.measureText(b.paint,
workPaint,
b.text,
0, offset, end,
dir, directions,
trailing, false,
null);
Log.i("BIDI", "dir: " + dir + " trail: " + trailing +
" offset: " + offset + " width: " + width);
}
} while (!trailing);
} while (dir > 0);
}
// utility for displaying arrays in hex
private static String hexArray(int[] array) {
StringBuilder sb = new StringBuilder();
sb.append('{');
for (int i : array) {
if (sb.length() > 1) {
sb.append(", ");
}
sb.append(Integer.toHexString(i));
}
sb.append('}');
return sb.toString();
}
private void checkDirections(Layout l, int i, String text,
Directions[] expectedDirs, Formatter f) {
Directions expected = expectedDirs[i];
Directions result = l.getLineDirections(0);
if (!Arrays.equals(expected.mDirections, result.mDirections)) {
f.format("%n[%2d] '%s', %s != %s", i, text,
hexArray(expected.mDirections),
hexArray(result.mDirections));
}
}
private void expectDirections(String msg, Directions expected, Directions result) {
if (!Arrays.equals(expected.mDirections, result.mDirections)) {
fail("expected: " + hexArray(expected.mDirections) +
" got: " + hexArray(result.mDirections));
}
}
}

View File

@@ -228,11 +228,11 @@ public class StaticLayoutTest extends TestCase {
}
}
private static LayoutBuilder builder() {
/* package */ static LayoutBuilder builder() {
return new LayoutBuilder();
}
private static class LayoutBuilder {
/* package */ static class LayoutBuilder {
String text = "This is a test";
TextPaint paint = new TextPaint(); // default
int width = 100;