Loading core/java/android/text/StaticLayout.java +62 −155 Original line number Diff line number Diff line Loading @@ -928,8 +928,6 @@ public class StaticLayout extends Layout { } } // The parameters that are not changed in the method are marked as final to make the code // easier to understand. private int out(final CharSequence text, final int start, final int end, int above, int below, int top, int bottom, int v, final float spacingmult, final float spacingadd, final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm, Loading Loading @@ -958,21 +956,29 @@ public class StaticLayout extends Layout { mLineDirections = grow; } lines[off + START] = start; lines[off + TOP] = v; if (chooseHt != null) { fm.ascent = above; fm.descent = below; fm.top = top; fm.bottom = bottom; // Information about hyphenation, tabs, and directions are needed for determining // ellipsization, so the values should be assigned before ellipsization. for (int i = 0; i < chooseHt.length; i++) { if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { ((LineHeightSpan.WithDensity) chooseHt[i]) .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); } else { chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); } } // 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; mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); above = fm.ascent; below = fm.descent; top = fm.top; bottom = fm.bottom; } final boolean firstLine = (j == 0); final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); boolean firstLine = (j == 0); boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); if (ellipsize != null) { // If there is only one line, then do any type of ellipsis except when it is MARQUEE Loading @@ -985,9 +991,9 @@ public class StaticLayout extends Layout { (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && ellipsize == TextUtils.TruncateAt.END); if (doEllipsis) { calculateEllipsis(text, start, end, widths, widthStart, ellipsisWidth - getTotalInsets(j), ellipsize, j, textWidth, paint, forceEllipsis, dir); calculateEllipsis(start, end, widths, widthStart, ellipsisWidth, ellipsize, j, textWidth, paint, forceEllipsis); } } Loading @@ -1006,28 +1012,6 @@ public class StaticLayout extends Layout { } } if (chooseHt != null) { fm.ascent = above; fm.descent = below; fm.top = top; fm.bottom = bottom; for (int i = 0; i < chooseHt.length; i++) { if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { ((LineHeightSpan.WithDensity) chooseHt[i]) .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); } else { chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); } } above = fm.ascent; below = fm.descent; top = fm.top; bottom = fm.bottom; } if (firstLine) { if (trackPad) { mTopPadding = top - above; Loading @@ -1038,6 +1022,8 @@ public class StaticLayout extends Layout { } } int extra; if (lastLine) { if (trackPad) { mBottomPadding = bottom - below; Loading @@ -1048,9 +1034,8 @@ public class StaticLayout extends Layout { } } final int extra; if (needMultiply && (addLastLineLineSpacing || !lastLine)) { final double ex = (below - above) * (spacingmult - 1) + spacingadd; double ex = (below - above) * (spacingmult - 1) + spacingadd; if (ex >= 0) { extra = (int)(ex + EXTRA_ROUNDING); } else { Loading @@ -1060,6 +1045,8 @@ public class StaticLayout extends Layout { extra = 0; } lines[off + START] = start; lines[off + TOP] = v; lines[off + DESCENT] = below + extra; lines[off + EXTRA] = extra; Loading @@ -1067,7 +1054,7 @@ public class StaticLayout extends Layout { // store the height as if it was ellipsized if (!mEllipsized && currentLineIsTheLastVisibleOne) { // below calculation as if it was the last line final int maxLineBelow = includePad ? bottom : below; int maxLineBelow = includePad ? bottom : below; // similar to the calculation of v below, without the extra. mMaxLineHeight = v + (maxLineBelow - above); } Loading @@ -1076,13 +1063,23 @@ 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; mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); mLineCount++; return v; } private void calculateEllipsis(CharSequence text, 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; Loading @@ -1090,53 +1087,11 @@ public class StaticLayout extends Layout { return; } float tempAvail = avail; int numberOfTries = 0; boolean lineFits = false; mWorkPaint.set(paint); do { final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths, widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir); if (ellipsizedWidth <= avail) { lineFits = true; } else { numberOfTries++; if (numberOfTries > 10) { // If the text still doesn't fit after ten tries, assume it will never fit and // ellipsize it all. mLines[mColumns * line + ELLIPSIS_START] = 0; mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart; lineFits = true; } 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; } } } while (!lineFits); mEllipsized = true; } 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. // // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it // should not be accessed while the method is running. private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths, int widthStart, float avail, TextUtils.TruncateAt where, int line, TextPaint paint, boolean forceEllipsis, int dir) { final int savedHyphenEdit = paint.getHyphenEdit(); paint.setHyphenEdit(0); 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) { Loading @@ -1144,9 +1099,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; Loading @@ -1157,13 +1112,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 || Loading @@ -1172,7 +1123,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; Loading @@ -1181,27 +1132,24 @@ public class StaticLayout extends Layout { sum += w; } if (forceEllipsis && i == len && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; } else { ellipsisStart = i; ellipsisCount = len - i; if (forceEllipsis && ellipsisCount == 0 && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; } // 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; Loading @@ -1209,9 +1157,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; Loading @@ -1223,53 +1171,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 boolean isSpanned = text instanceof Spanned; final Ellipsizer ellipsizedText = isSpanned ? new SpannedEllipsizer(text) : new Ellipsizer(text); ellipsizedText.mLayout = this; ellipsizedText.mMethod = where; final boolean hasTabs = getLineContainsTab(line); final TabStops tabStops; if (hasTabs && isSpanned) { final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart, lineEnd, TabStopSpan.class); if (tabs.length == 0) { tabStops = null; } else { tabStops = new TabStops(TAB_INCREMENT, tabs); } } else { tabStops = null; } paint.setHyphenEdit(hyphen); final TextLine textline = TextLine.obtain(); textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line), hasTabs, tabStops); // Since TextLine.metric() returns negative values for RTL text, multiplication by dir // converts it to an actual width. Note that we don't want to use the absolute value, // since we may actually have glyphs with negative advances, which by definition always // fit. final float ellipsizedWidth = textline.metrics(null) * dir; TextLine.recycle(textline); paint.setHyphenEdit(savedHyphenEdit); return ellipsizedWidth; } private float getTotalInsets(int line) { Loading Loading @@ -1509,8 +1418,6 @@ public class StaticLayout extends Layout { */ private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; private TextPaint mWorkPaint = new TextPaint(); private static final int COLUMNS_NORMAL = 5; private static final int COLUMNS_ELLIPSIZE = 7; private static final int START = 0; Loading core/java/android/text/TextUtils.java +74 −114 Original line number Diff line number Diff line Loading @@ -88,8 +88,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; } Loading Loading @@ -1194,11 +1194,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); } Loading @@ -1214,11 +1212,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, Loading @@ -1239,19 +1235,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) { int len = text.length(); final int len = text.length(); MeasuredParagraph mt = null; MeasuredParagraph resultMt = null; try { mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); float width = mt.getWholeWidth(); Loading @@ -1260,110 +1253,76 @@ public class TextUtils { if (callback != null) { callback.ellipsized(0, 0); } return text; } // First estimate of effective width of ellipsis. float ellipsisWidth = paint.measureText(ellipsis); int numberOfTries = 0; boolean textFits = false; int start, end; CharSequence result; do { if (avail < ellipsisWidth) { // Even the ellipsis can't fit. So it all goes. start = 0; end = len; } else { final float remainingWidth = avail - ellipsisWidth; if (where == TruncateAt.START) { start = 0; end = len - mt.breakText(len, false /* backwards */, remainingWidth); // XXX assumes ellipsis string does not require shaping and // is unaffected by style float ellipsiswid = paint.measureText(ellipsis); avail -= ellipsiswid; 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) { start = mt.breakText(len, true /* forwards */, remainingWidth); end = len; left = mt.breakText(len, true, avail); } else { end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2); start = mt.breakText(end, true /* forwards */, remainingWidth - mt.measure(end, len)); 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); } final char[] buf = mt.getChars(); final Spanned sp = text instanceof Spanned ? (Spanned) text : null; Spanned sp = text instanceof Spanned ? (Spanned) text : null; final int removed = end - start; final int removed = right - left; 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; 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; } final String s = new String(buf, 0, len); String s = new String(buf, 0, len); if (sp == null) { result = s; } else { final SpannableString ss = new SpannableString(s); return s; } SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); result = ss; return 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; } return ""; } if (remaining == 0) { // All text is gone. textFits = true; } else { resultMt = MeasuredParagraph.buildForMeasurement( paint, result, 0, result.length(), textDir, resultMt); width = resultMt.getWholeWidth(); if (width <= avail) { textFits = true; } else { numberOfTries++; if (numberOfTries > 10) { // If the text still doesn't fit after ten tries, assume it will never // fit and ellipsize it all. We do this by setting the width of the // ellipsis to be positive infinity, so we get to empty text in the next // round. ellipsisWidth = Float.POSITIVE_INFINITY; } else { // Adjust the width of the ellipsis by adding the amount 'width' is // still over. ellipsisWidth += width - avail; } } } } while (!textFits); if (callback != null) { callback.ellipsized(start, end); 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(); } return result; SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(text, 0, left); ssb.append(ellipsis); ssb.append(text, right, len); return ssb; } finally { if (mt != null) { mt.recycle(); } if (resultMt != null) { resultMt.recycle(); } } } Loading Loading @@ -1394,6 +1353,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<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, Loading graphics/java/android/graphics/Paint.java +1 −1 Original line number Diff line number Diff line Loading @@ -2742,7 +2742,7 @@ public class Paint { * @param offset index of caret position * @return width measurement between start and offset */ public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart, public float getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset) { if (text == null) { throw new IllegalArgumentException("text cannot be null"); Loading Loading
core/java/android/text/StaticLayout.java +62 −155 Original line number Diff line number Diff line Loading @@ -928,8 +928,6 @@ public class StaticLayout extends Layout { } } // The parameters that are not changed in the method are marked as final to make the code // easier to understand. private int out(final CharSequence text, final int start, final int end, int above, int below, int top, int bottom, int v, final float spacingmult, final float spacingadd, final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm, Loading Loading @@ -958,21 +956,29 @@ public class StaticLayout extends Layout { mLineDirections = grow; } lines[off + START] = start; lines[off + TOP] = v; if (chooseHt != null) { fm.ascent = above; fm.descent = below; fm.top = top; fm.bottom = bottom; // Information about hyphenation, tabs, and directions are needed for determining // ellipsization, so the values should be assigned before ellipsization. for (int i = 0; i < chooseHt.length; i++) { if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { ((LineHeightSpan.WithDensity) chooseHt[i]) .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); } else { chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); } } // 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; mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); above = fm.ascent; below = fm.descent; top = fm.top; bottom = fm.bottom; } final boolean firstLine = (j == 0); final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); boolean firstLine = (j == 0); boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); if (ellipsize != null) { // If there is only one line, then do any type of ellipsis except when it is MARQUEE Loading @@ -985,9 +991,9 @@ public class StaticLayout extends Layout { (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && ellipsize == TextUtils.TruncateAt.END); if (doEllipsis) { calculateEllipsis(text, start, end, widths, widthStart, ellipsisWidth - getTotalInsets(j), ellipsize, j, textWidth, paint, forceEllipsis, dir); calculateEllipsis(start, end, widths, widthStart, ellipsisWidth, ellipsize, j, textWidth, paint, forceEllipsis); } } Loading @@ -1006,28 +1012,6 @@ public class StaticLayout extends Layout { } } if (chooseHt != null) { fm.ascent = above; fm.descent = below; fm.top = top; fm.bottom = bottom; for (int i = 0; i < chooseHt.length; i++) { if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { ((LineHeightSpan.WithDensity) chooseHt[i]) .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); } else { chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); } } above = fm.ascent; below = fm.descent; top = fm.top; bottom = fm.bottom; } if (firstLine) { if (trackPad) { mTopPadding = top - above; Loading @@ -1038,6 +1022,8 @@ public class StaticLayout extends Layout { } } int extra; if (lastLine) { if (trackPad) { mBottomPadding = bottom - below; Loading @@ -1048,9 +1034,8 @@ public class StaticLayout extends Layout { } } final int extra; if (needMultiply && (addLastLineLineSpacing || !lastLine)) { final double ex = (below - above) * (spacingmult - 1) + spacingadd; double ex = (below - above) * (spacingmult - 1) + spacingadd; if (ex >= 0) { extra = (int)(ex + EXTRA_ROUNDING); } else { Loading @@ -1060,6 +1045,8 @@ public class StaticLayout extends Layout { extra = 0; } lines[off + START] = start; lines[off + TOP] = v; lines[off + DESCENT] = below + extra; lines[off + EXTRA] = extra; Loading @@ -1067,7 +1054,7 @@ public class StaticLayout extends Layout { // store the height as if it was ellipsized if (!mEllipsized && currentLineIsTheLastVisibleOne) { // below calculation as if it was the last line final int maxLineBelow = includePad ? bottom : below; int maxLineBelow = includePad ? bottom : below; // similar to the calculation of v below, without the extra. mMaxLineHeight = v + (maxLineBelow - above); } Loading @@ -1076,13 +1063,23 @@ 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; mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); mLineCount++; return v; } private void calculateEllipsis(CharSequence text, 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; Loading @@ -1090,53 +1087,11 @@ public class StaticLayout extends Layout { return; } float tempAvail = avail; int numberOfTries = 0; boolean lineFits = false; mWorkPaint.set(paint); do { final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths, widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir); if (ellipsizedWidth <= avail) { lineFits = true; } else { numberOfTries++; if (numberOfTries > 10) { // If the text still doesn't fit after ten tries, assume it will never fit and // ellipsize it all. mLines[mColumns * line + ELLIPSIS_START] = 0; mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart; lineFits = true; } 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; } } } while (!lineFits); mEllipsized = true; } 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. // // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it // should not be accessed while the method is running. private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths, int widthStart, float avail, TextUtils.TruncateAt where, int line, TextPaint paint, boolean forceEllipsis, int dir) { final int savedHyphenEdit = paint.getHyphenEdit(); paint.setHyphenEdit(0); 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) { Loading @@ -1144,9 +1099,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; Loading @@ -1157,13 +1112,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 || Loading @@ -1172,7 +1123,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; Loading @@ -1181,27 +1132,24 @@ public class StaticLayout extends Layout { sum += w; } if (forceEllipsis && i == len && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; } else { ellipsisStart = i; ellipsisCount = len - i; if (forceEllipsis && ellipsisCount == 0 && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; } // 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; Loading @@ -1209,9 +1157,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; Loading @@ -1223,53 +1171,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 boolean isSpanned = text instanceof Spanned; final Ellipsizer ellipsizedText = isSpanned ? new SpannedEllipsizer(text) : new Ellipsizer(text); ellipsizedText.mLayout = this; ellipsizedText.mMethod = where; final boolean hasTabs = getLineContainsTab(line); final TabStops tabStops; if (hasTabs && isSpanned) { final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart, lineEnd, TabStopSpan.class); if (tabs.length == 0) { tabStops = null; } else { tabStops = new TabStops(TAB_INCREMENT, tabs); } } else { tabStops = null; } paint.setHyphenEdit(hyphen); final TextLine textline = TextLine.obtain(); textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line), hasTabs, tabStops); // Since TextLine.metric() returns negative values for RTL text, multiplication by dir // converts it to an actual width. Note that we don't want to use the absolute value, // since we may actually have glyphs with negative advances, which by definition always // fit. final float ellipsizedWidth = textline.metrics(null) * dir; TextLine.recycle(textline); paint.setHyphenEdit(savedHyphenEdit); return ellipsizedWidth; } private float getTotalInsets(int line) { Loading Loading @@ -1509,8 +1418,6 @@ public class StaticLayout extends Layout { */ private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; private TextPaint mWorkPaint = new TextPaint(); private static final int COLUMNS_NORMAL = 5; private static final int COLUMNS_ELLIPSIZE = 7; private static final int START = 0; Loading
core/java/android/text/TextUtils.java +74 −114 Original line number Diff line number Diff line Loading @@ -88,8 +88,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; } Loading Loading @@ -1194,11 +1194,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); } Loading @@ -1214,11 +1212,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, Loading @@ -1239,19 +1235,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) { int len = text.length(); final int len = text.length(); MeasuredParagraph mt = null; MeasuredParagraph resultMt = null; try { mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); float width = mt.getWholeWidth(); Loading @@ -1260,110 +1253,76 @@ public class TextUtils { if (callback != null) { callback.ellipsized(0, 0); } return text; } // First estimate of effective width of ellipsis. float ellipsisWidth = paint.measureText(ellipsis); int numberOfTries = 0; boolean textFits = false; int start, end; CharSequence result; do { if (avail < ellipsisWidth) { // Even the ellipsis can't fit. So it all goes. start = 0; end = len; } else { final float remainingWidth = avail - ellipsisWidth; if (where == TruncateAt.START) { start = 0; end = len - mt.breakText(len, false /* backwards */, remainingWidth); // XXX assumes ellipsis string does not require shaping and // is unaffected by style float ellipsiswid = paint.measureText(ellipsis); avail -= ellipsiswid; 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) { start = mt.breakText(len, true /* forwards */, remainingWidth); end = len; left = mt.breakText(len, true, avail); } else { end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2); start = mt.breakText(end, true /* forwards */, remainingWidth - mt.measure(end, len)); 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); } final char[] buf = mt.getChars(); final Spanned sp = text instanceof Spanned ? (Spanned) text : null; Spanned sp = text instanceof Spanned ? (Spanned) text : null; final int removed = end - start; final int removed = right - left; 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; 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; } final String s = new String(buf, 0, len); String s = new String(buf, 0, len); if (sp == null) { result = s; } else { final SpannableString ss = new SpannableString(s); return s; } SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); result = ss; return 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; } return ""; } if (remaining == 0) { // All text is gone. textFits = true; } else { resultMt = MeasuredParagraph.buildForMeasurement( paint, result, 0, result.length(), textDir, resultMt); width = resultMt.getWholeWidth(); if (width <= avail) { textFits = true; } else { numberOfTries++; if (numberOfTries > 10) { // If the text still doesn't fit after ten tries, assume it will never // fit and ellipsize it all. We do this by setting the width of the // ellipsis to be positive infinity, so we get to empty text in the next // round. ellipsisWidth = Float.POSITIVE_INFINITY; } else { // Adjust the width of the ellipsis by adding the amount 'width' is // still over. ellipsisWidth += width - avail; } } } } while (!textFits); if (callback != null) { callback.ellipsized(start, end); 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(); } return result; SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(text, 0, left); ssb.append(ellipsis); ssb.append(text, right, len); return ssb; } finally { if (mt != null) { mt.recycle(); } if (resultMt != null) { resultMt.recycle(); } } } Loading Loading @@ -1394,6 +1353,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<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, Loading
graphics/java/android/graphics/Paint.java +1 −1 Original line number Diff line number Diff line Loading @@ -2742,7 +2742,7 @@ public class Paint { * @param offset index of caret position * @return width measurement between start and offset */ public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart, public float getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset) { if (text == null) { throw new IllegalArgumentException("text cannot be null"); Loading