diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 38c2477350004..2dc3f605873ae 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -2059,11 +2059,9 @@ public abstract class Layout { final String ellipsisString = TextUtils.getEllipsisString(method); final int ellipsisStringLen = ellipsisString.length(); - // Use the ellipsis string only if there are that at least as many characters to replace. - final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen; for (int i = 0; i < ellipsisCount; i++) { final char c; - if (useEllipsisString && i < ellipsisStringLen) { + if (i < ellipsisStringLen && ellipsisCount <= ellipsisStringLen) { c = ellipsisString.charAt(i); } else { c = TextUtils.ELLIPSIS_FILLER; diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index e66e475b3a236..3474c2b560908 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -720,7 +720,7 @@ public class StaticLayout extends Layout { // TODO: Support more justification mode, e.g. letter spacing, stretching. b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE); if (mLeftIndents != null || mRightIndents != null) { - // TODO(performance): it would be better to do this once per layout rather + // TODO(raph) performance: it would be better to do this once per layout rather // than once per paragraph, but that would require a change to the native // interface. int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; @@ -807,7 +807,7 @@ public class StaticLayout extends Layout { width += widths[j]; } } - flag |= flags[i] & TAB_MASK; // XXX May need to also have starting hyphen edit + flag |= flags[i] & TAB_MASK; } // Treat the last line and overflowed lines as a single line. breaks[remainingLineCount - 1] = breaks[breakCount - 1]; @@ -961,25 +961,6 @@ public class StaticLayout extends Layout { bottom = fm.bottom; } - // Information about hyphenation, tabs, and directions are needed for determining - // ellipsization, so the values should be assigned before ellipsization. - - // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining - // one bit for start field - lines[off + TAB] = flags & TAB_MASK; - lines[off + HYPHEN] = flags; - - lines[off + DIR] |= dir << DIR_SHIFT; - // easy means all chars < the first RTL, so no emoji, no nothing - // XXX a run with no text or all spaces is easy but might be an empty - // RTL paragraph. Make sure easy is false if this is the case. - if (easy) { - mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT; - } else { - mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, - start - widthStart, end - start); - } - boolean firstLine = (j == 0); boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); @@ -995,8 +976,8 @@ public class StaticLayout extends Layout { ellipsize == TextUtils.TruncateAt.END); if (doEllipsis) { calculateEllipsis(start, end, widths, widthStart, - ellipsisWidth - getTotalInsets(j), ellipsize, j, - textWidth, paint, forceEllipsis, dir); + ellipsisWidth, ellipsize, j, + textWidth, paint, forceEllipsis); } } @@ -1048,7 +1029,7 @@ public class StaticLayout extends Layout { extra = 0; } - lines[off + START] |= start; + lines[off + START] = start; lines[off + TOP] = v; lines[off + DESCENT] = below + extra; lines[off + EXTRA] = extra; @@ -1066,13 +1047,33 @@ public class StaticLayout extends Layout { lines[off + mColumns + START] = end; lines[off + mColumns + TOP] = v; + // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining + // one bit for start field + lines[off + TAB] |= flags & TAB_MASK; + lines[off + HYPHEN] = flags; + + lines[off + DIR] |= dir << DIR_SHIFT; + Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; + // easy means all chars < the first RTL, so no emoji, no nothing + // XXX a run with no text or all spaces is easy but might be an empty + // RTL paragraph. Make sure easy is false if this is the case. + if (easy) { + mLineDirections[j] = linedirs; + } else { + mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, + start - widthStart, end - start); + } + mLineCount++; return v; } - private void calculateEllipsis(int lineStart, int lineEnd, float[] widths, int widthStart, - float avail, TextUtils.TruncateAt where, int line, float textWidth, TextPaint paint, - boolean forceEllipsis, int dir) { + private void calculateEllipsis(int lineStart, int lineEnd, + float[] widths, int widthStart, + float avail, TextUtils.TruncateAt where, + int line, float textWidth, TextPaint paint, + boolean forceEllipsis) { + avail -= getTotalInsets(line); if (textWidth <= avail && !forceEllipsis) { // Everything fits! mLines[mColumns * line + ELLIPSIS_START] = 0; @@ -1080,35 +1081,11 @@ public class StaticLayout extends Layout { return; } - float tempAvail = avail; - while (true) { - float ellipsizedWidth = guessEllipsis(lineStart, lineEnd, widths, widthStart, tempAvail, - where, line, textWidth, paint, forceEllipsis, dir); - if (ellipsizedWidth <= avail) { - mEllipsized = true; - return; // The ellipsized line fits. - } else { - // Some side effect of ellipsization has caused the text to go over the available - // width. Let's make the available width shorter by exactly that amount and retry. - tempAvail -= ellipsizedWidth - avail; - } - } - } + float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); + int ellipsisStart = 0; + int ellipsisCount = 0; + int len = lineEnd - lineStart; - // Returns the width of the ellipsized line which in some rare cases can actually be larger - // than 'avail' (due to kerning or other context-based effect of replacement of text by - // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode, - // returns 0 so the caller can stop iterating. - private float guessEllipsis(int lineStart, int lineEnd, float[] widths, int widthStart, - float avail, TextUtils.TruncateAt where, int line, float textWidth, TextPaint paint, - boolean forceEllipsis, int dir) { - final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); - final int ellipsisStart; - final int ellipsisCount; - final int len = lineEnd - lineStart; - final int offset = lineStart - widthStart; - - int hyphen = getHyphen(line); // We only support start ellipsis on a single line if (where == TextUtils.TruncateAt.START) { if (mMaximumVisibleLineCount == 1) { @@ -1116,9 +1093,9 @@ public class StaticLayout extends Layout { int i; for (i = len; i > 0; i--) { - final float w = widths[i - 1 + offset]; + float w = widths[i - 1 + lineStart - widthStart]; if (w + sum + ellipsisWidth > avail) { - while (i < len && widths[i + offset] == 0.0f) { + while (i < len && widths[i + lineStart - widthStart] == 0.0f) { i++; } break; @@ -1129,13 +1106,9 @@ public class StaticLayout extends Layout { ellipsisStart = 0; ellipsisCount = i; - // Strip the potential hyphenation at beginning of line. - hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE; } else { - ellipsisStart = 0; - ellipsisCount = 0; if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Start ellipsis only supported with one line"); + Log.w(TAG, "Start Ellipsis only supported with one line"); } } } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || @@ -1144,7 +1117,7 @@ public class StaticLayout extends Layout { int i; for (i = 0; i < len; i++) { - final float w = widths[i + offset]; + float w = widths[i + lineStart - widthStart]; if (w + sum + ellipsisWidth > avail) { break; @@ -1153,27 +1126,24 @@ public class StaticLayout extends Layout { sum += w; } - if (forceEllipsis && i == len && len > 0) { + ellipsisStart = i; + ellipsisCount = len - i; + if (forceEllipsis && ellipsisCount == 0 && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; - } else { - ellipsisStart = i; - ellipsisCount = len - i; } - // Strip the potential hyphenation at end of line. - hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE; - } else { // where = TextUtils.TruncateAt.MIDDLE - // We only support middle ellipsis on a single line. + } else { + // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line if (mMaximumVisibleLineCount == 1) { float lsum = 0, rsum = 0; int left = 0, right = len; - final float ravail = (avail - ellipsisWidth) / 2; + float ravail = (avail - ellipsisWidth) / 2; for (right = len; right > 0; right--) { - final float w = widths[right - 1 + offset]; + float w = widths[right - 1 + lineStart - widthStart]; if (w + rsum > ravail) { - while (right < len && widths[right + offset] == 0.0f) { + while (right < len && widths[right + lineStart - widthStart] == 0.0f) { right++; } break; @@ -1181,9 +1151,9 @@ public class StaticLayout extends Layout { rsum += w; } - final float lavail = avail - ellipsisWidth - rsum; + float lavail = avail - ellipsisWidth - rsum; for (left = 0; left < right; left++) { - final float w = widths[left + offset]; + float w = widths[left + lineStart - widthStart]; if (w + lsum > lavail) { break; @@ -1195,46 +1165,14 @@ public class StaticLayout extends Layout { ellipsisStart = left; ellipsisCount = right - left; } else { - ellipsisStart = 0; - ellipsisCount = 0; if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Middle ellipsis only supported with one line"); + Log.w(TAG, "Middle Ellipsis only supported with one line"); } } } + mEllipsized = true; mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; - - if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) { - // Unsupported ellipsization mode or all text is ellipsized away. Return 0. - return 0.0f; - } - - final CharSequence text = getText(); - final boolean hasTabs = getLineContainsTab(line); - final TabStops tabStops; - if (hasTabs && text instanceof Spanned) { - final TabStopSpan[] tabs = getParagraphSpans((Spanned) text, lineStart, lineEnd, - TabStopSpan.class); - if (tabs.length == 0) { - tabStops = null; - } else { - tabStops = new TabStops(TAB_INCREMENT, tabs); - } - } else { - tabStops = null; - } - final TextLine textline = TextLine.obtain(); - paint.setHyphenEdit(hyphen); - textline.set(paint, - text, /* this is an instance of Ellipsizer or SpannedEllipsizer */ - lineStart, lineEnd, - dir, getLineDirections(line), - hasTabs, tabStops); - final float hyphenatedWidth = textline.metrics(null); - paint.setHyphenEdit(0); - TextLine.recycle(textline); - return hyphenatedWidth; } private float getTotalInsets(int line) { diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 014b1e4c2da5a..979c43ccfcec5 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -89,8 +89,8 @@ public class TextUtils { /** {@hide} */ @NonNull - public static String getEllipsisString(@NonNull TruncateAt method) { - return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL; + public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) { + return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL; } @@ -1187,11 +1187,9 @@ public class TextUtils { * or, if it does not fit, a truncated * copy with ellipsis character added at the specified edge or center. */ - @NonNull - public static CharSequence ellipsize(@NonNull CharSequence text, - @NonNull TextPaint p, - @FloatRange(from = 0.0) float avail, - @NonNull TruncateAt where) { + public static CharSequence ellipsize(CharSequence text, + TextPaint p, + float avail, TruncateAt where) { return ellipsize(text, p, avail, where, false, null); } @@ -1207,11 +1205,9 @@ public class TextUtils { * report the start and end of the ellipsized range. TextDirection * is determined by the first strong directional character. */ - @NonNull - public static CharSequence ellipsize(@NonNull CharSequence text, - @NonNull TextPaint paint, - @FloatRange(from = 0.0) float avail, - @NonNull TruncateAt where, + public static CharSequence ellipsize(CharSequence text, + TextPaint paint, + float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback) { return ellipsize(text, paint, avail, where, preserveLength, callback, @@ -1232,19 +1228,16 @@ public class TextUtils { * * @hide */ - @NonNull - public static CharSequence ellipsize(@NonNull CharSequence text, - @NonNull TextPaint paint, - @FloatRange(from = 0.0) float avail, - @NonNull TruncateAt where, + public static CharSequence ellipsize(CharSequence text, + TextPaint paint, + float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback, - @NonNull TextDirectionHeuristic textDir, - @NonNull String ellipsis) { + TextDirectionHeuristic textDir, String ellipsis) { - final int len = text.length(); - final MeasuredText mt = MeasuredText.obtain(); - MeasuredText resultMt = null; + int len = text.length(); + + MeasuredText mt = MeasuredText.obtain(); try { float width = setPara(mt, paint, text, 0, text.length(), textDir); @@ -1252,88 +1245,74 @@ public class TextUtils { if (callback != null) { callback.ellipsized(0, 0); } + return text; } - resultMt = MeasuredText.obtain(); - // First estimate of effective width of ellipsis. - float ellipsisWidth = paint.measureText(ellipsis); - while (true) { // Repeat until the ellipsized text fits. - final float remainingWidth = avail - ellipsisWidth; - final int start, end; - if (remainingWidth < 0) { - // Even the ellipsis can't fit. So it all goes. - start = 0; - end = len; - } else if (where == TruncateAt.START) { - start = 0; - end = len - mt.breakText(len, false /* backwards */, remainingWidth); - } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { - start = mt.breakText(len, true /* forwards */, remainingWidth); - end = len; - } else { - end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2); - start = mt.breakText(end, true /* forwards */, - remainingWidth - mt.measure(end, len)); - } + // XXX assumes ellipsis string does not require shaping and + // is unaffected by style + float ellipsiswid = paint.measureText(ellipsis); + avail -= ellipsiswid; - final char[] buf = mt.mChars; - final Spanned sp = text instanceof Spanned ? (Spanned) text : null; - - final CharSequence result; - final int removed = end - start; - final int remaining = len - removed; - if (preserveLength) { - int pos = start; - if (remaining > 0 && removed >= ellipsis.length()) { - ellipsis.getChars(0, ellipsis.length(), buf, start); - pos += ellipsis.length(); - } // else eliminate the ellipsis - while (pos < end) { - buf[pos++] = ELLIPSIS_FILLER; - } - final String s = new String(buf, 0, len); - if (sp == null) { - result = s; - } else { - final SpannableString ss = new SpannableString(s); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - result = ss; - } - } else { - if (remaining == 0) { - result = ""; - } else if (sp == null) { - final StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); - sb.append(buf, 0, start); - sb.append(ellipsis); - sb.append(buf, end, len - end); - result = sb.toString(); - } else { - final SpannableStringBuilder ssb = new SpannableStringBuilder(); - ssb.append(text, 0, start); - ssb.append(ellipsis); - ssb.append(text, end, len); - result = ssb; - } - } - - width = setPara(resultMt, paint, result, 0, result.length(), textDir); - if (width <= avail || remaining == 0) { // Text fits or all text is gone. - if (callback != null) { - callback.ellipsized(start, end); - } - return result; - } else { - // Adjust the width of the ellipsis by adding the amount 'width' is still over. - ellipsisWidth += width - avail; - } + int left = 0; + int right = len; + if (avail < 0) { + // it all goes + } else if (where == TruncateAt.START) { + right = len - mt.breakText(len, false, avail); + } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { + left = mt.breakText(len, true, avail); + } else { + right = len - mt.breakText(len, false, avail / 2); + avail -= mt.measure(right, len); + left = mt.breakText(right, true, avail); } + + if (callback != null) { + callback.ellipsized(left, right); + } + + char[] buf = mt.mChars; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + + final int removed = right - left; + final int remaining = len - removed; + if (preserveLength) { + if (remaining > 0 && removed >= ellipsis.length()) { + ellipsis.getChars(0, ellipsis.length(), buf, left); + left += ellipsis.length(); + } // else skip the ellipsis + for (int i = left; i < right; i++) { + buf[i] = ELLIPSIS_FILLER; + } + 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; + } + + if (remaining == 0) { + return ""; + } + + if (sp == null) { + StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); + sb.append(buf, 0, left); + sb.append(ellipsis); + sb.append(buf, right, len - right); + return sb.toString(); + } + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(text, 0, left); + ssb.append(ellipsis); + ssb.append(text, right, len); + return ssb; } finally { MeasuredText.recycle(mt); - if (resultMt != null) { - MeasuredText.recycle(resultMt); - } } } @@ -1364,6 +1343,7 @@ public class TextUtils { * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"}) * doesn't fit, it will return an empty string. */ + public static CharSequence listEllipsize(@Nullable Context context, @Nullable List elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,