diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java index e4f934e721732..eacd40d0db29e 100644 --- a/core/java/android/text/AndroidBidi.java +++ b/core/java/android/text/AndroidBidi.java @@ -16,6 +16,8 @@ package android.text; +import android.text.Layout.Directions; + /** * Access the ICU bidi implementation. * @hide @@ -44,5 +46,132 @@ package android.text; return result; } + /** + * Returns run direction information for a line within a paragraph. + * + * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or + * Layout.DIR_RIGHT_TO_LEFT + * @param levels levels as returned from {@link #bidi} + * @param lstart start of the line in the levels array + * @param chars the character array (used to determine whitespace) + * @param cstart the start of the line in the chars array + * @param len the length of the line + * @return the directions + */ + public static Directions directions(int dir, byte[] levels, int lstart, + char[] chars, int cstart, int len) { + + int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1; + int curLevel = levels[lstart]; + int minLevel = curLevel; + int runCount = 1; + for (int i = lstart + 1, e = lstart + len; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + ++runCount; + } + } + + // add final run for trailing counter-directional whitespace + int visLen = len; + if ((curLevel & 1) != (baseLevel & 1)) { + // look for visible end + while (--visLen >= 0) { + char ch = chars[cstart + visLen]; + + if (ch == '\n') { + --visLen; + break; + } + + if (ch != ' ' && ch != '\t') { + break; + } + } + ++visLen; + if (visLen != len) { + ++runCount; + } + } + + if (runCount == 1 && minLevel == baseLevel) { + // we're done, only one run on this line + if ((minLevel & 1) != 0) { + return Layout.DIRS_ALL_RIGHT_TO_LEFT; + } + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + int[] ld = new int[runCount * 2]; + int maxLevel = minLevel; + int levelBits = minLevel << Layout.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 = lstart; + curLevel = minLevel; + for (int i = lstart, e = lstart + visLen; i < e; ++i) { + int level = levels[i]; + 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 - lstart; + levelBits = curLevel << Layout.RUN_LEVEL_SHIFT; + prev = i; + } + } + ld[n] = (lstart + visLen - prev) | levelBits; + if (visLen < len) { + ld[++n] = visLen; + ld[++n] = (len - visLen) | (baseLevel << Layout.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 (levels[ld[i]] >= level) { + int e = i + 2; + while (e < ld.length && levels[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; + } + } + } + } + return new Directions(ld); + } + private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo); } \ No newline at end of file diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 944f7354fdb2c..9309b05c6fdaa 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * width because the width that was passed in was for the * full text, not the ellipsized form. */ - synchronized (sTemp) { - mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - source, 0, source.length(), - null))); - } + TextLine line = TextLine.obtain(); + line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + mMax = (int) FloatMath.ceil(line.metrics(null)); + TextLine.recycle(line); } if (includepad) { @@ -276,14 +276,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (fm == null) { fm = new Metrics(); } - - int wid; - synchronized (sTemp) { - wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - text, 0, text.length(), fm))); - } - fm.width = wid; + TextLine line = TextLine.obtain(); + line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + fm.width = (int) FloatMath.ceil(line.metrics(fm)); + TextLine.recycle(line); + return fm; } else { return null; @@ -389,7 +388,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public static class Metrics extends Paint.FontMetricsInt { public int width; - + @Override public String toString() { return super.toString() + " width=" + width; } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index ff1f2a60f36ff..3b8f2950f326d 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -19,12 +19,10 @@ package android.text; import com.android.internal.util.ArrayUtils; import android.emoji.EmojiFactory; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; -import android.graphics.RectF; import android.text.method.TextKeyListener; import android.text.style.AlignmentSpan; import android.text.style.LeadingMarginSpan; @@ -60,9 +58,7 @@ public abstract class Layout { MIN_EMOJI = -1; MAX_EMOJI = -1; } - }; - - private RectF mEmojiRect; + } /** * Return how wide a layout must be in order to display the @@ -91,8 +87,8 @@ public abstract class Layout { next = end; // note, omits trailing paragraph char - float w = measureText(paint, workPaint, - source, i, next, null, true, null); + float w = measurePara(paint, workPaint, + source, i, next, true, null); if (w > need) need = w; @@ -122,6 +118,15 @@ public abstract class Layout { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); + // Ensure paint doesn't have baselineShift set. + // While normally we don't modify the paint the user passed in, + // we were already doing this in Styled.drawUniformRun with both + // baselineShift and bgColor. We probably should reevaluate bgColor. + if (paint != null) { + paint.bgColor = 0; + paint.baselineShift = 0; + } + mText = text; mPaint = paint; mWorkPaint = new TextPaint(); @@ -262,6 +267,7 @@ public abstract class Layout { Alignment align = mAlignment; + TextLine tl = TextLine.obtain(); // Next draw the lines, one at a time. // the baseline is the top of the following line minus the current // line's descent. @@ -373,11 +379,11 @@ public abstract class Layout { // XXX: assumes there's nothing additional to be done c.drawText(buf, start, end, x, lbaseline, paint); } else { - drawText(c, buf, start, end, dir, directions, - x, ltop, lbaseline, lbottom, paint, mWorkPaint, - hasTab, spans); + tl.set(paint, buf, start, end, dir, directions, hasTab, spans); + tl.draw(c, x, ltop, lbaseline, lbottom); } } + TextLine.recycle(tl); } /** @@ -639,7 +645,7 @@ public abstract class Layout { private float getHorizontal(int offset, boolean trailing, int line) { int start = getLineStart(line); - int end = getLineVisibleEnd(line); + int end = getLineEnd(line); int dir = getParagraphDirection(line); boolean tab = getLineContainsTab(line); Directions directions = getLineDirections(line); @@ -649,17 +655,10 @@ public abstract class Layout { tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); } - float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, - dir, directions, trailing, tab, tabs); - - if (offset > end) { - if (dir == DIR_RIGHT_TO_LEFT) - wid -= measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - else - wid += measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - } + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, tab, tabs); + float wid = tl.measure(offset - start, trailing, null); + TextLine.recycle(tl); Alignment align = getParagraphAlignment(line); int left = getParagraphLeft(line); @@ -761,21 +760,15 @@ public abstract class Layout { private float getLineMax(int line, Object[] tabs, boolean full) { int start = getLineStart(line); - int end; + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + boolean hasTabs = getLineContainsTab(line); + Directions directions = getLineDirections(line); - if (full) { - end = getLineEnd(line); - } else { - end = getLineVisibleEnd(line); - } - boolean tab = getLineContainsTab(line); - - if (tabs == null && tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); - } - - return measureText(mPaint, mWorkPaint, - mText, start, end, null, tab, tabs); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, 1, directions, hasTabs, tabs); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; } /** @@ -975,165 +968,49 @@ public abstract class Layout { return getOffsetToLeftRightOf(offset, false); } - // 1) The caret marks the leading edge of a character. The character - // logically before it might be on a different level, and the active caret - // 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); + int lineDir = getParagraphDirection(line); - boolean paraIsRtl = getParagraphDirection(line) == -1; - int[] runs = getLineDirections(line).mDirections; - - int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; - boolean trailing = false; - - if (caret == lineStart) { - runIndex = -2; - } else if (caret == lineEnd) { - runIndex = runs.length; - } else { - // First, get information about the run containing the character with - // the active caret. - for (runIndex = 0; runIndex < runs.length; runIndex += 2) { - 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; - } - } - } - } + boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); + if (caret == (advance ? lineEnd : lineStart)) { + // walking off line, so look at the line we're headed to + if (caret == lineStart) { + if (line > 0) { + --line; + } else { + return caret; // at very start, don't move + } + } else { + if (line < getLineCount() - 1) { + ++line; + } else { + return caret; // at very end, don't move } - 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; - } - } - } + lineStart = getLineStart(line); + lineEnd = getLineEnd(line); + int newDir = getParagraphDirection(line); + if (newDir != lineDir) { + // unusual case. we want to walk onto the line, but it runs + // in a different direction than this one, so we fake movement + // in the opposite direction. + toLeft = !toLeft; + lineDir = newDir; + } } - // 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; + Directions directions = getLineDirections(line); - 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); + TextLine tl = TextLine.obtain(); + // XXX: we don't care about tabs + tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); + caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); + tl = TextLine.recycle(tl); + return caret; } private int getOffsetAtStartOf(int offset) { @@ -1427,373 +1304,28 @@ public abstract class Layout { return right; } - private void drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, Directions directions, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean hasTabs, Object[] parspans) { - char[] buf; - if (!hasTabs) { - if (directions == DIRS_ALL_LEFT_TO_RIGHT) { - if (DEBUG) { - Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); - } - Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); - return; + /* package */ + static float measurePara(TextPaint paint, TextPaint workPaint, + CharSequence text, int start, int end, boolean hasTabs, + Object[] tabs) { + + MeasuredText mt = MeasuredText.obtain(); + TextLine tl = TextLine.obtain(); + try { + mt.setPara(text, start, end, DIR_REQUEST_LTR); + Directions directions; + if (mt.mEasy){ + directions = DIRS_ALL_LEFT_TO_RIGHT; + } else { + directions = AndroidBidi.directions(mt.mDir, mt.mLevels, + 0, mt.mChars, 0, mt.mLen); } - buf = null; - } else { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); + tl.set(paint, text, start, end, 1, directions, hasTabs, tabs); + return tl.metrics(null); + } finally { + TextLine.recycle(tl); + MeasuredText.recycle(mt); } - - float h = 0; - - int lastRunIndex = directions.mDirections.length - 2; - for (int i = 0; i < directions.mDirections.length; i += 2) { - int here = start + directions.mDirections[i]; - int there = here + (directions.mDirections[i+1] & RUN_LENGTH_MASK); - boolean runIsRtl = (directions.mDirections[i+1] & RUN_RTL_FLAG) != 0; - - if (there > end) - there = end; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - int pj = j - start; - if (j == there || buf[pj] == '\t') { - h += Styled.drawText(canvas, text, - segstart, j, - dir, runIsRtl, x + h, - top, y, bottom, paint, workPaint, - i != lastRunIndex || j != end); - - if (j != there) - h = dir * nextTab(text, start, end, h * dir, parspans); - - segstart = j + 1; - } else if (hasTabs && buf[pj] >= 0xD800 && buf[pj] <= 0xDFFF && j + 1 < there) { - int emoji = Character.codePointAt(buf, pj); - - if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { - Bitmap bm = EMOJI_FACTORY. - getBitmapFromAndroidPua(emoji); - - if (bm != null) { - h += Styled.drawText(canvas, text, - segstart, j, - dir, runIsRtl, x + h, - top, y, bottom, paint, workPaint, - i != lastRunIndex || j != end); - - if (mEmojiRect == null) { - mEmojiRect = new RectF(); - } - - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - j, j + 1, - null); - - float bitmapHeight = bm.getHeight(); - float textHeight = -workPaint.ascent(); - float scale = textHeight / bitmapHeight; - float width = bm.getWidth() * scale; - - mEmojiRect.set(x + h, y - textHeight, - x + h + width, y); - - canvas.drawBitmap(bm, null, mEmojiRect, paint); - h += width; - - j++; - segstart = j + 1; - } - } - } - } - } - - if (hasTabs) - TextUtils.recycle(buf); - } - - /** - * 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, - CharSequence text, - int start, int offset, int end, - int dir, Directions directions, - boolean trailing, boolean hasTabs, - Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - int target = trailing ? offset - 1 : offset; - if (target < start) { - return 0; - } - - int[] runs = directions.mDirections; - for (int i = 0; i < runs.length; i += 2) { - int here = start + runs[i]; - int there = here + (runs[i+1] & RUN_LENGTH_MASK); - if (there > end) { - there = end; - } - boolean runIsRtl = (runs[i+1] & RUN_RTL_FLAG) != 0; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && j < there) { - codept = buf[j - start]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { - codept = Character.codePointAt(buf, j - start); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (j == there || codept == '\t' || bm != null) { - float segw; - - boolean inSegment = target >= segstart && target < j; - - if (inSegment) { - if (dir == DIR_LEFT_TO_RIGHT && !runIsRtl) { - h += Styled.measureText(paint, workPaint, text, - segstart, offset, - null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT && runIsRtl) { - h -= Styled.measureText(paint, workPaint, text, - segstart, offset, - null); - return h; - } - } - - // XXX Style.measureText assumes LTR? - segw = Styled.measureText(paint, workPaint, text, - segstart, j, - null); - - if (inSegment) { - if (dir == DIR_LEFT_TO_RIGHT) { - h += segw - Styled.measureText(paint, workPaint, - text, - segstart, - offset, null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= segw - Styled.measureText(paint, workPaint, - text, - segstart, - offset, null); - return h; - } - } - - if (dir == DIR_RIGHT_TO_LEFT) - h -= segw; - else - h += segw; - - if (j != there && buf[j - start] == '\t') { - if (offset == j) - return h; - - h = dir * nextTab(text, start, end, h * dir, tabs); - - if (target == j) { - return h; - } - } - - if (bm != null) { - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - j, j + 2, null); - - float wid = bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= wid; - } else { - h += wid; - } - - j++; - } - - segstart = j + 1; - } - } - } - - if (hasTabs) - TextUtils.recycle(buf); - - return h; - } - - /** - * Measure width of a run of text on a single line that is known to all be - * in the same direction as the paragraph base direction. Returns the width, - * and the line metrics in fm if fm is not null. - * - * @param paint the paint for the text; will not be modified - * @param workPaint paint available for modification - * @param text text - * @param start start of the line - * @param end limit of the line - * @param fm object to return integer metrics in, can be null - * @param hasTabs true if it is known that the line has tabs - * @param tabs tab position information - * @return the width of the text from start to end - */ - /* package */ static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int end, - Paint.FontMetricsInt fm, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - int len = end - start; - - int lastPos = 0; - float width = 0; - int ascent = 0, descent = 0, top = 0, bottom = 0; - - if (fm != null) { - fm.ascent = 0; - fm.descent = 0; - } - - for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && pos < len) { - codept = buf[pos]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { - codept = Character.codePointAt(buf, pos); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (pos == len || codept == '\t' || bm != null) { - 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, - start + lastPos, start + pos, - fm); - - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; - } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; - } - } - - if (pos != len) { - if (bm == null) { - // no emoji, must have hit a tab - width = nextTab(text, start, end, width, tabs); - } else { - // This sets up workPaint with the font on the emoji - // text, so that we can extract the ascent and scale. - - // We can't use the result of the previous call to - // measureText because the emoji might have its own style. - // We have to initialize workPaint here because if the - // text is unstyled measureText might not use workPaint - // at all. - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + pos, start + pos + 1, null); - - width += bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - // Since we had an emoji, we bump past the second half - // of the surrogate pair. - pos++; - } - } - - if (fm != null) { - if (fm.ascent < ascent) { - ascent = fm.ascent; - } - if (fm.descent > descent) { - descent = fm.descent; - } - - if (fm.top < top) { - top = fm.top; - } - if (fm.bottom > bottom) { - bottom = fm.bottom; - } - - // No need to take bitmap height into account here, - // since it is scaled to match the text height. - } - - lastPos = pos + 1; - } - } - - if (fm != null) { - fm.ascent = ascent; - fm.descent = descent; - fm.top = top; - fm.bottom = bottom; - } - - if (hasTabs) - TextUtils.recycle(buf); - - return width; } /** @@ -1898,6 +1430,7 @@ public abstract class Layout { * line is ellipsized, not getLineStart().) */ public abstract int getEllipsisStart(int line); + /** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java new file mode 100644 index 0000000000000..e3a113da2de30 --- /dev/null +++ b/core/java/android/text/MeasuredText.java @@ -0,0 +1,250 @@ +/* + * 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 com.android.internal.util.ArrayUtils; + +import android.graphics.Paint; +import android.icu.text.ArabicShaping; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * @hide + */ +class MeasuredText { + /* package */ CharSequence mText; + /* package */ int mTextStart; + /* package */ float[] mWidths; + /* package */ char[] mChars; + /* package */ byte[] mLevels; + /* package */ int mDir; + /* package */ boolean mEasy; + /* package */ int mLen; + private int mPos; + private float[] mWorkWidths; // temp buffer for Paint.measureText, arrgh + private TextPaint mWorkPaint; + + private MeasuredText() { + mWorkPaint = new TextPaint(); + } + + private static MeasuredText[] cached = new MeasuredText[3]; + + /* package */ + static MeasuredText obtain() { + MeasuredText mt; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + mt = cached[i]; + cached[i] = null; + return mt; + } + } + } + mt = new MeasuredText(); + Log.e("MEAS", "new: " + mt); + return mt; + } + + /* package */ + static MeasuredText recycle(MeasuredText mt) { + mt.mText = null; + if (mt.mLen < 1000) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = mt; + break; + } + } + } + } + return null; + } + + /** + * Analyzes text for + * bidirectional runs. Allocates working buffers. + */ + /* package */ + void setPara(CharSequence text, int start, int end, int bidiRequest) { + mText = text; + mTextStart = start; + + int len = end - start; + mLen = len; + mPos = 0; + + if (mWidths == null || mWidths.length < len) { + mWidths = new float[ArrayUtils.idealFloatArraySize(len)]; + mWorkWidths = new float[mWidths.length]; + } + if (mChars == null || mChars.length < len) { + mChars = new char[ArrayUtils.idealCharArraySize(len)]; + } + TextUtils.getChars(text, start, end, mChars, 0); + + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + ReplacementSpan[] spans = spanned.getSpans(start, end, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int startInPara = spanned.getSpanStart(spans[i]) - start; + int endInPara = spanned.getSpanEnd(spans[i]) - start; + for (int j = startInPara; j < endInPara; j++) { + mChars[j] = '\uFFFC'; + } + } + } + + if (TextUtils.doesNotNeedBidi(mChars, 0, len)) { + mDir = 1; + mEasy = true; + } else { + if (mLevels == null || mLevels.length < len) { + mLevels = new byte[ArrayUtils.idealByteArraySize(len)]; + } + mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); + mEasy = false; + + // shape + if (mLen > 0) { + byte[] levels = mLevels; + char[] chars = mChars; + byte level = levels[0]; + int pi = 0; + for (int i = 1, e = mLen;; ++i) { + if (i == e || levels[i] != level) { + if ((level & 0x1) != 0) { + AndroidCharacter.mirror(chars, pi, i - pi); + ArabicShaping.SHAPER.shape(chars, pi, i - pi); + } + if (i == e) { + break; + } + pi = i; + level = levels[i]; + } + } + } + } + } + + float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { + int p = mPos; + float[] w = mWidths, ww = mWorkWidths; + int count = paint.getTextWidths(mChars, p, len, ww); + int width = 0; + if (count < len) { + // must have surrogate pairs in here, pad out the array with zero + // for the trailing surrogates + char[] chars = mChars; + for (int i = 0, e = mLen; i < count; ++i) { + width += (w[p++] = ww[i]); + if (p < e && chars[p] >= '\udc00' && chars[p] < '\ue000' && + chars[p-1] >= '\ud800' && chars[p-1] < '\udc00') { + w[p++] = 0; + } + } + } else { + for (int i = 0; i < len; ++i) { + width += (w[p++] = ww[i]); + } + } + mPos = p; + if (fm != null) { + paint.getFontMetricsInt(fm); + } + return width; + } + + float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, + Paint.FontMetricsInt fm) { + + TextPaint workPaint = mWorkPaint; + workPaint.set(paint); + // XXX paint should not have a baseline shift, but... + workPaint.baselineShift = 0; + + ReplacementSpan replacement = null; + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(workPaint); + } + } + + float wid; + if (replacement == null) { + wid = addStyleRun(workPaint, len, fm); + } else { + // Use original text. Shouldn't matter. + wid = replacement.getSize(workPaint, mText, mTextStart + mPos, + mTextStart + mPos + len, fm); + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + return wid; + } + + int breakText(int start, int limit, boolean forwards, float width) { + float[] w = mWidths; + if (forwards) { + for (int i = start; i < limit; ++i) { + if ((width -= w[i]) < 0) { + return i - start; + } + } + } else { + for (int i = limit; --i >= start;) { + if ((width -= w[i]) < 0) { + return limit - i -1; + } + } + } + + return limit - start; + } + + float measure(int start, int limit) { + float width = 0; + float[] w = mWidths; + for (int i = start; i < limit; ++i) { + width += w[i]; + } + return width; + } +} \ No newline at end of file diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index bfa0ab61f3e4d..0c6c545dfb46b 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -23,8 +23,6 @@ import android.graphics.Paint; import android.text.style.LeadingMarginSpan; import android.text.style.LineHeightSpan; import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; -import android.util.Log; /** * StaticLayout is a Layout for text that will not be edited after it @@ -96,13 +94,13 @@ extends Layout mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); + generate(source, bufstart, bufend, paint, outerwidth, align, spacingmult, spacingadd, includepad, includepad, ellipsize != null, ellipsizedWidth, ellipsize); - mChdirs = null; - mChs = null; - mWidths = null; + mMeasured = MeasuredText.recycle(mMeasured); mFontMetricsInt = null; } @@ -113,6 +111,7 @@ extends Layout mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); } /* package */ void generate(CharSequence source, int bufstart, int bufend, @@ -130,38 +129,22 @@ extends Layout Paint.FontMetricsInt fm = mFontMetricsInt; int[] choosehtv = null; - int end = TextUtils.indexOf(source, '\n', bufstart, bufend); - int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart; - boolean first = true; + MeasuredText measured = mMeasured; - if (mChdirs == null) { - mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)]; - mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)]; - mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)]; - } - - byte[] chdirs = mChdirs; - char[] chs = mChs; - float[] widths = mWidths; - - AlteredCharSequence alter = null; Spanned spanned = null; - if (source instanceof Spanned) spanned = (Spanned) source; int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX - for (int start = bufstart; start <= bufend; start = end) { - if (first) - first = false; + int paraEnd; + for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend); + if (paraEnd < 0) + paraEnd = bufend; else - end = TextUtils.indexOf(source, '\n', start, bufend); - - if (end < 0) - end = bufend; - else - end++; + paraEnd++; + int paraLen = paraEnd - paraStart; int firstWidthLineCount = 1; int firstwidth = outerwidth; @@ -170,19 +153,20 @@ extends Layout LineHeightSpan[] chooseht = null; if (spanned != null) { - LeadingMarginSpan[] sp; - - sp = spanned.getSpans(start, end, LeadingMarginSpan.class); + LeadingMarginSpan[] sp = spanned.getSpans(paraStart, paraEnd, + LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { - firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + firstWidthLineCount = + ((LeadingMarginSpan.LeadingMarginSpan2)lms) + .getLeadingMarginLineCount(); } } - chooseht = spanned.getSpans(start, end, LineHeightSpan.class); + chooseht = spanned.getSpans(paraStart, paraEnd, LineHeightSpan.class); if (chooseht.length != 0) { if (choosehtv == null || @@ -194,7 +178,7 @@ extends Layout for (int i = 0; i < chooseht.length; i++) { int o = spanned.getSpanStart(chooseht[i]); - if (o < start) { + if (o < paraStart) { // starts in this layout, before the // current paragraph @@ -208,135 +192,48 @@ extends Layout } } - if (end - start > chdirs.length) { - chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)]; - mChdirs = chdirs; - } - if (end - start > chs.length) { - chs = new char[ArrayUtils.idealCharArraySize(end - start)]; - mChs = chs; - } - if ((end - start) * 2 > widths.length) { - widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)]; - mWidths = widths; - } + measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); + char[] chs = measured.mChars; + float[] widths = measured.mWidths; + byte[] chdirs = measured.mLevels; + int dir = measured.mDir; + boolean easy = measured.mEasy; - TextUtils.getChars(source, start, end, chs, 0); - final int n = end - start; - - boolean easy = true; - boolean altered = false; - int dir = DEFAULT_DIR; // XXX pass value in - - for (int i = 0; i < n; i++) { - if (chs[i] >= FIRST_RIGHT_TO_LEFT) { - easy = false; - break; - } - } - - // Ensure that none of the underlying characters are treated - // as viable breakpoints, and that the entire run gets the - // same bidi direction. - - if (source instanceof Spanned) { - Spanned sp = (Spanned) source; - ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class); - - for (int y = 0; y < spans.length; y++) { - int a = sp.getSpanStart(spans[y]); - int b = sp.getSpanEnd(spans[y]); - - for (int x = a; x < b; x++) { - chs[x - start] = '\uFFFC'; - } - } - } - - if (!easy) { - // XXX put override flags, etc. into chdirs - // XXX supply dir rather than force - dir = AndroidBidi.bidi(DIR_REQUEST_DEFAULT_LTR, chs, chdirs, n, false); - - // Do mirroring for right-to-left segments - - for (int i = 0; i < n; i++) { - if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - int j; - - for (j = i; j < n; j++) { - if (chdirs[j] != - Character.DIRECTIONALITY_RIGHT_TO_LEFT) - break; - } - - if (AndroidCharacter.mirror(chs, i, j - i)) - altered = true; - - i = j - 1; - } - } - } - - CharSequence sub; - - if (altered) { - if (alter == null) - alter = AlteredCharSequence.make(source, chs, start, end); - else - alter.update(chs, start, end); - - sub = alter; - } else { - sub = source; - } + CharSequence sub = source; int width = firstwidth; float w = 0; - int here = start; + int here = paraStart; - int ok = start; + int ok = paraStart; float okwidth = w; int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; - int fit = start; + int fit = paraStart; float fitwidth = w; int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; boolean tab = false; - int next; - for (int i = start; i < end; i = next) { + int spanEnd; + for (int spanStart = paraStart; spanStart < paraEnd; spanStart = spanEnd) { if (spanned == null) - next = end; + spanEnd = paraEnd; else - next = spanned.nextSpanTransition(i, end, - MetricAffectingSpan. - class); + spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, + MetricAffectingSpan.class); + + int spanLen = spanEnd - spanStart; + int startInPara = spanStart - paraStart; + int endInPara = spanEnd - paraStart; if (spanned == null) { - paint.getTextWidths(sub, i, next, widths); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - paint.getFontMetricsInt(fm); + measured.addStyleRun(paint, spanLen, fm); } else { - mWorkPaint.baselineShift = 0; - - Styled.getTextWidths(paint, mWorkPaint, - spanned, i, next, - widths, fm); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - if (mWorkPaint.baselineShift < 0) { - fm.ascent += mWorkPaint.baselineShift; - fm.top += mWorkPaint.baselineShift; - } else { - fm.descent += mWorkPaint.baselineShift; - fm.bottom += mWorkPaint.baselineShift; - } + MetricAffectingSpan[] spans = + spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + measured.addStyleRun(paint, spans, spanLen, fm); } int fmtop = fm.top; @@ -344,27 +241,17 @@ extends Layout int fmascent = fm.ascent; int fmdescent = fm.descent; - if (false) { - StringBuilder sb = new StringBuilder(); - for (int j = i; j < next; j++) { - sb.append(widths[j - start + (end - start)]); - sb.append(' '); - } - - Log.e("text", sb.toString()); - } - - for (int j = i; j < next; j++) { - char c = chs[j - start]; + for (int j = spanStart; j < spanEnd; j++) { + char c = chs[j - paraStart]; float before = w; if (c == '\n') { ; } else if (c == '\t') { - w = Layout.nextTab(sub, start, end, w, null); + w = Layout.nextTab(sub, paraStart, paraEnd, w, null); tab = true; - } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) { - int emoji = Character.codePointAt(chs, j - start); + } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) { + int emoji = Character.codePointAt(chs, j - paraStart); if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { Bitmap bm = EMOJI_FACTORY. @@ -387,13 +274,13 @@ extends Layout tab = true; j++; } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); @@ -429,12 +316,12 @@ extends Layout if (c == ' ' || c == '\t' || ((c == '.' || c == ',' || c == ':' || c == ';') && - (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || ((c == '/' || c == '-') && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || (c >= FIRST_CJK && isIdeographic(c, true) && - j + 1 < next && isIdeographic(chs[j + 1 - start], false))) { + j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { okwidth = w; ok = j + 1; @@ -451,7 +338,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -461,9 +348,9 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -487,7 +374,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -497,9 +384,9 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -513,18 +400,19 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, fit == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, fitwidth, paint); here = fit; } else { // Log.e("text", "output one " + here + " to " +(here + 1)); - measureText(paint, mWorkPaint, - source, here, here + 1, fm, tab, - null); + // XXX not sure why the existing fm wasn't ok. + // measureText(paint, mWorkPaint, + // source, here, here + 1, fm, tab, + // null); v = out(source, here, here+1, @@ -533,18 +421,18 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, here + 1 == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, - widths[here - start], paint); + widths[here - paraStart], paint); here = here + 1; } - if (here < i) { - j = next = here; // must remeasure + if (here < spanStart) { + j = spanEnd = here; // must remeasure } else { j = here - 1; // continue looping } @@ -561,7 +449,7 @@ extends Layout } } - if (end != here) { + if (paraEnd != here) { if ((fittop | fitbottom | fitdescent | fitascent) == 0) { paint.getFontMetricsInt(fm); @@ -574,20 +462,20 @@ extends Layout // Log.e("text", "output rest " + here + " to " + end); v = out(source, - here, end, fitascent, fitdescent, + here, paraEnd, fitascent, fitdescent, fittop, fitbottom, v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, - end == bufend, includepad, trackpad, - widths, start, end - start, + needMultiply, paraStart, chdirs, dir, easy, + paraEnd == bufend, includepad, trackpad, + chs, widths, here - paraStart, where, ellipsizedWidth, w, paint); } - start = end; + paraStart = paraEnd; - if (end == bufend) + if (paraEnd == bufend) break; } @@ -602,9 +490,9 @@ extends Layout v, spacingmult, spacingadd, null, null, fm, false, - needMultiply, bufend, chdirs, DEFAULT_DIR, true, + needMultiply, bufend, null, DEFAULT_DIR, true, true, includepad, trackpad, - widths, bufstart, 0, + null, null, bufstart, where, ellipsizedWidth, 0, paint); } } @@ -714,28 +602,6 @@ extends Layout } */ - private static int getFit(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - float wid) { - int high = end + 1, low = start - 1, guess; - - while (high - low > 1) { - guess = (high + low) / 2; - - if (measureText(paint, workPaint, - text, start, guess, null, true, null) > wid) - high = guess; - else - low = guess; - } - - if (low < start) - return start; - else - return low; - } - private int out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, @@ -744,7 +610,7 @@ extends Layout boolean needMultiply, int pstart, byte[] chdirs, int dir, boolean easy, boolean last, boolean includepad, boolean trackpad, - float[] widths, int widstart, int widoff, + char[] chs, float[] widths, int widstart, TextUtils.TruncateAt ellipsize, float ellipsiswidth, float textwidth, TextPaint paint) { int j = mLineCount; @@ -752,8 +618,6 @@ extends Layout int want = off + mColumns + TOP; int[] lines = mLines; - // Log.e("text", "line " + start + " to " + end + (last ? "===" : "")); - if (want >= lines.length) { int nlen = ArrayUtils.idealIntArraySize(want + 1); int[] grow = new int[nlen]; @@ -840,122 +704,12 @@ extends Layout 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); - - 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; - } - // 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] = AndroidBidi.directions(dir, chdirs, widstart, chs, + widstart, end - start); // If ellipsize is in marquee mode, do not apply ellipsis on the first line if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) { - calculateEllipsis(start, end, widths, widstart, widoff, + calculateEllipsis(start, end, widths, widstart, ellipsiswidth, ellipsize, j, textwidth, paint); } @@ -966,7 +720,7 @@ extends Layout } private void calculateEllipsis(int linestart, int lineend, - float[] widths, int widstart, int widoff, + float[] widths, int widstart, float avail, TextUtils.TruncateAt where, int line, float textwidth, TextPaint paint) { int len = lineend - linestart; @@ -986,7 +740,7 @@ extends Layout int i; for (i = len; i >= 0; i--) { - float w = widths[i - 1 + linestart - widstart + widoff]; + float w = widths[i - 1 + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1002,7 +756,7 @@ extends Layout int i; for (i = 0; i < len; i++) { - float w = widths[i + linestart - widstart + widoff]; + float w = widths[i + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1019,7 +773,7 @@ extends Layout float ravail = (avail - ellipsiswid) / 2; for (right = len; right >= 0; right--) { - float w = widths[right - 1 + linestart - widstart + widoff]; + float w = widths[right - 1 + linestart - widstart]; if (w + rsum > ravail) { break; @@ -1030,7 +784,7 @@ extends Layout float lavail = avail - ellipsiswid - rsum; for (left = 0; left < right; left++) { - float w = widths[left + linestart - widstart + widoff]; + float w = widths[left + linestart - widstart]; if (w + lsum > lavail) { break; @@ -1047,7 +801,7 @@ extends Layout mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; } - // Override the baseclass so we can directly access our members, + // Override the base class so we can directly access our members, // rather than relying on member functions. // The logic mirrors that of Layout.getLineForVertical // FIXME: It may be faster to do a linear search for layouts without many lines. @@ -1156,10 +910,8 @@ extends Layout private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; /* - * These are reused across calls to generate() + * This is reused across calls to generate() */ - private byte[] mChdirs; - private char[] mChs; - private float[] mWidths; + private MeasuredText mMeasured; private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); } diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java deleted file mode 100644 index 513b2cd444373..0000000000000 --- a/core/java/android/text/Styled.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (C) 2006 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.graphics.Canvas; -import android.graphics.Paint; -import android.text.style.CharacterStyle; -import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; - -/** - * This class provides static methods for drawing and measuring styled text, - * like {@link android.text.Spanned} object with - * {@link android.text.style.ReplacementSpan}. - * - * @hide - */ -public class Styled -{ - /** - * Draws and/or measures a uniform run of text on a single line. No span of - * interest should start or end in the middle of this run (if not - * drawing, character spans that don't affect metrics can be ignored). - * Neither should the run direction change in the middle of the run. - * - *
The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - *
On return, workPaint will reflect the original paint plus any - * modifications made by character styles on the run. - * - *
The returned width is signed and will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawUniformRun(Canvas canvas, - Spanned text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - boolean haveWidth = false; - float ret = 0; - CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); - - ReplacementSpan replacement = null; - - // XXX: This shouldn't be modifying paint, only workPaint. - // However, the members belonging to TextPaint should have default - // values anyway. Better to ensure this in the Layout constructor. - paint.bgColor = 0; - paint.baselineShift = 0; - workPaint.set(paint); - - if (spans.length > 0) { - for (int i = 0; i < spans.length; i++) { - CharacterStyle span = spans[i]; - - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateDrawState(workPaint); - } - } - } - - if (replacement == null) { - CharSequence tmp; - int tmpstart, tmpend; - - if (runIsRtl) { - tmp = TextUtils.getReverse(text, start, end); - tmpstart = 0; - // XXX: assumes getReverse doesn't change the length of the text - tmpend = end - start; - } else { - tmp = text; - tmpstart = start; - tmpend = end; - } - - if (fmi != null) { - workPaint.getFontMetricsInt(fmi); - } - - if (canvas != null) { - if (workPaint.bgColor != 0) { - int c = workPaint.getColor(); - Paint.Style s = workPaint.getStyle(); - workPaint.setColor(workPaint.bgColor); - workPaint.setStyle(Paint.Style.FILL); - - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - canvas.drawRect(x - ret, top, x, bottom, workPaint); - else - canvas.drawRect(x, top, x + ret, bottom, workPaint); - - workPaint.setStyle(s); - workPaint.setColor(c); - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - canvas.drawText(tmp, tmpstart, tmpend, - x - ret, y + workPaint.baselineShift, workPaint); - } else { - if (needWidth) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - - canvas.drawText(tmp, tmpstart, tmpend, - x, y + workPaint.baselineShift, workPaint); - } - } else { - if (needWidth && !haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - } else { - ret = replacement.getSize(workPaint, text, start, end, fmi); - - if (canvas != null) { - if (dir == Layout.DIR_RIGHT_TO_LEFT) - replacement.draw(canvas, text, start, end, - x - ret, top, y, bottom, workPaint); - else - replacement.draw(canvas, text, start, end, - x, top, y, bottom, workPaint); - } - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - return -ret; - else - return ret; - } - - /** - * Returns the advance widths for a uniform left-to-right run of text with - * no style changes in the middle of the run. If any style is replacement - * text, the first character will get the width of the replacement and the - * remaining characters will get a width of 0. - * - * @param paint the paint, will not be modified - * @param workPaint a paint to modify; on return will reflect the original - * paint plus the effect of all spans on the run - * @param text the text - * @param start the start of the run - * @param end the limit of the run - * @param widths array to receive the advance widths of the characters. Must - * be at least a large as (end - start). - * @param fmi FontMetrics information; can be null - * @return the actual number of widths returned - */ - public static int getTextWidths(TextPaint paint, - TextPaint workPaint, - Spanned text, int start, int end, - float[] widths, Paint.FontMetricsInt fmi) { - MetricAffectingSpan[] spans = - text.getSpans(start, end, MetricAffectingSpan.class); - - ReplacementSpan replacement = null; - workPaint.set(paint); - - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateMeasureState(workPaint); - } - } - - if (replacement == null) { - workPaint.getFontMetricsInt(fmi); - workPaint.getTextWidths(text, start, end, widths); - } else { - int wid = replacement.getSize(workPaint, text, start, end, fmi); - - if (end > start) { - widths[0] = wid; - for (int i = start + 1; i < end; i++) - widths[i - start] = 0; - } - } - return end - start; - } - - /** - * Renders and/or measures a directional run of text on a single line. - * Unlike {@link #drawUniformRun}, this can render runs that cross style - * boundaries. Returns the signed advance width, if requested. - * - *
The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - *
This optimizes for unstyled text and so workPaint might not be - * modified by this call. - * - *
The returned advance width will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawDirectionalRun(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - // XXX: It looks like all calls to this API match dir and runIsRtl, so - // having both parameters is redundant and confusing. - - // fast path for unstyled text - if (!(text instanceof Spanned)) { - float ret = 0; - - if (runIsRtl) { - CharSequence tmp = TextUtils.getReverse(text, start, end); - // XXX: this assumes getReverse doesn't tweak the length of - // the text - int tmpend = end - start; - - if (canvas != null || needWidth) - ret = paint.measureText(tmp, 0, tmpend); - - if (canvas != null) - canvas.drawText(tmp, 0, tmpend, - x - ret, y, paint); - } else { - if (needWidth) - ret = paint.measureText(text, start, end); - - if (canvas != null) - canvas.drawText(text, start, end, x, y, paint); - } - - if (fmi != null) { - paint.getFontMetricsInt(fmi); - } - - return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 - } - - float ox = x; - int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; - - Spanned sp = (Spanned) text; - Class> division; - - if (canvas == null) - division = MetricAffectingSpan.class; - else - division = CharacterStyle.class; - - int next; - for (int i = start; i < end; i = next) { - next = sp.nextSpanTransition(i, end, division); - - // XXX: if dir and runIsRtl were not the same, this would draw - // spans in the wrong order, but no one appears to call it this - // way. - x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, - x, top, y, bottom, fmi, paint, workPaint, - needWidth || next != end); - - if (fmi != null) { - if (fmi.ascent < minAscent) - minAscent = fmi.ascent; - if (fmi.descent > maxDescent) - maxDescent = fmi.descent; - - if (fmi.top < minTop) - minTop = fmi.top; - if (fmi.bottom > maxBottom) - maxBottom = fmi.bottom; - } - } - - if (fmi != null) { - if (start == end) { - paint.getFontMetricsInt(fmi); - } else { - fmi.ascent = minAscent; - fmi.descent = maxDescent; - fmi.top = minTop; - fmi.bottom = maxBottom; - } - } - - return x - ox; - } - - /** - * Draws a unidirectional run of text on a single line, and optionally - * returns the signed advance. Unlike drawDirectionalRun, the paragraph - * direction and run direction can be different. - */ - /* package */ static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl - if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || - (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { - // TODO: this needs the real direction - float ch = drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, - workPaint, true); - - ch *= dir; // DIR_RIGHT_TO_LEFT == -1 - drawDirectionalRun(canvas, text, start, end, -dir, - runIsRtl, x + ch, top, y, bottom, null, paint, - workPaint, true); - - return ch; - } - - return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, - x, top, y, bottom, null, paint, workPaint, - needWidth); - } - - /** - * Draws a run of text on a single line, with its - * origin at (x,y), in the specified Paint. The origin is interpreted based - * on the Align setting in the Paint. - * - * This method considers style information in the text (e.g. even when text - * is an instance of {@link android.text.Spanned}, this method correctly - * draws the text). See also - * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, - * float, Paint)} and - * {@link android.graphics.Canvas#drawRect(float, float, float, float, - * Paint)}. - * - * @param canvas The target canvas - * @param text The text to be drawn - * @param start The index of the first character in text to draw - * @param end (end - 1) is the index of the last character in text to draw - * @param direction The direction of the text. This must be - * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or - * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. - * @param x The x-coordinate of origin for where to draw the text - * @param top The top side of the rectangle to be drawn - * @param y The y-coordinate of origin for where to draw the text - * @param bottom The bottom side of the rectangle to be drawn - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal - * workspace. - * @param needWidth If true, this method returns the width of drawn text - * @return Width of the drawn text if needWidth is true - */ - public static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int direction, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // For safety. - direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT - : Layout.DIR_RIGHT_TO_LEFT; - - // Hide runIsRtl parameter since it is meaningless for external - // developers. - // XXX: the runIsRtl probably ought to be the same as direction, then - // this could draw rtl text. - return drawText(canvas, text, start, end, direction, false, - x, top, y, bottom, paint, workPaint, needWidth); - } - - /** - * Returns the width of a run of left-to-right text on a single line, - * considering style information in the text (e.g. even when text is an - * instance of {@link android.text.Spanned}, this method correctly measures - * the width of the text). - * - * @param paint the main {@link TextPaint} object; will not be modified - * @param workPaint the {@link TextPaint} object available for modification; - * will not necessarily be used - * @param text the text to measure - * @param start the index of the first character to start measuring - * @param end 1 beyond the index of the last character to measure - * @param fmi FontMetrics information; can be null - * @return The width of the text - */ - public static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - Paint.FontMetricsInt fmi) { - return drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, - 0, 0, 0, 0, fmi, paint, workPaint, true); - } -} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java new file mode 100644 index 0000000000000..8ab481be7199f --- /dev/null +++ b/core/java/android/text/TextLine.java @@ -0,0 +1,1053 @@ +/* + * 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 com.android.internal.util.ArrayUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Paint.FontMetricsInt; +import android.icu.text.ArabicShaping; +import android.text.Layout.Directions; +import android.text.style.CharacterStyle; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.util.Log; + +/** + * Represents a line of styled text, for measuring in visual order and + * for rendering. + * + *
Get a new instance using obtain(), and when finished with it, return it + * to the pool using recycle(). + * + *
Call set to prepare the instance for use, then either draw, measure, + * metrics, or caretToLeftRightOf. + * + * @hide + */ +class TextLine { + private TextPaint mPaint; + private CharSequence mText; + private int mStart; + private int mLen; + private int mDir; + private Directions mDirections; + private boolean mHasTabs; + private TabStopSpan[] mTabs; + + private char[] mChars; + private boolean mCharsValid; + private Spanned mSpanned; + private TextPaint mWorkPaint = new TextPaint(); + private int mPreppedIndex; + private int mPreppedLimit; + + private static TextLine[] cached = new TextLine[3]; + + /** + * Returns a new TextLine from the shared pool. + * + * @return an uninitialized TextLine + */ + static TextLine obtain() { + TextLine tl; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + tl = cached[i]; + cached[i] = null; + return tl; + } + } + } + tl = new TextLine(); + Log.e("TLINE", "new: " + tl); + return tl; + } + + /** + * Puts a TextLine back into the shared pool. Do not use this TextLine once + * it has been returned. + * @param tl the textLine + * @return null, as a convenience from clearing references to the provided + * TextLine + */ + static TextLine recycle(TextLine tl) { + tl.mText = null; + tl.mPaint = null; + tl.mDirections = null; + if (tl.mLen < 250) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = tl; + break; + } + } + } + } + return null; + } + + /** + * Initializes a TextLine and prepares it for use. + * + * @param paint the base paint for the line + * @param text the text, can be Styled + * @param start the start of the line relative to the text + * @param limit the limit of the line relative to the text + * @param dir the paragraph direction of this line + * @param directions the directions information of this line + * @param hasTabs true if the line might contain tabs or emoji + * @param spans array of paragraph-level spans, of which only TabStopSpans + * are used. Can be null. + */ + void set(TextPaint paint, CharSequence text, int start, int limit, int dir, + Directions directions, boolean hasTabs, Object[] spans) { + mPaint = paint; + mText = text; + mStart = start; + mLen = limit - start; + mDir = dir; + mDirections = directions; + mHasTabs = hasTabs; + mSpanned = null; + mPreppedIndex = 0; + mPreppedLimit = 0; + + boolean hasReplacement = false; + if (text instanceof Spanned) { + mSpanned = (Spanned) text; + hasReplacement = mSpanned.getSpans(start, limit, + ReplacementSpan.class).length > 0; + } + + mCharsValid = hasReplacement || hasTabs || + directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + + if (mCharsValid) { + if (mChars == null || mChars.length < mLen) { + mChars = new char[ArrayUtils.idealCharArraySize(mLen)]; + } + TextUtils.getChars(text, start, limit, mChars, 0); + + if (hasTabs) { + TabStopSpan[] tabs = mTabs; + int tabLen = 0; + if (mSpanned != null && spans == null) { + TabStopSpan[] newTabs = mSpanned.getSpans(start, limit, + TabStopSpan.class); + if (tabs == null || tabs.length < newTabs.length) { + tabs = newTabs; + } else { + for (int i = 0; i < newTabs.length; ++i) { + tabs[i] = newTabs[i]; + } + } + tabLen = newTabs.length; + } else if (spans != null) { + if (tabs == null || tabs.length < spans.length) { + tabs = new TabStopSpan[spans.length]; + } + for (int i = 0; i < spans.length; ++i) { + if (spans[i] instanceof TabStopSpan) { + tabs[tabLen++] = (TabStopSpan) spans[i]; + } + } + } + + if (tabs != null && tabLen < tabs.length){ + tabs[tabLen] = null; + } + mTabs = tabs; + } + } + } + + /** + * Renders the TextLine. + * + * @param c the canvas to render on + * @param x the leading margin position + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + */ + void draw(Canvas c, float x, int top, int y, int bottom) { + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); + return; + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); + return; + } + } + + float h = 0; + int[] runs = mDirections.mDirections; + RectF emojiRect = null; + + int lastRunIndex = runs.length - 2; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + char[] chars = mChars; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = mChars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(mChars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom, + i != lastRunIndex || j != mLen); + + if (codept == '\t') { + h = mDir * nextTab(h * mDir); + } else if (bm != null) { + float bmAscent = ascent(j); + float bitmapHeight = bm.getHeight(); + float scale = -bmAscent / bitmapHeight; + float width = bm.getWidth() * scale; + + if (emojiRect == null) { + emojiRect = new RectF(); + } + emojiRect.set(x + h, y + bmAscent, + x + h + width, y); + c.drawBitmap(bm, null, emojiRect, mPaint); + h += width; + j++; + } + segstart = j + 1; + } + } + } + } + + /** + * Returns metrics information for the entire line. + * + * @param fmi receives font metrics information, can be null + * @return the signed width of the line + */ + float metrics(FontMetricsInt fmi) { + return measure(mLen, false, fmi); + } + + /** + * Returns information about a position on the line. + * + * @param offset the line-relative character offset, between 0 and the + * line length, inclusive + * @param trailing true to measure the trailing edge of the character + * before offset, false to measure the leading edge of the character + * at offset. + * @param fmi receives metrics information about the requested + * character, can be null. + * @return the signed offset from the leading margin to the requested + * character edge. + */ + float measure(int offset, boolean trailing, FontMetricsInt fmi) { + int target = trailing ? offset - 1 : offset; + if (target < 0) { + return 0; + } + + float h = 0; + + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + return measureRun( 0, 0, target, mLen, false, fmi); + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + return measureRun(0, 0, target, mLen, true, fmi); + } + } + + char[] chars = mChars; + int[] runs = mDirections.mDirections; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = chars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(chars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + boolean inSegment = target >= segstart && target < j; + + boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; + if (inSegment && advance) { + return h += measureRun(i, segstart, offset, j, runIsRtl, fmi); + } + + float w = measureRun(i, segstart, j, j, runIsRtl, fmi); + h += advance ? w : -w; + + if (inSegment) { + return h += measureRun(i, segstart, offset, j, runIsRtl, null); + } + + if (codept == '\t') { + if (offset == j) { + return h; + } + h = mDir * nextTab(h * mDir); + if (target == j) { + return h; + } + } + + if (bm != null) { + float bmAscent = ascent(j); + float wid = bm.getWidth() * -bmAscent / bm.getHeight(); + h += mDir * wid; + j++; + } + + segstart = j + 1; + } + } + } + + return h; + } + + /** + * Draws a unidirectional (but possibly multi-styled) run of text. + * + * @param c the canvas to draw on + * @param runIndex the index of this directional run + * @param start the line-relative start + * @param limit the line-relative limit + * @param runIsRtl true if the run is right-to-left + * @param x the position of the run that is closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param needWidth true if the width value is required. + * @return the signed width of the run, based on the paragraph direction. + * Only valid if needWidth is true. + */ + private float drawRun(Canvas c, int runIndex, int start, + int limit, boolean runIsRtl, float x, int top, int y, int bottom, + boolean needWidth) { + + if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { + float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null); + handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top, + y, bottom, null, false, PREP_NONE); + return w; + } + + return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top, + y, bottom, null, needWidth, PREP_NEEDED); + } + + /** + * Measures a unidirectional (but possibly multi-styled) run of text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param offset the offset to measure to, between start and limit inclusive + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + * @param fmi receives metrics information about the requested + * run, can be null. + * @return the signed width from the start of the run to the leading edge + * of the character at offset, based on the run (not paragraph) direction + */ + private float measureRun(int runIndex, int start, + int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) { + return handleRun(runIndex, start, offset, limit, runIsRtl, null, + 0, 0, 0, 0, fmi, true, PREP_NEEDED); + } + + /** + * Prepares a run for measurement or rendering. This ensures that any + * required shaping of the text in the run has been performed so that + * measurements reflect the shaped text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + */ + private void prepRun(int runIndex, int start, int limit, + boolean runIsRtl) { + handleRun(runIndex, start, limit, limit, runIsRtl, null, 0, 0, 0, + 0, null, false, PREP_ONLY); + } + + /** + * Walk the cursor through this line, skipping conjuncts and + * zero-width characters. + * + *
This function cannot properly walk the cursor off the ends of the line + * since it does not know about any shaping on the previous/following line + * that might affect the cursor position. Callers must either avoid these + * situations or handle the result specially. + * + *
The paint is required because the region around the cursor might not
+ * have been formatted yet, and the valid positions can depend on the glyphs
+ * used to render the text, which in turn depends on the paint.
+ *
+ * @param paint the base paint of the line
+ * @param cursor the starting position of the cursor, between 0 and the
+ * length of the line, inclusive
+ * @param toLeft true if the caret is moving to the left.
+ * @return the new offset. If it is less than 0 or greater than the length
+ * of the line, the previous/following line should be examined to get the
+ * actual offset.
+ */
+ int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
+ // 1) The caret marks the leading edge of a character. The character
+ // logically before it might be on a different level, and the active caret
+ // 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.
+
+ int lineStart = 0;
+ int lineEnd = mLen;
+ boolean paraIsRtl = mDir == -1;
+ int[] runs = mDirections.mDirections;
+
+ int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
+ boolean trailing = false;
+
+ if (cursor == lineStart) {
+ runIndex = -2;
+ } else if (cursor == lineEnd) {
+ runIndex = runs.length;
+ } else {
+ // First, get information about the run containing the character with
+ // the active caret.
+ for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
+ runStart = lineStart + runs[runIndex];
+ if (cursor >= runStart) {
+ runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
+ if (runLimit > lineEnd) {
+ runLimit = lineEnd;
+ }
+ if (cursor < runLimit) {
+ runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
+ Layout.RUN_LEVEL_MASK;
+ if (cursor == 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 = cursor - 1;
+ for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
+ prevRunStart = lineStart + runs[prevRunIndex];
+ if (pos >= prevRunStart) {
+ prevRunLimit = prevRunStart +
+ (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
+ if (prevRunLimit > lineEnd) {
+ prevRunLimit = lineEnd;
+ }
+ if (pos < prevRunLimit) {
+ prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
+ & Layout.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 runIsRtl = (runLevel & 0x1) != 0;
+ boolean advance = toLeft == runIsRtl;
+ if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
+ // Moving within or into the run, so we can move logically.
+ prepRun(runIndex, runStart, runLimit, runIsRtl);
+ newCaret = getOffsetBeforeAfter(runIndex, cursor, 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] & Layout.RUN_LENGTH_MASK);
+ if (otherRunLimit > lineEnd) {
+ otherRunLimit = lineEnd;
+ }
+ int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
+ Layout.RUN_LEVEL_MASK;
+ boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
+
+ advance = toLeft == otherRunIsRtl;
+ if (newCaret == -1) {
+ prepRun(otherRunIndex, otherRunStart, otherRunLimit,
+ otherRunIsRtl);
+ newCaret = getOffsetBeforeAfter(otherRunIndex,
+ 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(-1, cursor, 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;
+ }
+
+ /**
+ * Returns the next valid offset within this directional run, skipping
+ * conjuncts and zero-width characters. This should not be called to walk
+ * off the end of the run.
+ *
+ * @param runIndex the run index
+ * @param offset the offset
+ * @param after true if the new offset should logically follow the provided
+ * offset
+ * @return the new offset
+ */
+ private int getOffsetBeforeAfter(int runIndex, int offset, boolean after) {
+ // XXX note currently there is no special handling of zero-width
+ // combining marks, since the only analysis involves mock shaping.
+
+ boolean offEnd = offset == (after ? mLen : 0);
+ if (runIndex >= 0 && !offEnd && mCharsValid) {
+ char[] chars = mChars;
+ if (after) {
+ int cp = Character.codePointAt(chars, offset, mLen);
+ if (cp >= 0x10000) {
+ ++offset;
+ }
+ while (++offset < mLen && chars[offset] == '\ufeff'){}
+ } else {
+ while (--offset >= 0 && chars[offset] == '\ufeff'){}
+ int cp = Character.codePointBefore(chars, offset + 1);
+ if (cp >= 0x10000) {
+ --offset;
+ }
+ }
+ return offset;
+ }
+
+ if (after) {
+ return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
+ }
+ return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
+ }
+
+ /**
+ * Utility function for measuring and rendering text. The text must
+ * not include a tab or emoji.
+ *
+ * @param wp the working paint
+ * @param start the start of the text
+ * @param limit the limit of the text
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null if rendering is not needed
+ * @param x the edge of the run closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width of the run is needed
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleText(TextPaint wp, int start, int limit,
+ boolean runIsRtl, Canvas c, float x, int top, int y, int bottom,
+ FontMetricsInt fmi, boolean needWidth) {
+
+ float ret = 0;
+
+ int runLen = limit - start;
+ if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) {
+ if (mCharsValid) {
+ ret = wp.measureText(mChars, start, runLen);
+ } else {
+ ret = wp.measureText(mText, mStart + start,
+ mStart + start + runLen);
+ }
+ }
+
+ if (fmi != null) {
+ wp.getFontMetricsInt(fmi);
+ }
+
+ if (c != null) {
+ if (runIsRtl) {
+ x -= ret;
+ }
+
+ if (wp.bgColor != 0) {
+ int color = wp.getColor();
+ Paint.Style s = wp.getStyle();
+ wp.setColor(wp.bgColor);
+ wp.setStyle(Paint.Style.FILL);
+
+ c.drawRect(x, top, x + ret, bottom, wp);
+
+ wp.setStyle(s);
+ wp.setColor(color);
+ }
+
+ drawTextRun(c, wp, start, limit, runIsRtl, x, y + wp.baselineShift);
+ }
+
+ return runIsRtl ? -ret : ret;
+ }
+
+ /**
+ * Utility function for measuring and rendering a replacement.
+ *
+ * @param replacement the replacement
+ * @param wp the work paint
+ * @param runIndex the run index
+ * @param start the start of the run
+ * @param limit the limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null if not rendering
+ * @param x the edge of the replacement closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width of the replacement is needed
+ * @param prepFlags one of PREP_NONE, PREP_REQUIRED, or PREP_ONLY
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
+ int runIndex, int start, int limit, boolean runIsRtl, Canvas c,
+ float x, int top, int y, int bottom, FontMetricsInt fmi,
+ boolean needWidth, int prepFlags) {
+
+ float ret = 0;
+
+ // Preparation replaces the first character of the series with the
+ // object-replacement character and the remainder with zero width
+ // non-break space aka BOM. Cursor movement code skips over the BOMs
+ // so that the replacement character is the only character 'seen'.
+ if (prepFlags != PREP_NONE && limit > start &&
+ (runIndex > mPreppedIndex ||
+ (runIndex == mPreppedIndex && start >= mPreppedLimit))) {
+ char[] chars = mChars;
+ chars[start] = '\ufffc';
+ for (int i = start + 1; i < limit; ++i) {
+ chars[i] = '\ufeff'; // used as ZWNBS, marks positions to skip
+ }
+ mPreppedIndex = runIndex;
+ mPreppedLimit = limit;
+ }
+
+ if (prepFlags != PREP_ONLY) {
+ int textStart = mStart + start;
+ int textLimit = mStart + limit;
+
+ if (needWidth || (c != null && runIsRtl)) {
+ ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
+ }
+
+ if (c != null) {
+ if (runIsRtl) {
+ x -= ret;
+ }
+ replacement.draw(c, mText, textStart, textLimit,
+ x, top, y, bottom, wp);
+ }
+ }
+
+ return runIsRtl ? -ret : ret;
+ }
+
+ /**
+ * Utility function for handling a unidirectional run. The run must not
+ * contain tabs or emoji but can contain styles.
+ *
+ * @param p the base paint
+ * @param runIndex the run index
+ * @param start the line-relative start of the run
+ * @param offset the offset to measure to, between start and limit inclusive
+ * @param limit the limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null
+ * @param x the end of the run closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width is required
+ * @param prepFlags one of PREP_NONE, PREP_REQUIRED, or PREP_ONLY
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleRun(int runIndex, int start, int offset,
+ int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
+ int bottom, FontMetricsInt fmi, boolean needWidth, int prepFlags) {
+
+ // Shaping needs to take into account context up to metric boundaries,
+ // but rendering needs to take into account character style boundaries.
+ // So we iterate through metric runs, shape using the initial
+ // paint (the same typeface is used up to the next metric boundary),
+ // then within each metric run iterate through character style runs.
+ float ox = x;
+ for (int i = start, inext; i < offset; i = inext) {
+ TextPaint wp = mWorkPaint;
+ wp.set(mPaint);
+
+ int mnext;
+ if (mSpanned == null) {
+ inext = limit;
+ mnext = offset;
+ } else {
+ inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit,
+ MetricAffectingSpan.class) - mStart;
+
+ mnext = inext < offset ? inext : offset;
+ MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i,
+ mStart + mnext, MetricAffectingSpan.class);
+
+ if (spans.length > 0) {
+ ReplacementSpan replacement = null;
+ for (int j = 0; j < spans.length; j++) {
+ MetricAffectingSpan span = spans[j];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ } else {
+ span.updateDrawState(wp); // XXX or measureState?
+ }
+ }
+
+ if (replacement != null) {
+ x += handleReplacement(replacement, wp, runIndex, i,
+ mnext, runIsRtl, c, x, top, y, bottom, fmi,
+ needWidth || mnext < offset, prepFlags);
+ continue;
+ }
+ }
+ }
+
+ if (prepFlags != PREP_NONE) {
+ handlePrep(wp, runIndex, i, inext, runIsRtl);
+ }
+
+ if (prepFlags != PREP_ONLY) {
+ if (mSpanned == null || c == null) {
+ x += handleText(wp, i, mnext, runIsRtl, c, x, top,
+ y, bottom, fmi, needWidth || mnext < offset);
+ } else {
+ for (int j = i, jnext; j < mnext; j = jnext) {
+ jnext = mSpanned.nextSpanTransition(mStart + j,
+ mStart + mnext, CharacterStyle.class) - mStart;
+
+ CharacterStyle[] spans = mSpanned.getSpans(mStart + j,
+ mStart + jnext, CharacterStyle.class);
+
+ wp.set(mPaint);
+ for (int k = 0; k < spans.length; k++) {
+ CharacterStyle span = spans[k];
+ span.updateDrawState(wp);
+ }
+
+ x += handleText(wp, j, jnext, runIsRtl, c, x,
+ top, y, bottom, fmi, needWidth || jnext < offset);
+ }
+ }
+ }
+ }
+
+ return x - ox;
+ }
+
+ private static final int PREP_NONE = 0;
+ private static final int PREP_NEEDED = 1;
+ private static final int PREP_ONLY = 2;
+
+ /**
+ * Prepares text for measuring or rendering.
+ *
+ * @param paint the paint used to shape the text
+ * @param runIndex the run index
+ * @param start the start of the text to prepare
+ * @param limit the limit of the text to prepare
+ * @param runIsRtl true if the run is right-to-left
+ */
+ private void handlePrep(TextPaint paint, int runIndex, int start, int limit,
+ boolean runIsRtl) {
+
+ // The current implementation 'prepares' text by manipulating the
+ // character array. In order to keep track of what ranges have
+ // already been prepared, it uses the runIndex and the limit of
+ // the prepared text within that run. This index is required
+ // since operations that prepare the text always proceed in visual
+ // order and the limit itself does not let us know which runs have
+ // been processed and which have not.
+ //
+ // This bookkeeping is an attempt to let us process a line partially,
+ // for example, by only shaping up to the cursor position. This may
+ // not make sense if we can reuse the line, say by caching repeated
+ // accesses to the same line for both measuring and drawing, since in
+ // those cases we'd always prepare the entire line. At the
+ // opposite extreme, we might shape and then immediately discard only
+ // the run of text we're working with at the moment, instead of retaining
+ // the results of shaping (as the chars array is). In this case as well
+ // we would not need to do the index/limit bookkeeping.
+ //
+ // Technically, the only reason for bookkeeping is so that we don't
+ // re-mirror already-mirrored glyphs, since the shaping and object
+ // replacement operations will not change already-processed text.
+
+ if (runIndex > mPreppedIndex ||
+ (runIndex == mPreppedIndex && start >= mPreppedLimit)) {
+ if (runIsRtl) {
+ int runLen = limit - start;
+ AndroidCharacter.mirror(mChars, start, runLen);
+ ArabicShaping.SHAPER.shape(mChars, start, runLen);
+
+ // Note: tweaked MockShaper to put '\ufeff' in place of
+ // alef when it forms lam-alef ligatures, so no extra
+ // processing is necessary here.
+ }
+ mPreppedIndex = runIndex;
+ mPreppedLimit = limit;
+ }
+ }
+
+ /**
+ * Render a text run with the set-up paint.
+ *
+ * @param c the canvas
+ * @param wp the paint used to render the text
+ * @param start the run start
+ * @param limit the run limit
+ * @param runIsRtl true if the run is right-to-left
+ * @param x the x position of the left edge of the run
+ * @param y the baseline of the run
+ */
+ private void drawTextRun(Canvas c, TextPaint wp, int start, int limit,
+ boolean runIsRtl, float x, int y) {
+
+ // Since currently skia only renders text left-to-right, we need to
+ // put the shaped characters into visual order before rendering.
+ // Since we might want to re-render the line again, we swap them
+ // back when we're done. If we left them swapped, measurement
+ // would be broken since it expects the characters in logical order.
+ if (runIsRtl) {
+ swapRun(start, limit);
+ }
+ if (mCharsValid) {
+ c.drawText(mChars, start, limit - start, x, y, wp);
+ } else {
+ c.drawText(mText, mStart + start, mStart + limit, x, y, wp);
+ }
+ if (runIsRtl) {
+ swapRun(start, limit);
+ }
+ }
+
+ /**
+ * Reverses the order of characters in the chars array between start and
+ * limit, used by drawTextRun.
+ * @param start the start of the run to reverse
+ * @param limit the limit of the run to reverse
+ */
+ private void swapRun(int start, int limit) {
+ // First we swap all the characters one for one, then we
+ // do another pass looking for surrogate pairs and swapping them
+ // back into their logical order.
+ char[] chars = mChars;
+ for (int s = start, e = limit - 1; s < e; ++s, --e) {
+ char ch = chars[s]; chars[s] = chars[e]; chars[e] = ch;
+ }
+
+ for (int s = start, e = limit - 1; s < e; ++s) {
+ char c1 = chars[s];
+ if (c1 >= 0xdc00 && c1 < 0xe000) {
+ char c2 = chars[s+1];
+ if (c2 >= 0xd800 && c2 < 0xdc00) {
+ chars[s++] = c2;
+ chars[s] = c1;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the ascent of the text at start. This is used for scaling
+ * emoji.
+ *
+ * @param pos the line-relative position
+ * @return the ascent of the text at start
+ */
+ float ascent(int pos) {
+ if (mSpanned == null) {
+ return mPaint.ascent();
+ }
+
+ pos += mStart;
+ MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1,
+ MetricAffectingSpan.class);
+ if (spans.length == 0) {
+ return mPaint.ascent();
+ }
+
+ TextPaint wp = mWorkPaint;
+ wp.set(mPaint);
+ for (MetricAffectingSpan span : spans) {
+ span.updateMeasureState(wp);
+ }
+ return wp.ascent();
+ }
+
+ /**
+ * Returns the next tab position.
+ *
+ * @param h the (unsigned) offset from the leading margin
+ * @return the (unsigned) tab position after this offset
+ */
+ float nextTab(float h) {
+ float nh = Float.MAX_VALUE;
+ boolean alltabs = false;
+
+ if (mHasTabs && mTabs != null) {
+ TabStopSpan[] tabs = mTabs;
+ for (int i = 0; i < tabs.length && tabs[i] != null; ++i) {
+ int where = tabs[i].getTabStop();
+ if (where < nh && where > h) {
+ nh = where;
+ }
+ }
+ if (nh != Float.MAX_VALUE) {
+ return nh;
+ }
+ }
+
+ return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
+ }
+
+ private static final int TAB_INCREMENT = 20;
+}
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 9589bf369e6ea..2d6c7b68fce36 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -17,12 +17,11 @@
package android.text;
import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.method.TextKeyListener.Capitalize;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
@@ -45,10 +44,8 @@ import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Printer;
-import com.android.internal.util.ArrayUtils;
-
-import java.util.regex.Pattern;
import java.util.Iterator;
+import java.util.regex.Pattern;
public class TextUtils {
private TextUtils() { /* cannot be instantiated */ }
@@ -983,7 +980,7 @@ public class TextUtils {
/**
* Returns the original text if it fits in the specified width
* given the properties of the specified Paint,
- * or, if it does not fit, a copy with ellipsis character added
+ * or, if it does not fit, a copy with ellipsis character added
* at the specified edge or center.
* If preserveLength is specified, the returned copy
* will be padded with zero-width spaces to preserve the original
@@ -992,7 +989,7 @@ public class TextUtils {
* report the start and end of the ellipsized range.
*/
public static CharSequence ellipsize(CharSequence text,
- TextPaint p,
+ TextPaint paint,
float avail, TruncateAt where,
boolean preserveLength,
EllipsizeCallback callback) {
@@ -1003,13 +1000,12 @@ public class TextUtils {
int len = text.length();
- // Use Paint.breakText() for the non-Spanned case to avoid having
- // to allocate memory and accumulate the character widths ourselves.
+ MeasuredText mt = MeasuredText.obtain();
+ try {
+ float width = setPara(mt, paint, text, 0, text.length(),
+ Layout.DIR_REQUEST_DEFAULT_LTR);
- if (!(text instanceof Spanned)) {
- float wid = p.measureText(text, 0, len);
-
- if (wid <= avail) {
+ if (width <= avail) {
if (callback != null) {
callback.ellipsized(0, 0);
}
@@ -1017,250 +1013,69 @@ public class TextUtils {
return text;
}
- float ellipsiswid = p.measureText(sEllipsis);
+ // XXX assumes ellipsis string does not require shaping and
+ // is unaffected by style
+ float ellipsiswid = paint.measureText(sEllipsis);
+ avail -= ellipsiswid;
- if (ellipsiswid > avail) {
- if (callback != null) {
- callback.ellipsized(0, len);
- }
-
- if (preserveLength) {
- char[] buf = obtain(len);
- for (int i = 0; i < len; i++) {
- buf[i] = '\uFEFF';
- }
- String ret = new String(buf, 0, len);
- recycle(buf);
- return ret;
- } else {
- return "";
- }
- }
-
- if (where == TruncateAt.START) {
- int fit = p.breakText(text, 0, len, false,
- avail - ellipsiswid, null);
-
- if (callback != null) {
- callback.ellipsized(0, len - fit);
- }
-
- if (preserveLength) {
- return blank(text, 0, len - fit);
- } else {
- return sEllipsis + text.toString().substring(len - fit, len);
- }
+ int left = 0;
+ int right = len;
+ if (avail < 0) {
+ // it all goes
+ } else if (where == TruncateAt.START) {
+ right = len - mt.breakText(0, len, false, avail);
} else if (where == TruncateAt.END) {
- int fit = p.breakText(text, 0, len, true,
- avail - ellipsiswid, null);
-
- if (callback != null) {
- callback.ellipsized(fit, len);
- }
-
- if (preserveLength) {
- return blank(text, fit, len);
- } else {
- return text.toString().substring(0, fit) + sEllipsis;
- }
- } else /* where == TruncateAt.MIDDLE */ {
- int right = p.breakText(text, 0, len, false,
- (avail - ellipsiswid) / 2, null);
- float used = p.measureText(text, len - right, len);
- int left = p.breakText(text, 0, len - right, true,
- avail - ellipsiswid - used, null);
-
- if (callback != null) {
- callback.ellipsized(left, len - right);
- }
-
- if (preserveLength) {
- return blank(text, left, len - right);
- } else {
- String s = text.toString();
- return s.substring(0, left) + sEllipsis +
- s.substring(len - right, len);
- }
- }
- }
-
- // But do the Spanned cases by hand, because it's such a pain
- // to iterate the span transitions backwards and getTextWidths()
- // will give us the information we need.
-
- // getTextWidths() always writes into the start of the array,
- // so measure each span into the first half and then copy the
- // results into the second half to use later.
-
- float[] wid = new float[len * 2];
- TextPaint temppaint = new TextPaint();
- Spanned sp = (Spanned) text;
-
- int next;
- for (int i = 0; i < len; i = next) {
- next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
-
- Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
- System.arraycopy(wid, 0, wid, len + i, next - i);
- }
-
- float sum = 0;
- for (int i = 0; i < len; i++) {
- sum += wid[len + i];
- }
-
- if (sum <= avail) {
- if (callback != null) {
- callback.ellipsized(0, 0);
- }
-
- return text;
- }
-
- float ellipsiswid = p.measureText(sEllipsis);
-
- if (ellipsiswid > avail) {
- if (callback != null) {
- callback.ellipsized(0, len);
- }
-
- if (preserveLength) {
- char[] buf = obtain(len);
- for (int i = 0; i < len; i++) {
- buf[i] = '\uFEFF';
- }
- SpannableString ss = new SpannableString(new String(buf, 0, len));
- recycle(buf);
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
+ left = mt.breakText(0, len, true, avail);
} else {
- return "";
- }
- }
-
- if (where == TruncateAt.START) {
- sum = 0;
- int i;
-
- for (i = len; i >= 0; i--) {
- float w = wid[len + i - 1];
-
- if (w + sum + ellipsiswid > avail) {
- break;
- }
-
- sum += w;
- }
-
- if (callback != null) {
- callback.ellipsized(0, i);
- }
-
- if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, 0, i));
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(1, text, i, len);
-
- return out;
- }
- } else if (where == TruncateAt.END) {
- sum = 0;
- int i;
-
- for (i = 0; i < len; i++) {
- float w = wid[len + i];
-
- if (w + sum + ellipsiswid > avail) {
- break;
- }
-
- sum += w;
- }
-
- if (callback != null) {
- callback.ellipsized(i, len);
- }
-
- if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, i, len));
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(0, text, 0, i);
-
- return out;
- }
- } else /* where = TruncateAt.MIDDLE */ {
- float lsum = 0, rsum = 0;
- int left = 0, right = len;
-
- float ravail = (avail - ellipsiswid) / 2;
- for (right = len; right >= 0; right--) {
- float w = wid[len + right - 1];
-
- if (w + rsum > ravail) {
- break;
- }
-
- rsum += w;
- }
-
- float lavail = avail - ellipsiswid - rsum;
- for (left = 0; left < right; left++) {
- float w = wid[len + left];
-
- if (w + lsum > lavail) {
- break;
- }
-
- lsum += w;
+ right = len - mt.breakText(0, len, false, avail / 2);
+ avail -= mt.measure(right, len);
+ left = mt.breakText(0, right, true, avail);
}
if (callback != null) {
callback.ellipsized(left, right);
}
+ char[] buf = mt.mChars;
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+
+ int remaining = len - (right - left);
if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, left, right));
+ if (remaining > 0) { // else eliminate the ellipsis too
+ buf[left++] = '\u2026';
+ }
+ for (int i = left; i < right; i++) {
+ buf[i] = '\uFEFF';
+ }
+ String s = new String(buf, 0, len);
+ if (sp == null) {
+ return s;
+ }
+ SpannableString ss = new SpannableString(s);
copySpansFrom(sp, 0, len, Object.class, ss, 0);
return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(0, text, 0, left);
- out.insert(out.length(), text, right, len);
-
- return out;
}
- }
- }
- private static String blank(CharSequence source, int start, int end) {
- int len = source.length();
- char[] buf = obtain(len);
-
- if (start != 0) {
- getChars(source, 0, start, buf, 0);
- }
- if (end != len) {
- getChars(source, end, len, buf, end);
- }
-
- if (start != end) {
- buf[start] = '\u2026';
-
- for (int i = start + 1; i < end; i++) {
- buf[i] = '\uFEFF';
+ if (remaining == 0) {
+ return "";
}
- }
-
- String ret = new String(buf, 0, len);
- recycle(buf);
- return ret;
+ if (sp == null) {
+ StringBuilder sb = new StringBuilder(remaining + sEllipsis.length());
+ sb.append(buf, 0, left);
+ sb.append(sEllipsis);
+ sb.append(buf, right, len - right);
+ return sb.toString();
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ ssb.append(text, 0, left);
+ ssb.append(sEllipsis);
+ ssb.append(text, right, len);
+ return ssb;
+ } finally {
+ MeasuredText.recycle(mt);
+ }
}
/**
@@ -1278,80 +1093,121 @@ public class TextUtils {
TextPaint p, float avail,
String oneMore,
String more) {
- int len = text.length();
- char[] buf = new char[len];
- TextUtils.getChars(text, 0, len, buf, 0);
- int commaCount = 0;
- for (int i = 0; i < len; i++) {
- if (buf[i] == ',') {
- commaCount++;
- }
- }
-
- float[] wid;
-
- if (text instanceof Spanned) {
- Spanned sp = (Spanned) text;
- TextPaint temppaint = new TextPaint();
- wid = new float[len * 2];
-
- int next;
- for (int i = 0; i < len; i = next) {
- next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
-
- Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
- System.arraycopy(wid, 0, wid, len + i, next - i);
+ MeasuredText mt = MeasuredText.obtain();
+ try {
+ int len = text.length();
+ float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR);
+ if (width <= avail) {
+ return text;
}
- System.arraycopy(wid, len, wid, 0, len);
- } else {
- wid = new float[len];
- p.getTextWidths(text, 0, len, wid);
- }
+ char[] buf = mt.mChars;
- int ok = 0;
- int okRemaining = commaCount + 1;
- String okFormat = "";
-
- int w = 0;
- int count = 0;
-
- for (int i = 0; i < len; i++) {
- w += wid[i];
-
- if (buf[i] == ',') {
- count++;
-
- int remaining = commaCount - count + 1;
- float moreWid;
- String format;
-
- if (remaining == 1) {
- format = " " + oneMore;
- } else {
- format = " " + String.format(more, remaining);
- }
-
- moreWid = p.measureText(format);
-
- if (w + moreWid <= avail) {
- ok = i + 1;
- okRemaining = remaining;
- okFormat = format;
+ int commaCount = 0;
+ for (int i = 0; i < len; i++) {
+ if (buf[i] == ',') {
+ commaCount++;
}
}
- }
- if (w <= avail) {
- return text;
- } else {
+ int remaining = commaCount + 1;
+
+ int ok = 0;
+ int okRemaining = remaining;
+ String okFormat = "";
+
+ int w = 0;
+ int count = 0;
+ float[] widths = mt.mWidths;
+
+ int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR :
+ Layout.DIR_REQUEST_RTL;
+
+ MeasuredText tempMt = MeasuredText.obtain();
+ for (int i = 0; i < len; i++) {
+ w += widths[i];
+
+ if (buf[i] == ',') {
+ count++;
+
+ String format;
+ // XXX should not insert spaces, should be part of string
+ // XXX should use plural rules and not assume English plurals
+ if (--remaining == 1) {
+ format = " " + oneMore;
+ } else {
+ format = " " + String.format(more, remaining);
+ }
+
+ // XXX this is probably ok, but need to look at it more
+ tempMt.setPara(format, 0, format.length(), request);
+ float moreWid = mt.addStyleRun(p, mt.mLen, null);
+
+ if (w + moreWid <= avail) {
+ ok = i + 1;
+ okRemaining = remaining;
+ okFormat = format;
+ }
+ }
+ }
+ MeasuredText.recycle(tempMt);
+
SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
out.insert(0, text, 0, ok);
return out;
+ } finally {
+ MeasuredText.recycle(mt);
}
}
+ private static float setPara(MeasuredText mt, TextPaint paint,
+ CharSequence text, int start, int end, int bidiRequest) {
+
+ mt.setPara(text, start, end, bidiRequest);
+
+ float width;
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+ int len = end - start;
+ if (sp == null) {
+ width = mt.addStyleRun(paint, len, null);
+ } else {
+ width = 0;
+ int spanEnd;
+ for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
+ spanEnd = sp.nextSpanTransition(spanStart, len,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = sp.getSpans(
+ spanStart, spanEnd, MetricAffectingSpan.class);
+ width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
+ }
+ }
+
+ return width;
+ }
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ /* package */
+ static boolean doesNotNeedBidi(CharSequence s, int start, int end) {
+ for (int i = start; i < end; i++) {
+ if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /* package */
+ static boolean doesNotNeedBidi(char[] text, int start, int len) {
+ for (int i = start, e = i + len; i < e; i++) {
+ if (text[i] >= FIRST_RIGHT_TO_LEFT) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/* package */ static char[] obtain(int len) {
char[] buf;
@@ -1529,7 +1385,7 @@ public class TextUtils {
*/
public static final int CAP_MODE_CHARACTERS
= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
-
+
/**
* Capitalization mode for {@link #getCapsMode}: capitalize the first
* character of all words. This value is explicitly defined to be the same as
@@ -1537,7 +1393,7 @@ public class TextUtils {
*/
public static final int CAP_MODE_WORDS
= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
-
+
/**
* Capitalization mode for {@link #getCapsMode}: capitalize the first
* character of each sentence. This value is explicitly defined to be the same as
@@ -1545,13 +1401,13 @@ public class TextUtils {
*/
public static final int CAP_MODE_SENTENCES
= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
-
+
/**
* Determine what caps mode should be in effect at the current offset in
* the text. Only the mode bits set in reqModes will be
* checked. Note that the caps mode flags here are explicitly defined
* to match those in {@link InputType}.
- *
+ *
* @param cs The text that should be checked for caps modes.
* @param off Location in the text at which to check.
* @param reqModes The modes to be checked: may be any combination of
@@ -1651,7 +1507,7 @@ public class TextUtils {
return mode;
}
-
+
private static Object sLock = new Object();
private static char[] sTemp = null;
}
diff --git a/icu4j/java/android/icu/text/ArabicShaping.java b/icu4j/java/android/icu/text/ArabicShaping.java
new file mode 100644
index 0000000000000..13e2175340a3a
--- /dev/null
+++ b/icu4j/java/android/icu/text/ArabicShaping.java
@@ -0,0 +1,1947 @@
+/*
+*******************************************************************************
+* Copyright (C) 2001-2009, International Business Machines
+* Corporation and others. All Rights Reserved.
+*******************************************************************************
+*/
+
+/*
+ * Ported with minor modifications from ICU4J 4.2's
+ * com.ibm.icu.text.ArabicShaping class.
+ */
+
+package android.icu.text;
+
+
+/**
+ * Shape Arabic text on a character basis.
+ *
+ *
ArabicShaping performs basic operations for "shaping" Arabic text. It is most + * useful for use with legacy data formats and legacy display technology + * (simple terminals). All operations are performed on Unicode characters.
+ * + *Text-based shaping means that some character code points in the text are + * replaced by others depending on the context. It transforms one kind of text + * into another. In comparison, modern displays for Arabic text select + * appropriate, context-dependent font glyphs for each text element, which means + * that they transform text into a glyph vector.
+ * + *Text transformations are necessary when modern display technology is not + * available or when text needs to be transformed to or from legacy formats that + * use "shaped" characters. Since the Arabic script is cursive, connecting + * adjacent letters to each other, computers select images for each letter based + * on the surrounding letters. This usually results in four images per Arabic + * letter: initial, middle, final, and isolated forms. In Unicode, on the other + * hand, letters are normally stored abstract, and a display system is expected + * to select the necessary glyphs. (This makes searching and other text + * processing easier because the same letter has only one code.) It is possible + * to mimic this with text transformations because there are characters in + * Unicode that are rendered as letters with a specific shape + * (or cursive connectivity). They were included for interoperability with + * legacy systems and codepages, and for unsophisticated display systems.
+ * + *A second kind of text transformations is supported for Arabic digits: + * For compatibility with legacy codepages that only include European digits, + * it is possible to replace one set of digits by another, changing the + * character code points. These operations can be performed for either + * Arabic-Indic Digits (U+0660...U+0669) or Eastern (Extended) Arabic-Indic + * digits (U+06f0...U+06f9).
+ * + *Some replacements may result in more or fewer characters (code points). + * By default, this means that the destination buffer may receive text with a + * length different from the source length. Some legacy systems rely on the + * length of the text to be constant. They expect extra spaces to be added + * or consumed either next to the affected character or at the end of the + * text.
+ * @stable ICU 2.0 + * + * @hide + */ +public class ArabicShaping { + private final int options; + private boolean isLogical; // convenience + private boolean spacesRelativeToTextBeginEnd; + private char tailChar; + + public static final ArabicShaping SHAPER = new ArabicShaping( + ArabicShaping.TEXT_DIRECTION_LOGICAL | + ArabicShaping.LENGTH_FIXED_SPACES_NEAR | + ArabicShaping.LETTERS_SHAPE | + ArabicShaping.DIGITS_NOOP); + + /** + * Convert a range of text in the source array, putting the result + * into a range of text in the destination array, and return the number + * of characters written. + * + * @param source An array containing the input text + * @param sourceStart The start of the range of text to convert + * @param sourceLength The length of the range of text to convert + * @param dest The destination array that will receive the result. + * It may beNULL only if destSize is 0.
+ * @param destStart The start of the range of the destination buffer to use.
+ * @param destSize The size (capacity) of the destination buffer.
+ * If destSize is 0, then no output is produced,
+ * but the necessary buffer size is returned ("preflighting"). This
+ * does not validate the text against the options, for example,
+ * if letters are being unshaped, and spaces are being consumed
+ * following lamalef, this will not detect a lamalef without a
+ * corresponding space. An error will be thrown when the actual
+ * conversion is attempted.
+ * @return The number of chars written to the destination buffer.
+ * If an error occurs, then no output was written, or it may be
+ * incomplete.
+ * @throws ArabicShapingException if the text cannot be converted according to the options.
+ * @stable ICU 2.0
+ */
+ public int shape(char[] source, int sourceStart, int sourceLength,
+ char[] dest, int destStart, int destSize) throws ArabicShapingException {
+ if (source == null) {
+ throw new IllegalArgumentException("source can not be null");
+ }
+ if (sourceStart < 0 || sourceLength < 0 || sourceStart + sourceLength > source.length) {
+ throw new IllegalArgumentException("bad source start (" + sourceStart +
+ ") or length (" + sourceLength +
+ ") for buffer of length " + source.length);
+ }
+ if (dest == null && destSize != 0) {
+ throw new IllegalArgumentException("null dest requires destSize == 0");
+ }
+ if ((destSize != 0) &&
+ (destStart < 0 || destSize < 0 || destStart + destSize > dest.length)) {
+ throw new IllegalArgumentException("bad dest start (" + destStart +
+ ") or size (" + destSize +
+ ") for buffer of length " + dest.length);
+ }
+ /* Validate input options */
+ if ( ((options&TASHKEEL_MASK) > 0) &&
+ !(((options & TASHKEEL_MASK)==TASHKEEL_BEGIN) ||
+ ((options & TASHKEEL_MASK)==TASHKEEL_END ) ||
+ ((options & TASHKEEL_MASK)==TASHKEEL_RESIZE )||
+ ((options & TASHKEEL_MASK)==TASHKEEL_REPLACE_BY_TATWEEL)) ){
+ throw new IllegalArgumentException("Wrong Tashkeel argument");
+ }
+
+ ///CLOVER:OFF
+ //According to Steven Loomis, the code is unreachable when you OR all the constants within the if statements
+ if(((options&LAMALEF_MASK) > 0)&&
+ !(((options & LAMALEF_MASK)==LAMALEF_BEGIN) ||
+ ((options & LAMALEF_MASK)==LAMALEF_END ) ||
+ ((options & LAMALEF_MASK)==LAMALEF_RESIZE )||
+ ((options & LAMALEF_MASK)==LAMALEF_AUTO) ||
+ ((options & LAMALEF_MASK)==LAMALEF_NEAR))){
+ throw new IllegalArgumentException("Wrong Lam Alef argument");
+ }
+ ///CLOVER:ON
+
+ /* Validate Tashkeel (Tashkeel replacement options should be enabled in shaping mode only)*/
+ if(((options&TASHKEEL_MASK) > 0) && (options&LETTERS_MASK) == LETTERS_UNSHAPE) {
+ throw new IllegalArgumentException("Tashkeel replacement should not be enabled in deshaping mode ");
+ }
+ return internalShape(source, sourceStart, sourceLength, dest, destStart, destSize);
+ }
+
+ /**
+ * Convert a range of text in place. This may only be used if the Length option
+ * does not grow or shrink the text.
+ *
+ * @param source An array containing the input text
+ * @param start The start of the range of text to convert
+ * @param length The length of the range of text to convert
+ * @throws ArabicShapingException if the text cannot be converted according to the options.
+ * @stable ICU 2.0
+ */
+ public void shape(char[] source, int start, int length) throws ArabicShapingException {
+ if ((options & LAMALEF_MASK) == LAMALEF_RESIZE) {
+ throw new ArabicShapingException("Cannot shape in place with length option resize.");
+ }
+ shape(source, start, length, source, start, length);
+ }
+
+ /**
+ * Convert a string, returning the new string.
+ *
+ * @param text the string to convert
+ * @return the converted string
+ * @throws ArabicShapingException if the string cannot be converted according to the options.
+ * @stable ICU 2.0
+ */
+ public String shape(String text) throws ArabicShapingException {
+ char[] src = text.toCharArray();
+ char[] dest = src;
+ if (((options & LAMALEF_MASK) == LAMALEF_RESIZE) &&
+ ((options & LETTERS_MASK) == LETTERS_UNSHAPE)) {
+
+ dest = new char[src.length * 2]; // max
+ }
+ int len = shape(src, 0, src.length, dest, 0, dest.length);
+
+ return new String(dest, 0, len);
+ }
+
+ /**
+ * Construct ArabicShaping using the options flags.
+ * The flags are as follows:COPYRIGHT AND PERMISSION NOTICE
+ ++Copyright (c) 1995-2006 International Business Machines Corporation and others +
++All rights reserved. +
++Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies +of the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. +
++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, +OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. +
++Except as contained in this notice, the name of a copyright holder shall not be +used in advertising or otherwise to promote the sale, use or other dealings in +this Software without prior written authorization of the copyright holder. +
+ ++All trademarks and registered trademarks mentioned herein are the property of their respective owners. +
+ +