package android.text;
import android.annotation.IntDef;
-import android.emoji.EmojiFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.text.style.ReplacementSpan;
import android.text.style.TabStopSpan;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
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;
+ /** @hide */
+ @IntDef({JUSTIFICATION_MODE_NONE, JUSTIFICATION_MODE_INTER_WORD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JustificationMode {}
- static {
- if (EMOJI_FACTORY != null) {
- MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
- MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
- } else {
- MIN_EMOJI = -1;
- MAX_EMOJI = -1;
- }
- }
+ /**
+ * Value for justification mode indicating no justification.
+ */
+ public static final int JUSTIFICATION_MODE_NONE = 0;
/**
- * Return how wide a layout must be in order to display the
- * specified text with one line per paragraph.
+ * Value for justification mode indicating the text is justified by stretching word spacing.
+ */
+ public static final int JUSTIFICATION_MODE_INTER_WORD = 1;
+
+ /**
+ * Return how wide a layout must be in order to display the specified text with one line per
+ * paragraph.
+ *
+ * <p>As of O, Uses
+ * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
+ * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
*/
public static float getDesiredWidth(CharSequence source,
TextPaint paint) {
}
/**
+ * Return how wide a layout must be in order to display the specified text slice with one
+ * line per paragraph.
+ *
+ * <p>As of O, Uses
+ * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
+ * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
+ */
+ public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) {
+ return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+ }
+
+ /**
* Return how wide a layout must be in order to display the
* specified text slice with one line per paragraph.
+ *
+ * @hide
*/
- public static float getDesiredWidth(CharSequence source,
- int start, int end,
- TextPaint paint) {
+ public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
+ TextDirectionHeuristic textDir) {
float need = 0;
int next;
next = end;
// note, omits trailing paragraph char
- float w = measurePara(paint, source, i, next);
+ float w = measurePara(paint, source, i, next, textDir);
if (w > need)
need = w;
mTextDir = textDir;
}
+ /** @hide */
+ protected void setJustificationMode(@JustificationMode int justificationMode) {
+ mJustificationMode = justificationMode;
+ }
+
/**
* Replace constructor properties of this Layout with new ones. Be careful.
*/
drawText(canvas, firstLine, lastLine);
}
+ private boolean isJustificationRequired(int lineNum) {
+ if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
+ final int lineEnd = getLineEnd(lineNum);
+ return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
+ }
+
+ private float getJustifyWidth(int lineNum) {
+ Alignment paraAlign = mAlignment;
+ TabStops tabStops = null;
+ boolean tabStopsIsInitialized = false;
+
+ int left = 0;
+ int right = mWidth;
+
+ final int dir = getParagraphDirection(lineNum);
+
+ ParagraphStyle[] spans = NO_PARA_SPANS;
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ final int start = getLineStart(lineNum);
+
+ final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
+
+ if (isFirstParaLine) {
+ final int spanEnd = sp.nextSpanTransition(start, mText.length(),
+ ParagraphStyle.class);
+ spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
+
+ for (int n = spans.length - 1; n >= 0; n--) {
+ if (spans[n] instanceof AlignmentSpan) {
+ paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
+ break;
+ }
+ }
+ }
+
+ final int length = spans.length;
+ boolean useFirstLineMargin = isFirstParaLine;
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan2) {
+ int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
+ int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
+ if (lineNum < startLine + count) {
+ useFirstLineMargin = true;
+ break;
+ }
+ }
+ }
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan) {
+ LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ right -= margin.getLeadingMargin(useFirstLineMargin);
+ } else {
+ left += margin.getLeadingMargin(useFirstLineMargin);
+ }
+ }
+ }
+ }
+
+ if (getLineContainsTab(lineNum)) {
+ tabStops = new TabStops(TAB_INCREMENT, spans);
+ }
+
+ final Alignment align;
+ if (paraAlign == Alignment.ALIGN_LEFT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
+ } else if (paraAlign == Alignment.ALIGN_RIGHT) {
+ align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
+ } else {
+ align = paraAlign;
+ }
+
+ final int indentWidth;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ } else {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ }
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ } else {
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ }
+ } else { // Alignment.ALIGN_CENTER
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
+ }
+
+ return right - left - indentWidth;
+ }
+
/**
* @hide
*/
int previousLineEnd = getLineStart(firstLine);
ParagraphStyle[] spans = NO_PARA_SPANS;
int spanEnd = 0;
- TextPaint paint = mPaint;
+ final TextPaint paint = mPaint;
CharSequence buf = mText;
Alignment paraAlign = mAlignment;
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
+ final boolean justify = isJustificationRequired(lineNum);
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
int ltop = previousLineBottom;
}
}
- 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 {
}
int x;
+ final int indentWidth;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
- x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ x = left + indentWidth;
} else {
- x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ x = right - indentWidth;
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
- x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
+ x = right - max - indentWidth;
} else {
- x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
+ x = left - max + indentWidth;
}
} else { // Alignment.ALIGN_CENTER
+ indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
max = max & ~1;
- x = ((right + left - max) >> 1) +
- getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
+ x = ((right + left - max) >> 1) + indentWidth;
}
}
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 && !justify) {
// 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);
+ if (justify) {
+ tl.justify(right - left - indentWidth);
+ }
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
}
/**
+ * Return the total height of this layout.
+ *
+ * @param cap if true and max lines is set, returns the height of the layout at the max lines.
+ *
+ * @hide
+ */
+ public int getHeight(boolean cap) {
+ return getHeight();
+ }
+
+ /**
* Return the base alignment of this layout.
*/
public final Alignment getAlignment() {
/**
* 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);
}
/**
+ * 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
if (limit > lineEnd) {
limit = lineEnd;
}
+ if (limit == start) {
+ continue;
+ }
level[limit - lineStart - 1] =
(byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
}
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.
+ * 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
TextLine.recycle(tl);
if (clamped) {
- for (int offset = 0; offset <= wid.length; ++offset) {
+ for (int offset = 0; offset < wid.length; ++offset) {
if (wid[offset] > mWidth) {
wid[offset] = mWidth;
}
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);
+ mPaint.setHyphenEdit(getHyphen(line));
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
+ if (isJustificationRequired(line)) {
+ tl.justify(getJustifyWidth(line));
+ }
float width = tl.metrics(null);
+ mPaint.setHyphenEdit(0);
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);
+ mPaint.setHyphenEdit(getHyphen(line));
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
+ if (isJustificationRequired(line)) {
+ tl.justify(getJustifyWidth(line));
+ }
float width = tl.metrics(null);
+ mPaint.setHyphenEdit(0);
TextLine.recycle(tl);
return width;
}
low = guess;
}
- if (low < 0)
+ if (low < 0) {
return 0;
- else
+ } else {
return low;
+ }
}
/**
* 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);
- Directions dirs = getLineDirections(line);
+ final int lineEndOffset = getLineEnd(line);
+ final int lineStartOffset = getLineStart(line);
- if (line == getLineCount() - 1)
- max++;
+ Directions dirs = getLineDirections(line);
+ 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);
- int best = min;
- float bestdist = Math.abs(horizontal.get(best) - horiz);
+ new HorizontalMeasurementProvider(line, primary);
+
+ 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 (horizontal.get(adguess) * swap >= horiz * swap)
+ if (horizontal.get(adguess) * swap >= horiz * swap) {
high = guess;
- else
+ } else {
low = guess;
+ }
}
if (low < here + 1)
low = here + 1;
if (low < there) {
- low = getOffsetAtStartOf(low);
-
- float dist = Math.abs(horizontal.get(low) - horiz);
-
- int aft = TextUtils.getOffsetAfter(mText, low);
- if (aft < there) {
- float other = Math.abs(horizontal.get(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;
+ }
}
}
best = max;
}
+ TextLine.recycle(tl);
return best;
}
*/
private class HorizontalMeasurementProvider {
private final int mLine;
+ private final boolean mPrimary;
private float[] mHorizontals;
private int mLineStartOffset;
- HorizontalMeasurementProvider(final int line) {
+ HorizontalMeasurementProvider(final int line, final boolean primary) {
mLine = line;
+ mPrimary = primary;
init();
}
return;
}
- mHorizontals = getLineHorizontals(mLine, false, true);
+ mHorizontals = getLineHorizontals(mLine, false, mPrimary);
mLineStartOffset = getLineStart(mLine);
}
float get(final int offset) {
if (mHorizontals == null || offset < mLineStartOffset
|| offset >= mLineStartOffset + mHorizontals.length) {
- return getPrimaryHorizontal(offset);
+ return getHorizontal(offset, mPrimary);
} else {
return mHorizontals[offset - mLineStartOffset];
}
return end - 1;
}
- // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
- if (!(ch == ' ' || ch == '\t' || ch == 0x1680 ||
- (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) ||
- ch == 0x205F || ch == 0x3000)) {
+ if (!TextLine.isLineEndSpace(ch)) {
break;
}
}
/* package */
- static float measurePara(TextPaint paint, CharSequence text, int start, int end) {
-
+ static float measurePara(TextPaint paint, CharSequence text, int start, int end,
+ TextDirectionHeuristic textDir) {
MeasuredText mt = MeasuredText.obtain();
TextLine tl = TextLine.obtain();
try {
- mt.setPara(text, start, end, TextDirectionHeuristics.LTR, null);
+ mt.setPara(text, start, end, textDir, null);
Directions directions;
int dir;
if (mt.mEasy) {
}
}
tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
- return margin + tl.metrics(null);
+ return margin + Math.abs(tl.metrics(null));
} finally {
TextLine.recycle(tl);
MeasuredText.recycle(mt);
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) {
// To simply test for an RTL direction, test the bit using
// DIR_RTL_FLAG, if set then the direction is rtl.
- /* package */ int[] mDirections;
- /* package */ Directions(int[] dirs) {
+ /**
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public int[] mDirections;
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public Directions(int[] dirs) {
mDirections = dirs;
}
}
private boolean mSpannedText;
private TextDirectionHeuristic mTextDir;
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
+ private int mJustificationMode;
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;
private static final int TAB_INCREMENT = 20;
- /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
new Directions(new int[] { 0, RUN_LENGTH_MASK });
- /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
}