package android.text;
import android.annotation.IntDef;
-import android.emoji.EmojiFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
private static final ParagraphStyle[] NO_PARA_SPANS =
ArrayUtils.emptyArray(ParagraphStyle.class);
- /* package */ static final EmojiFactory EMOJI_FACTORY = EmojiFactory.newAvailableInstance();
- /* package */ static final int MIN_EMOJI, MAX_EMOJI;
-
- static {
- if (EMOJI_FACTORY != null) {
- MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
- MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
- } else {
- MIN_EMOJI = -1;
- MAX_EMOJI = -1;
- }
- }
-
/**
* Return how wide a layout must be in order to display the
* specified text with one line per paragraph.
}
}
- boolean hasTabOrEmoji = getLineContainsTab(lineNum);
+ boolean hasTab = getLineContainsTab(lineNum);
// Can't tell if we have tabs for sure, currently
- if (hasTabOrEmoji && !tabStopsIsInitialized) {
+ if (hasTab && !tabStopsIsInitialized) {
if (tabStops == null) {
tabStops = new TabStops(TAB_INCREMENT, spans);
} else {
paint.setHyphenEdit(getHyphen(lineNum));
Directions directions = getLineDirections(lineNum);
- if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
- tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
+ tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
/**
* Returns whether the specified line contains one or more
- * characters that need to be handled specially, like tabs
- * or emoji.
+ * characters that need to be handled specially, like tabs.
*/
public abstract boolean getLineContainsTab(int line);
return false;
}
+ /**
+ * Returns the range of the run that the character at offset belongs to.
+ * @param offset the offset
+ * @return The range of the run
+ * @hide
+ */
+ public long getRunRange(int offset) {
+ int line = getLineForOffset(offset);
+ Directions dirs = getLineDirections(line);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+ return TextUtils.packRangeInLong(0, getLineEnd(line));
+ }
+ int[] runs = dirs.mDirections;
+ int lineStart = getLineStart(line);
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (offset >= start && offset < limit) {
+ return TextUtils.packRangeInLong(start, limit);
+ }
+ }
+ // Should happen only if the offset is "out of bounds"
+ return TextUtils.packRangeInLong(0, getLineEnd(line));
+ }
+
+ /**
+ * Checks if the trailing BiDi level should be used for an offset
+ *
+ * This method is useful when the offset is at the BiDi level transition point and determine
+ * which run need to be used. For example, let's think about following input: (L* denotes
+ * Left-to-Right characters, R* denotes Right-to-Left characters.)
+ * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
+ * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6
+ *
+ * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here
+ * since they are at the BiDi transition point. In Android, the offset is considered to be
+ * associated with the trailing run if the BiDi level of the trailing run is higher than of the
+ * previous run. In this case, the BiDi level of the input text is as follows:
+ *
+ * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
+ * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
+ * BiDi Level: 0 0 0 1 1 1 0 0 0
+ *
+ * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi
+ * level of Run 1 is higher than the level of Run 0. Similarly, the offset = 6 is a part of Run
+ * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher
+ * than the level of Run 2.
+ *
+ * @returns true if offset is at the BiDi level transition point and trailing BiDi level is
+ * higher than previous BiDi level. See above for the detail.
+ */
private boolean primaryIsTrailingPrevious(int offset) {
int line = getLineForOffset(offset);
int lineStart = getLineStart(line);
}
/**
+ * Computes in linear time the results of calling
+ * #primaryIsTrailingPrevious for all offsets on a line.
+ * @param line The line giving the offsets we compute the information for
+ * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
+ */
+ private boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int[] runs = getLineDirections(line).mDirections;
+
+ boolean[] trailing = new boolean[lineEnd - lineStart + 1];
+
+ byte[] level = new byte[lineEnd - lineStart + 1];
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ level[limit - lineStart - 1] =
+ (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+ }
+
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+ trailing[start - lineStart] = currentLevel > (start == lineStart
+ ? (getParagraphDirection(line) == 1 ? 0 : 1)
+ : level[start - lineStart - 1]);
+ }
+
+ return trailing;
+ }
+
+ /**
* Get the primary horizontal position for the specified text offset.
* This is the location where a new character would be inserted in
* the paragraph's primary direction.
return getHorizontal(offset, !trailing, clamped);
}
+ private float getHorizontal(int offset, boolean primary) {
+ return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
+ }
+
private float getHorizontal(int offset, boolean trailing, boolean clamped) {
int line = getLineForOffset(offset);
int start = getLineStart(line);
int end = getLineEnd(line);
int dir = getParagraphDirection(line);
- boolean hasTabOrEmoji = getLineContainsTab(line);
+ boolean hasTab = getLineContainsTab(line);
Directions directions = getLineDirections(line);
TabStops tabStops = null;
- if (hasTabOrEmoji && mText instanceof Spanned) {
+ if (hasTab && mText instanceof Spanned) {
// Just checking this line should be good enough, tabs should be
// consistent across all lines in a paragraph.
TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
}
TextLine tl = TextLine.obtain();
- tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
+ tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
float wid = tl.measure(offset - start, trailing, null);
TextLine.recycle(tl);
}
/**
+ * Computes in linear time the results of calling
+ * #getHorizontal for all offsets on a line.
+ * @param line The line giving the offsets we compute information for
+ * @param clamped Whether to clamp the results to the width of the layout
+ * @param primary Whether the results should be the primary or the secondary horizontal
+ * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
+ */
+ private float[] getLineHorizontals(int line, boolean clamped, boolean primary) {
+ int start = getLineStart(line);
+ int end = getLineEnd(line);
+ int dir = getParagraphDirection(line);
+ boolean hasTab = getLineContainsTab(line);
+ Directions directions = getLineDirections(line);
+
+ TabStops tabStops = null;
+ if (hasTab && mText instanceof Spanned) {
+ // Just checking this line should be good enough, tabs should be
+ // consistent across all lines in a paragraph.
+ TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
+ if (tabs.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
+ }
+ }
+
+ TextLine tl = TextLine.obtain();
+ tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
+ boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line);
+ if (!primary) {
+ for (int offset = 0; offset < trailings.length; ++offset) {
+ trailings[offset] = !trailings[offset];
+ }
+ }
+ float[] wid = tl.measureAllOffsets(trailings, null);
+ TextLine.recycle(tl);
+
+ if (clamped) {
+ for (int offset = 0; offset <= wid.length; ++offset) {
+ if (wid[offset] > mWidth) {
+ wid[offset] = mWidth;
+ }
+ }
+ }
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+
+ int lineStartPos = getLineStartPos(line, left, right);
+ float[] horizontal = new float[end - start + 1];
+ for (int offset = 0; offset < horizontal.length; ++offset) {
+ horizontal[offset] = lineStartPos + wid[offset];
+ }
+ return horizontal;
+ }
+
+ /**
* Get the leftmost position that should be exposed for horizontal
* scrolling on the specified line.
*/
int start = getLineStart(line);
int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
- boolean hasTabsOrEmoji = getLineContainsTab(line);
+ boolean hasTabs = getLineContainsTab(line);
TabStops tabStops = null;
- if (hasTabsOrEmoji && mText instanceof Spanned) {
+ if (hasTabs && mText instanceof Spanned) {
// Just checking this line should be good enough, tabs should be
// consistent across all lines in a paragraph.
TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
int dir = getParagraphDirection(line);
TextLine tl = TextLine.obtain();
- tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
float width = tl.metrics(null);
TextLine.recycle(tl);
return width;
private float getLineExtent(int line, TabStops tabStops, boolean full) {
int start = getLineStart(line);
int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
- boolean hasTabsOrEmoji = getLineContainsTab(line);
+ boolean hasTabs = getLineContainsTab(line);
Directions directions = getLineDirections(line);
int dir = getParagraphDirection(line);
TextLine tl = TextLine.obtain();
- tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
float width = tl.metrics(null);
TextLine.recycle(tl);
return width;
* closest to the specified horizontal position.
*/
public int getOffsetForHorizontal(int line, float horiz) {
+ return getOffsetForHorizontal(line, horiz, true);
+ }
+
+ /**
+ * Get the character offset on the specified line whose position is
+ * closest to the specified horizontal position.
+ *
+ * @param line the line used to find the closest offset
+ * @param horiz the horizontal position used to find the closest offset
+ * @param primary whether to use the primary position or secondary position to find the offset
+ *
+ * @hide
+ */
+ public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
// TODO: use Paint.getOffsetForAdvance to avoid binary search
- int max = getLineEnd(line) - 1;
- int min = getLineStart(line);
+ final int lineEndOffset = getLineEnd(line);
+ final int lineStartOffset = getLineStart(line);
+
Directions dirs = getLineDirections(line);
- if (line == getLineCount() - 1)
- max++;
+ TextLine tl = TextLine.obtain();
+ // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
+ tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
+ false, null);
+ final HorizontalMeasurementProvider horizontal =
+ new HorizontalMeasurementProvider(line, primary);
- int best = min;
- float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
+ final int max;
+ if (line == getLineCount() - 1) {
+ max = lineEndOffset;
+ } else {
+ max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
+ !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
+ }
+ int best = lineStartOffset;
+ float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz);
for (int i = 0; i < dirs.mDirections.length; i += 2) {
- int here = min + dirs.mDirections[i];
+ int here = lineStartOffset + dirs.mDirections[i];
int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
- int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
+ boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0;
+ int swap = isRtl ? -1 : 1;
if (there > max)
there = max;
guess = (high + low) / 2;
int adguess = getOffsetAtStartOf(guess);
- if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
+ if (horizontal.get(adguess) * swap >= horiz * swap)
high = guess;
else
low = guess;
low = here + 1;
if (low < there) {
- low = getOffsetAtStartOf(low);
-
- float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
-
- int aft = TextUtils.getOffsetAfter(mText, low);
- if (aft < there) {
- float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
-
- if (other < dist) {
- dist = other;
- low = aft;
+ int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
+ low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
+ if (low >= here && low < there) {
+ float dist = Math.abs(horizontal.get(low) - horiz);
+ if (aft < there) {
+ float other = Math.abs(horizontal.get(aft) - horiz);
+
+ if (other < dist) {
+ dist = other;
+ low = aft;
+ }
}
- }
- if (dist < bestdist) {
- bestdist = dist;
- best = low;
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = low;
+ }
}
}
- float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
+ float dist = Math.abs(horizontal.get(here) - horiz);
if (dist < bestdist) {
bestdist = dist;
}
}
- float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
+ float dist = Math.abs(horizontal.get(max) - horiz);
if (dist <= bestdist) {
bestdist = dist;
best = max;
}
+ TextLine.recycle(tl);
return best;
}
/**
+ * Responds to #getHorizontal queries, by selecting the better strategy between:
+ * - calling #getHorizontal explicitly for each query
+ * - precomputing all #getHorizontal measurements, and responding to any query in constant time
+ * The first strategy is used for LTR-only text, while the second is used for all other cases.
+ * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other
+ * contexts.
+ */
+ private class HorizontalMeasurementProvider {
+ private final int mLine;
+ private final boolean mPrimary;
+
+ private float[] mHorizontals;
+ private int mLineStartOffset;
+
+ HorizontalMeasurementProvider(final int line, final boolean primary) {
+ mLine = line;
+ mPrimary = primary;
+ init();
+ }
+
+ private void init() {
+ final Directions dirs = getLineDirections(mLine);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
+ return;
+ }
+
+ mHorizontals = getLineHorizontals(mLine, false, mPrimary);
+ mLineStartOffset = getLineStart(mLine);
+ }
+
+ float get(final int offset) {
+ if (mHorizontals == null) {
+ return getHorizontal(offset, mPrimary);
+ } else {
+ return mHorizontals[offset - mLineStartOffset];
+ }
+ }
+ }
+
+ /**
* Return the text offset after the last character on the specified line.
*/
public final int getLineEnd(int line) {
return ArrayUtils.emptyArray(type);
}
- return text.getSpans(start, end, type);
+ if(text instanceof SpannableStringBuilder) {
+ return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
+ } else {
+ return text.getSpans(start, end, type);
+ }
}
private char getEllipsisChar(TextUtils.TruncateAt method) {