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]; Directions[] objects = new Directions[1];
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
ints[START] = reflowed.getLineStart(i) | ints[START] = reflowed.getLineStart(i) |
(reflowed.getParagraphDirection(i) << DIR_SHIFT) | (reflowed.getParagraphDirection(i) << DIR_SHIFT) |

View File

@@ -16,19 +16,25 @@
package android.text; package android.text;
import com.android.internal.util.ArrayUtils;
import android.emoji.EmojiFactory; import android.emoji.EmojiFactory;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Path; import android.text.method.TextKeyListener;
import com.android.internal.util.ArrayUtils; import android.text.style.AlignmentSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineBackgroundSpan;
import android.text.style.ParagraphStyle;
import android.text.style.ReplacementSpan;
import android.text.style.TabStopSpan;
import android.view.KeyEvent;
import junit.framework.Assert; import junit.framework.Assert;
import android.text.style.*;
import android.text.method.TextKeyListener;
import android.view.KeyEvent;
/** /**
* A base class that manages text layout in visual elements on * A base class that manages text layout in visual elements on
@@ -524,13 +530,85 @@ public abstract class Layout {
*/ */
public abstract int getBottomPadding(); public abstract int getBottomPadding();
// return the level of the character at offset.
// XXX remove if not needed
private int getRunLevelAtOffset(int offset) {
int line = getLineForOffset(offset);
int lineStart = getLineStart(line);
int lineEnd = getLineVisibleEnd(line);
int[] runs = getLineDirections(line).mDirections;
for (int i = 0; i < runs.length; i += 2) {
int start = runs[i];
if (offset >= start) {
int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
if (limit > lineEnd) {
limit = lineEnd;
}
if (offset < limit) {
return (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
}
}
}
return getParagraphDirection(line) == 1 ? 0 : 1;
}
private boolean primaryIsTrailingPrevious(int offset) {
int line = getLineForOffset(offset);
int lineStart = getLineStart(line);
int lineEnd = getLineVisibleEnd(line);
int[] runs = getLineDirections(line).mDirections;
int levelAt = -1;
for (int i = 0; i < runs.length; i += 2) {
int start = lineStart + runs[i];
int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
if (limit > lineEnd) {
limit = lineEnd;
}
if (offset >= start && offset < limit) {
if (offset > start) {
// Previous character is at same level, so don't use trailing.
return false;
}
levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
break;
}
}
if (levelAt == -1) {
// Offset was limit of line.
levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
}
// At level boundary, check previous level.
int levelBefore = -1;
if (offset == lineStart) {
levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
} else {
offset -= 1;
for (int i = 0; i < runs.length; i += 2) {
int start = lineStart + runs[i];
int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
if (limit > lineEnd) {
limit = lineEnd;
}
if (offset >= start && offset < limit) {
levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
break;
}
}
}
return levelBefore < levelAt;
}
/** /**
* Get the primary horizontal position for the specified text offset. * Get the primary horizontal position for the specified text offset.
* This is the location where a new character would be inserted in * This is the location where a new character would be inserted in
* the paragraph's primary direction. * the paragraph's primary direction.
*/ */
public float getPrimaryHorizontal(int offset) { public float getPrimaryHorizontal(int offset) {
return getHorizontal(offset, false, true); boolean trailing = primaryIsTrailingPrevious(offset);
return getHorizontal(offset, trailing);
} }
/** /**
@@ -539,17 +617,17 @@ public abstract class Layout {
* the direction other than the paragraph's primary direction. * the direction other than the paragraph's primary direction.
*/ */
public float getSecondaryHorizontal(int offset) { public float getSecondaryHorizontal(int offset) {
return getHorizontal(offset, true, true); boolean trailing = primaryIsTrailingPrevious(offset);
return getHorizontal(offset, !trailing);
} }
private float getHorizontal(int offset, boolean trailing, boolean alt) { private float getHorizontal(int offset, boolean trailing) {
int line = getLineForOffset(offset); int line = getLineForOffset(offset);
return getHorizontal(offset, trailing, alt, line); return getHorizontal(offset, trailing, line);
} }
private float getHorizontal(int offset, boolean trailing, boolean alt, private float getHorizontal(int offset, boolean trailing, int line) {
int line) {
int start = getLineStart(line); int start = getLineStart(line);
int end = getLineVisibleEnd(line); int end = getLineVisibleEnd(line);
int dir = getParagraphDirection(line); int dir = getParagraphDirection(line);
@@ -562,7 +640,7 @@ public abstract class Layout {
} }
float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end,
dir, directions, trailing, alt, tab, tabs); dir, directions, trailing, tab, tabs);
if (offset > end) { if (offset > end) {
if (dir == DIR_RIGHT_TO_LEFT) if (dir == DIR_RIGHT_TO_LEFT)
@@ -738,7 +816,7 @@ public abstract class Layout {
} }
/** /**
* Get the character offset on the specfied line whose position is * Get the character offset on the specified line whose position is
* closest to the specified horizontal position. * closest to the specified horizontal position.
*/ */
public int getOffsetForHorizontal(int line, float horiz) { public int getOffsetForHorizontal(int line, float horiz) {
@@ -752,14 +830,13 @@ public abstract class Layout {
int best = min; int best = min;
float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
int here = min; for (int i = 0; i < dirs.mDirections.length; i += 2) {
for (int i = 0; i < dirs.mDirections.length; i++) { int here = min + dirs.mDirections[i];
int there = here + dirs.mDirections[i]; int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
int swap = ((i & 1) == 0) ? 1 : -1; int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
if (there > max) if (there > max)
there = max; there = max;
int high = there - 1 + 1, low = here + 1 - 1, guess; int high = there - 1 + 1, low = here + 1 - 1, guess;
while (high - low > 1) { while (high - low > 1) {
@@ -802,8 +879,6 @@ public abstract class Layout {
bestdist = dist; bestdist = dist;
best = here; best = here;
} }
here = there;
} }
float dist = Math.abs(getPrimaryHorizontal(max) - horiz); float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
@@ -882,207 +957,178 @@ public abstract class Layout {
return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
} }
/**
* Return the text offset that would be reached by moving left
* (possibly onto another line) from the specified offset.
*/
public int getOffsetToLeftOf(int offset) { public int getOffsetToLeftOf(int offset) {
int line = getLineForOffset(offset); return getOffsetToLeftRightOf(offset, true);
int start = getLineStart(line);
int end = getLineEnd(line);
Directions dirs = getLineDirections(line);
if (line != getLineCount() - 1)
end--;
float horiz = getPrimaryHorizontal(offset);
int best = offset;
float besth = Integer.MIN_VALUE;
int candidate;
candidate = TextUtils.getOffsetBefore(mText, offset);
if (candidate >= start && candidate <= end) {
float h = getPrimaryHorizontal(candidate);
if (h < horiz && h > besth) {
best = candidate;
besth = h;
}
} }
candidate = TextUtils.getOffsetAfter(mText, offset);
if (candidate >= start && candidate <= end) {
float h = getPrimaryHorizontal(candidate);
if (h < horiz && h > besth) {
best = candidate;
besth = h;
}
}
int here = start;
for (int i = 0; i < dirs.mDirections.length; i++) {
int there = here + dirs.mDirections[i];
if (there > end)
there = end;
float h = getPrimaryHorizontal(here);
if (h < horiz && h > besth) {
best = here;
besth = h;
}
candidate = TextUtils.getOffsetAfter(mText, here);
if (candidate >= start && candidate <= end) {
h = getPrimaryHorizontal(candidate);
if (h < horiz && h > besth) {
best = candidate;
besth = h;
}
}
candidate = TextUtils.getOffsetBefore(mText, there);
if (candidate >= start && candidate <= end) {
h = getPrimaryHorizontal(candidate);
if (h < horiz && h > besth) {
best = candidate;
besth = h;
}
}
here = there;
}
float h = getPrimaryHorizontal(end);
if (h < horiz && h > besth) {
best = end;
besth = h;
}
if (best != offset)
return best;
int dir = getParagraphDirection(line);
if (dir > 0) {
if (line == 0)
return best;
else
return getOffsetForHorizontal(line - 1, 10000);
} else {
if (line == getLineCount() - 1)
return best;
else
return getOffsetForHorizontal(line + 1, 10000);
}
}
/**
* Return the text offset that would be reached by moving right
* (possibly onto another line) from the specified offset.
*/
public int getOffsetToRightOf(int offset) { public int getOffsetToRightOf(int offset) {
int line = getLineForOffset(offset); return getOffsetToLeftRightOf(offset, false);
int start = getLineStart(line);
int end = getLineEnd(line);
Directions dirs = getLineDirections(line);
if (line != getLineCount() - 1)
end--;
float horiz = getPrimaryHorizontal(offset);
int best = offset;
float besth = Integer.MAX_VALUE;
int candidate;
candidate = TextUtils.getOffsetBefore(mText, offset);
if (candidate >= start && candidate <= end) {
float h = getPrimaryHorizontal(candidate);
if (h > horiz && h < besth) {
best = candidate;
besth = h;
}
} }
candidate = TextUtils.getOffsetAfter(mText, offset); // 1) The caret marks the leading edge of a character. The character
if (candidate >= start && candidate <= end) { // logically before it might be on a different level, and the active caret
float h = getPrimaryHorizontal(candidate); // position is on the character at the lower level. If that character
// was the previous character, the caret is on its trailing edge.
// 2) Take this character/edge and move it in the indicated direction.
// This gives you a new character and a new edge.
// 3) This position is between two visually adjacent characters. One of
// these might be at a lower level. The active position is on the
// character at the lower level.
// 4) If the active position is on the trailing edge of the character,
// the new caret position is the following logical character, else it
// is the character.
private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
int line = getLineForOffset(caret);
int lineStart = getLineStart(line);
int lineEnd = getLineEnd(line);
if (h > horiz && h < besth) { boolean paraIsRtl = getParagraphDirection(line) == -1;
best = candidate; int[] runs = getLineDirections(line).mDirections;
besth = h;
}
}
int here = start; int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
for (int i = 0; i < dirs.mDirections.length; i++) { boolean trailing = false;
int there = here + dirs.mDirections[i];
if (there > end)
there = end;
float h = getPrimaryHorizontal(here); if (caret == lineStart) {
runIndex = -2;
if (h > horiz && h < besth) { } else if (caret == lineEnd) {
best = here; runIndex = runs.length;
besth = h;
}
candidate = TextUtils.getOffsetAfter(mText, here);
if (candidate >= start && candidate <= end) {
h = getPrimaryHorizontal(candidate);
if (h > horiz && h < besth) {
best = candidate;
besth = h;
}
}
candidate = TextUtils.getOffsetBefore(mText, there);
if (candidate >= start && candidate <= end) {
h = getPrimaryHorizontal(candidate);
if (h > horiz && h < besth) {
best = candidate;
besth = h;
}
}
here = there;
}
float h = getPrimaryHorizontal(end);
if (h > horiz && h < besth) {
best = end;
besth = h;
}
if (best != offset)
return best;
int dir = getParagraphDirection(line);
if (dir > 0) {
if (line == getLineCount() - 1)
return best;
else
return getOffsetForHorizontal(line + 1, -10000);
} else { } else {
if (line == 0) // First, get information about the run containing the character with
return best; // the active caret.
else for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
return getOffsetForHorizontal(line - 1, -10000); runStart = lineStart + runs[runIndex];
if (caret >= runStart) {
runLimit = runStart + (runs[runIndex+1] & RUN_LENGTH_MASK);
if (runLimit > lineEnd) {
runLimit = lineEnd;
} }
if (caret < runLimit) {
runLevel = (runs[runIndex+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
if (caret == runStart) {
// The caret is on a run boundary, see if we should
// use the position on the trailing edge of the previous
// logical character instead.
int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
int pos = caret - 1;
for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
prevRunStart = lineStart + runs[prevRunIndex];
if (pos >= prevRunStart) {
prevRunLimit = prevRunStart + (runs[prevRunIndex+1] & RUN_LENGTH_MASK);
if (prevRunLimit > lineEnd) {
prevRunLimit = lineEnd;
}
if (pos < prevRunLimit) {
prevRunLevel = (runs[prevRunIndex+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
if (prevRunLevel < runLevel) {
// Start from logically previous character.
runIndex = prevRunIndex;
runLevel = prevRunLevel;
runStart = prevRunStart;
runLimit = prevRunLimit;
trailing = true;
break;
}
}
}
}
}
break;
}
}
}
// caret might be = lineEnd. This is generally a space or paragraph
// separator and has an associated run, but might be the end of
// text, in which case it doesn't. If that happens, we ran off the
// end of the run list, and runIndex == runs.length. In this case,
// we are at a run boundary so we skip the below test.
if (runIndex != runs.length) {
boolean rtlRun = (runLevel & 0x1) != 0;
boolean advance = toLeft == rtlRun;
if (caret != (advance ? runLimit : runStart) || advance != trailing) {
// Moving within or into the run, so we can move logically.
newCaret = getOffsetBeforeAfter(caret, advance);
// If the new position is internal to the run, we're at the strong
// position already so we're finished.
if (newCaret != (advance ? runLimit : runStart)) {
return newCaret;
}
}
}
}
// If newCaret is -1, we're starting at a run boundary and crossing
// into another run. Otherwise we've arrived at a run boundary, and
// need to figure out which character to attach to. Note we might
// need to run this twice, if we cross a run boundary and end up at
// another run boundary.
while (true) {
boolean advance = toLeft == paraIsRtl;
int otherRunIndex = runIndex + (advance ? 2 : -2);
if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
int otherRunStart = lineStart + runs[otherRunIndex];
int otherRunLimit = otherRunStart + (runs[otherRunIndex+1] & RUN_LENGTH_MASK);
if (otherRunLimit > lineEnd) {
otherRunLimit = lineEnd;
}
int otherRunLevel = runs[otherRunIndex+1] >>> RUN_LEVEL_SHIFT & RUN_LEVEL_MASK;
boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
advance = toLeft == otherRunIsRtl;
if (newCaret == -1) {
newCaret = getOffsetBeforeAfter(advance ? otherRunStart : otherRunLimit, advance);
if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
// Crossed and ended up at a new boundary, repeat a second and final time.
runIndex = otherRunIndex;
runLevel = otherRunLevel;
continue;
}
break;
}
// The new caret is at a boundary.
if (otherRunLevel < runLevel) {
// The strong character is in the other run.
newCaret = advance ? otherRunStart : otherRunLimit;
}
break;
}
if (newCaret == -1) {
// We're walking off the end of the line. The paragraph
// level is always equal to or lower than any internal level, so
// the boundaries get the strong caret.
newCaret = getOffsetBeforeAfter(caret, advance);
break;
}
// Else we've arrived at the end of the line. That's a strong position.
// We might have arrived here by crossing over a run with no internal
// breaks and dropping out of the above loop before advancing one final
// time, so reset the caret.
// Note, we use '<=' below to handle a situation where the only run
// on the line is a counter-directional run. If we're not advancing,
// we can end up at the 'lineEnd' position but the caret we want is at
// the lineStart.
if (newCaret <= lineEnd) {
newCaret = advance ? lineEnd : lineStart;
}
break;
}
return newCaret;
}
// utility, maybe just roll into the above.
private int getOffsetBeforeAfter(int offset, boolean after) {
if (after) {
return TextUtils.getOffsetAfter(mText, offset);
}
return TextUtils.getOffsetBefore(mText, offset);
} }
private int getOffsetAtStartOf(int offset) { private int getOffsetAtStartOf(int offset) {
// XXX this probably should skip local reorderings and
// zero-width characters, look at callers
if (offset == 0) if (offset == 0)
return 0; return 0;
@@ -1204,9 +1250,10 @@ public abstract class Layout {
if (lineend > linestart && mText.charAt(lineend - 1) == '\n') if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
lineend--; lineend--;
int here = linestart; for (int i = 0; i < dirs.mDirections.length; i += 2) {
for (int i = 0; i < dirs.mDirections.length; i++) { int here = linestart + dirs.mDirections[i];
int there = here + dirs.mDirections[i]; int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
if (there > lineend) if (there > lineend)
there = lineend; there = lineend;
@@ -1215,14 +1262,12 @@ public abstract class Layout {
int en = Math.min(end, there); int en = Math.min(end, there);
if (st != en) { if (st != en) {
float h1 = getHorizontal(st, false, false, line); float h1 = getHorizontal(st, false, line);
float h2 = getHorizontal(en, true, false, line); float h2 = getHorizontal(en, true, line);
dest.addRect(h1, top, h2, bottom, Path.Direction.CW); dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
} }
} }
here = there;
} }
} }
@@ -1395,27 +1440,31 @@ public abstract class Layout {
float h = 0; float h = 0;
int here = 0; int lastRunIndex = directions.mDirections.length - 2;
for (int i = 0; i < directions.mDirections.length; i++) { for (int i = 0; i < directions.mDirections.length; i += 2) {
int there = here + directions.mDirections[i]; int here = start + directions.mDirections[i];
if (there > end - start) int there = here + (directions.mDirections[i+1] & RUN_LENGTH_MASK);
there = end - start; boolean runIsRtl = (directions.mDirections[i+1] & RUN_RTL_FLAG) != 0;
if (there > end)
there = end;
int segstart = here; int segstart = here;
for (int j = hasTabs ? here : there; j <= there; j++) { for (int j = hasTabs ? here : there; j <= there; j++) {
if (j == there || buf[j] == '\t') { int pj = j - start;
if (j == there || buf[pj] == '\t') {
h += Styled.drawText(canvas, text, h += Styled.drawText(canvas, text,
start + segstart, start + j, segstart, j,
dir, (i & 1) != 0, x + h, dir, runIsRtl, x + h,
top, y, bottom, paint, workPaint, top, y, bottom, paint, workPaint,
start + j != end); i != lastRunIndex || j != end);
if (j != there && buf[j] == '\t') if (j != there)
h = dir * nextTab(text, start, end, h * dir, parspans); h = dir * nextTab(text, start, end, h * dir, parspans);
segstart = j + 1; segstart = j + 1;
} else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) { } else if (hasTabs && buf[pj] >= 0xD800 && buf[pj] <= 0xDFFF && j + 1 < there) {
int emoji = Character.codePointAt(buf, j); int emoji = Character.codePointAt(buf, pj);
if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
Bitmap bm = EMOJI_FACTORY. Bitmap bm = EMOJI_FACTORY.
@@ -1423,10 +1472,10 @@ public abstract class Layout {
if (bm != null) { if (bm != null) {
h += Styled.drawText(canvas, text, h += Styled.drawText(canvas, text,
start + segstart, start + j, segstart, j,
dir, (i & 1) != 0, x + h, dir, runIsRtl, x + h,
top, y, bottom, paint, workPaint, top, y, bottom, paint, workPaint,
start + j != end); i != lastRunIndex || j != end);
if (mEmojiRect == null) { if (mEmojiRect == null) {
mEmojiRect = new RectF(); mEmojiRect = new RectF();
@@ -1434,7 +1483,7 @@ public abstract class Layout {
workPaint.set(paint); workPaint.set(paint);
Styled.measureText(paint, workPaint, text, Styled.measureText(paint, workPaint, text,
start + j, start + j + 1, j, j + 1,
null); null);
float bitmapHeight = bm.getHeight(); float bitmapHeight = bm.getHeight();
@@ -1454,21 +1503,24 @@ public abstract class Layout {
} }
} }
} }
here = there;
} }
if (hasTabs) if (hasTabs)
TextUtils.recycle(buf); TextUtils.recycle(buf);
} }
private static float measureText(TextPaint paint, /**
* Get the distance from the margin to the requested edge of the character
* at offset on the line from start to end. Trailing indicates the edge
* should be the trailing edge of the character at offset-1.
*/
/* package */ static float measureText(TextPaint paint,
TextPaint workPaint, TextPaint workPaint,
CharSequence text, CharSequence text,
int start, int offset, int end, int start, int offset, int end,
int dir, Directions directions, int dir, Directions directions,
boolean trailing, boolean alt, boolean trailing, boolean hasTabs,
boolean hasTabs, Object[] tabs) { Object[] tabs) {
char[] buf = null; char[] buf = null;
if (hasTabs) { if (hasTabs) {
@@ -1478,19 +1530,19 @@ public abstract class Layout {
float h = 0; float h = 0;
if (alt) { int target = trailing ? offset - 1 : offset;
if (dir == DIR_RIGHT_TO_LEFT) if (target < start) {
trailing = !trailing; return 0;
} }
int here = 0; int[] runs = directions.mDirections;
for (int i = 0; i < directions.mDirections.length; i++) { for (int i = 0; i < runs.length; i += 2) {
if (alt) int here = start + runs[i];
trailing = !trailing; int there = here + (runs[i+1] & RUN_LENGTH_MASK);
if (there > end) {
int there = here + directions.mDirections[i]; there = end;
if (there > end - start) }
there = end - start; boolean runIsRtl = (runs[i+1] & RUN_RTL_FLAG) != 0;
int segstart = here; int segstart = here;
for (int j = hasTabs ? here : there; j <= there; j++) { for (int j = hasTabs ? here : there; j <= there; j++) {
@@ -1498,11 +1550,11 @@ public abstract class Layout {
Bitmap bm = null; Bitmap bm = null;
if (hasTabs && j < there) { if (hasTabs && j < there) {
codept = buf[j]; codept = buf[j - start];
} }
if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) {
codept = Character.codePointAt(buf, j); codept = Character.codePointAt(buf, j - start);
if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
@@ -1512,33 +1564,34 @@ public abstract class Layout {
if (j == there || codept == '\t' || bm != null) { if (j == there || codept == '\t' || bm != null) {
float segw; float segw;
if (offset < start + j || boolean inSegment = target >= segstart && target < j;
(trailing && offset <= start + j)) {
if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { if (inSegment) {
if (dir == DIR_LEFT_TO_RIGHT && !runIsRtl) {
h += Styled.measureText(paint, workPaint, text, h += Styled.measureText(paint, workPaint, text,
start + segstart, offset, segstart, offset,
null); null);
return h; return h;
} }
if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { if (dir == DIR_RIGHT_TO_LEFT && runIsRtl) {
h -= Styled.measureText(paint, workPaint, text, h -= Styled.measureText(paint, workPaint, text,
start + segstart, offset, segstart, offset,
null); null);
return h; return h;
} }
} }
// XXX Style.measureText assumes LTR?
segw = Styled.measureText(paint, workPaint, text, segw = Styled.measureText(paint, workPaint, text,
start + segstart, start + j, segstart, j,
null); null);
if (offset < start + j || if (inSegment) {
(trailing && offset <= start + j)) {
if (dir == DIR_LEFT_TO_RIGHT) { if (dir == DIR_LEFT_TO_RIGHT) {
h += segw - Styled.measureText(paint, workPaint, h += segw - Styled.measureText(paint, workPaint,
text, text,
start + segstart, segstart,
offset, null); offset, null);
return h; return h;
} }
@@ -1546,7 +1599,7 @@ public abstract class Layout {
if (dir == DIR_RIGHT_TO_LEFT) { if (dir == DIR_RIGHT_TO_LEFT) {
h -= segw - Styled.measureText(paint, workPaint, h -= segw - Styled.measureText(paint, workPaint,
text, text,
start + segstart, segstart,
offset, null); offset, null);
return h; return h;
} }
@@ -1557,11 +1610,15 @@ public abstract class Layout {
else else
h += segw; h += segw;
if (j != there && buf[j] == '\t') { if (j != there && buf[j - start] == '\t') {
if (offset == start + j) if (offset == j)
return h; return h;
h = dir * nextTab(text, start, end, h * dir, tabs); h = dir * nextTab(text, start, end, h * dir, tabs);
if (target == j) {
return h;
}
} }
if (bm != null) { if (bm != null) {
@@ -1569,7 +1626,7 @@ public abstract class Layout {
Styled.measureText(paint, workPaint, text, Styled.measureText(paint, workPaint, text,
j, j + 2, null); j, j + 2, null);
float wid = (float) bm.getWidth() * float wid = bm.getWidth() *
-workPaint.ascent() / bm.getHeight(); -workPaint.ascent() / bm.getHeight();
if (dir == DIR_RIGHT_TO_LEFT) { if (dir == DIR_RIGHT_TO_LEFT) {
@@ -1584,8 +1641,6 @@ public abstract class Layout {
segstart = j + 1; segstart = j + 1;
} }
} }
here = there;
} }
if (hasTabs) if (hasTabs)
@@ -1652,6 +1707,8 @@ public abstract class Layout {
if (pos == len || codept == '\t' || bm != null) { if (pos == len || codept == '\t' || bm != null) {
workPaint.baselineShift = 0; workPaint.baselineShift = 0;
// XXX Styled.measureText assumes the run direction is LTR,
// but it might not be. Check this.
width += Styled.measureText(paint, workPaint, text, width += Styled.measureText(paint, workPaint, text,
start + lastPos, start + pos, start + lastPos, start + pos,
fm); fm);
@@ -1683,7 +1740,7 @@ public abstract class Layout {
Styled.measureText(paint, workPaint, text, Styled.measureText(paint, workPaint, text,
start + pos, start + pos + 1, null); start + pos, start + pos + 1, null);
width += (float) bm.getWidth() * width += bm.getWidth() *
-workPaint.ascent() / bm.getHeight(); -workPaint.ascent() / bm.getHeight();
// Since we had an emoji, we bump past the second half // Since we had an emoji, we bump past the second half
@@ -1804,23 +1861,22 @@ public abstract class Layout {
/** /**
* Stores information about bidirectional (left-to-right or right-to-left) * Stores information about bidirectional (left-to-right or right-to-left)
* text within the layout of a line. TODO: This work is not complete * text within the layout of a line.
* or correct and will be fleshed out in a later revision.
*/ */
public static class Directions { public static class Directions {
private short[] mDirections; // Directions represents directional runs within a line of text.
// Runs are pairs of ints listed in visual order, starting from the
// leading margin. The first int of each pair is the offset from
// the first character of the line to the start of the run. The
// second int represents both the length and level of the run.
// The length is in the lower bits, accessed by masking with
// DIR_LENGTH_MASK. The level is in the higher bits, accessed
// by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
// To simply test for an RTL direction, test the bit using
// DIR_RTL_FLAG, if set then the direction is rtl.
// The values in mDirections are the offsets from the first character /* package */ int[] mDirections;
// in the line to the next flip in direction. Runs at even indices /* package */ Directions(int[] dirs) {
// are left-to-right, the others are right-to-left. So, for example,
// a line that starts with a right-to-left run has 0 at mDirections[0],
// since the 'first' (ltr) run is zero length.
//
// The code currently assumes that each run is adjacent to the previous
// one, progressing in the base line direction. This isn't sufficient
// to handle nested runs, for example numeric text in an rtl context
// in an ltr paragraph.
/* package */ Directions(short[] dirs) {
mDirections = dirs; mDirections = dirs;
} }
} }
@@ -1942,6 +1998,11 @@ public abstract class Layout {
/* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
/* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
/* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
/* package */ static final int RUN_LEVEL_SHIFT = 26;
/* package */ static final int RUN_LEVEL_MASK = 0x3f;
/* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
public enum Alignment { public enum Alignment {
ALIGN_NORMAL, ALIGN_NORMAL,
ALIGN_OPPOSITE, ALIGN_OPPOSITE,
@@ -1953,9 +2014,8 @@ public abstract class Layout {
private static final int TAB_INCREMENT = 20; private static final int TAB_INCREMENT = 20;
/* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
new Directions(new short[] { 32767 }); new Directions(new int[] { 0, RUN_LENGTH_MASK });
/* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
new Directions(new short[] { 0, 32767 }); new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
} }

View File

@@ -224,7 +224,7 @@ extends Layout
boolean easy = true; boolean easy = true;
boolean altered = false; boolean altered = false;
int dir = DEFAULT_DIR; // XXX int dir = DEFAULT_DIR; // XXX pass value in
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (chs[i] >= FIRST_RIGHT_TO_LEFT) { if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
@@ -253,7 +253,8 @@ extends Layout
if (!easy) { if (!easy) {
// XXX put override flags, etc. into chdirs // 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 // 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'; private static final char FIRST_CJK = '\u2E80';
/** /**
* Returns true if the specified character is one of those specified * Returns true if the specified character is one of those specified
@@ -1062,49 +830,123 @@ extends Layout
if (tab) if (tab)
lines[off + TAB] |= TAB_MASK; lines[off + TAB] |= TAB_MASK;
{
lines[off + DIR] |= dir << DIR_SHIFT; lines[off + DIR] |= dir << DIR_SHIFT;
Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; // easy means all chars < the first RTL, so no emoji, no nothing
int count = 0; // 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) { if (easy) {
for (int k = start; k < end; k++) { mLineDirections[j] = linedirs;
if (chdirs[k - pstart] != cur) {
count++;
cur = chdirs[k - pstart];
}
}
}
Directions linedirs;
if (count == 0) {
linedirs = DIRS_ALL_LEFT_TO_RIGHT;
} else { } else {
short[] ld = new short[count + 1]; int startOff = start - pstart;
int baseLevel = dir == DIR_LEFT_TO_RIGHT ? 0 : 1;
cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; int curLevel = chdirs[startOff];
count = 0; int minLevel = curLevel;
int here = start; int runCount = 1;
for (int i = start + 1; i < end; ++i) {
for (int k = start; k < end; k++) { int level = chdirs[i - pstart];
if (chdirs[k - pstart] != cur) { if (level != curLevel) {
// XXX check to make sure we don't curLevel = level;
// overflow short ++runCount;
ld[count++] = (short) (k - here);
cur = chdirs[k - pstart];
here = k;
} }
} }
ld[count] = (short) (end - here); // 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);
if (count == 1 && ld[0] == 0) { if (ch == '\n') {
--visEnd;
break;
}
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; linedirs = DIRS_ALL_RIGHT_TO_LEFT;
} else {
linedirs = new Directions(ld);
} }
// we're done, only one run on this line
} else {
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);
}
}
// 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 {
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; mLineDirections[j] = linedirs;

View File

@@ -22,7 +22,7 @@ import android.util.Log;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
* Tests StaticLayout bidi implementation. * Quick check of native bidi implementation.
*/ */
public class StaticLayoutBidiTest extends TestCase { public class StaticLayoutBidiTest extends TestCase {
@@ -41,73 +41,47 @@ public class StaticLayoutBidiTest extends TestCase {
//@SmallTest //@SmallTest
public void testAllLtr() { public void testAllLtr() {
expectBidi(REQ_DL, "a test", "000000", L); expectNativeBidi(REQ_DL, "a test", "000000", L);
} }
//@SmallTest //@SmallTest
public void testLtrRtl() { public void testLtrRtl() {
expectBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L); expectNativeBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L);
} }
//@SmallTest //@SmallTest
public void testAllRtl() { 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 //@SmallTest
public void testRtlLtr() { public void testRtlLtr() {
expectBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111000", R); expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
} }
//@SmallTest //@SmallTest
public void testRAllLtr() { public void testRAllLtr() {
expectBidi(REQ_R, "a test", "000000", R); expectNativeBidi(REQ_R, "a test", "222222", R);
} }
//@SmallTest //@SmallTest
public void testRLtrRtl() { public void testRLtrRtl() {
expectBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "0001111", R); expectNativeBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "2221111", R);
} }
//@SmallTest //@SmallTest
public void testLAllRtl() { 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 //@SmallTest
public void testLRtlLtr() { public void testLRtlLtr() {
expectBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L); expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
}
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);
} }
//@SmallTest //@SmallTest
public void testNativeBidi() { public void testNativeBidi() {
// native bidi returns levels, not simply directions expectNativeBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L);
expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R);
} }
private void expectNativeBidi(int dir, String text, 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(); return new LayoutBuilder();
} }
private static class LayoutBuilder { /* package */ static class LayoutBuilder {
String text = "This is a test"; String text = "This is a test";
TextPaint paint = new TextPaint(); // default TextPaint paint = new TextPaint(); // default
int width = 100; int width = 100;