Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e88b5df5 authored by Roozbeh Pournader's avatar Roozbeh Pournader
Browse files

Make ellipsize retry if text doesn't fit

This fixes the cases where the replacement of parts of text with
ellipsis may result in more-than-expected width of text due to
contextual width changes in the font, such as kerning or Arabic
shaping.

The calculations in TextUtils.ellipsize() and StaticLayout are fixed
to recalculate the new width and reduce it further until the text
actuall fits. BoringLayout and DynamicLayout get fixed too since
they use the other two implementations indirectly.

Also reverse a recently-introduced incorrect check for
multi-character ellipsis in Layout.java.

Fixes: 31537595
Fixes: 64156587
Test: Manual (Arabic edge cases ellipsize correctly)
Test: bit CtsTextTestCases:*
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.EditTextTest
Test: bit CtsWidgetTestCases:android.widget.cts.CheckedTextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.AutoCompleteTextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.MultiAutoCompleteTextViewTest
Test: bit FrameworksCoreTests:android.text.
Test: adb shell am instrument -w com.android.documentsui.tests/android.support.test.runner.AndroidJUnitRunner
Change-Id: Iaddcc8b01c78d477e2c29b339d321c9631426f34
parent a19cd51d
Loading
Loading
Loading
Loading
+174 −86
Original line number Original line Diff line number Diff line
@@ -559,7 +559,7 @@ public class StaticLayout extends Layout {
        Builder.recycle(b);
        Builder.recycle(b);
    }
    }


    /* package */ StaticLayout(CharSequence text) {
    /* package */ StaticLayout(@Nullable CharSequence text) {
        super(text, null, 0, null, 0, 0);
        super(text, null, 0, null, 0, 0);


        mColumns = COLUMNS_ELLIPSIZE;
        mColumns = COLUMNS_ELLIPSIZE;
@@ -601,7 +601,7 @@ public class StaticLayout extends Layout {
    }
    }


    /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
    /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
        CharSequence source = b.mText;
        final CharSequence source = b.mText;
        int bufStart = b.mStart;
        int bufStart = b.mStart;
        int bufEnd = b.mEnd;
        int bufEnd = b.mEnd;
        TextPaint paint = b.mPaint;
        TextPaint paint = b.mPaint;
@@ -722,7 +722,7 @@ public class StaticLayout extends Layout {
                    // TODO: Support more justification mode, e.g. letter spacing, stretching.
                    // TODO: Support more justification mode, e.g. letter spacing, stretching.
                    b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE);
                    b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE);
            if (mLeftIndents != null || mRightIndents != null) {
            if (mLeftIndents != null || mRightIndents != null) {
                // TODO(raph) performance: it would be better to do this once per layout rather
                // TODO(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
                // than once per paragraph, but that would require a change to the native
                // interface.
                // interface.
                int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
                int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
@@ -809,7 +809,7 @@ public class StaticLayout extends Layout {
                            width += widths[j];
                            width += widths[j];
                        }
                        }
                    }
                    }
                    flag |= flags[i] & TAB_MASK;
                    flag |= flags[i] & TAB_MASK; // XXX May need to also have starting hyphen edit
                }
                }
                // Treat the last line and overflowed lines as a single line.
                // Treat the last line and overflowed lines as a single line.
                breaks[remainingLineCount - 1] = breaks[breakCount - 1];
                breaks[remainingLineCount - 1] = breaks[breakCount - 1];
@@ -911,17 +911,16 @@ public class StaticLayout extends Layout {
        }
        }
    }
    }


    private int out(CharSequence text, int start, int end,
    // The parameters that are not changed in the method are marked as final to make the code
                      int above, int below, int top, int bottom, int v,
    // easier to understand.
                      float spacingmult, float spacingadd,
    private int out(final CharSequence text, final int start, final int end, int above, int below,
                      LineHeightSpan[] chooseHt, int[] chooseHtv,
            int top, int bottom, int v, final float spacingmult, final float spacingadd,
                      Paint.FontMetricsInt fm, int flags,
            final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
                      boolean needMultiply, byte[] chdirs, int dir,
            final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
                      boolean easy, int bufEnd, boolean includePad,
            final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
                      boolean trackPad, boolean addLastLineLineSpacing, char[] chs,
            final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
            final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
                      float ellipsisWidth, float textWidth,
            final float textWidth, final TextPaint paint, final boolean moreChars) {
                      TextPaint paint, boolean moreChars) {
        final int j = mLineCount;
        final int j = mLineCount;
        final int off = j * mColumns;
        final int off = j * mColumns;
        final int want = off + mColumns + TOP;
        final int want = off + mColumns + TOP;
@@ -941,30 +940,30 @@ public class StaticLayout extends Layout {
            mLineDirections = grow;
            mLineDirections = grow;
        }
        }


        if (chooseHt != null) {
        lines[off + START] = start;
            fm.ascent = above;
        lines[off + TOP] = v;
            fm.descent = below;
            fm.top = top;
            fm.bottom = bottom;


            for (int i = 0; i < chooseHt.length; i++) {
        // Information about hyphenation, tabs, and directions are needed for determining
                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
        // ellipsization, so the values should be assigned before ellipsization.
                    ((LineHeightSpan.WithDensity) chooseHt[i]).
                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);


                } else {
        // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
        // one bit for start field
                }
        lines[off + TAB] |= flags & TAB_MASK;
            }
        lines[off + HYPHEN] = flags;


            above = fm.ascent;
        lines[off + DIR] |= dir << DIR_SHIFT;
            below = fm.descent;
        // easy means all chars < the first RTL, so no emoji, no nothing
            top = fm.top;
        // XXX a run with no text or all spaces is easy but might be an empty
            bottom = fm.bottom;
        // 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);
        final boolean firstLine = (j == 0);
        boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
        final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);


        if (ellipsize != null) {
        if (ellipsize != null) {
            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
@@ -977,9 +976,9 @@ public class StaticLayout extends Layout {
                    (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
                    (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
                            ellipsize == TextUtils.TruncateAt.END);
                            ellipsize == TextUtils.TruncateAt.END);
            if (doEllipsis) {
            if (doEllipsis) {
                calculateEllipsis(start, end, widths, widthStart,
                calculateEllipsis(text, start, end, widths, widthStart,
                        ellipsisWidth, ellipsize, j,
                        ellipsisWidth - getTotalInsets(j), ellipsize, j,
                        textWidth, paint, forceEllipsis);
                        textWidth, paint, forceEllipsis, dir);
            }
            }
        }
        }


@@ -998,6 +997,28 @@ 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 (firstLine) {
            if (trackPad) {
            if (trackPad) {
                mTopPadding = top - above;
                mTopPadding = top - above;
@@ -1008,8 +1029,6 @@ public class StaticLayout extends Layout {
            }
            }
        }
        }


        int extra;

        if (lastLine) {
        if (lastLine) {
            if (trackPad) {
            if (trackPad) {
                mBottomPadding = bottom - below;
                mBottomPadding = bottom - below;
@@ -1020,8 +1039,9 @@ public class StaticLayout extends Layout {
            }
            }
        }
        }


        final int extra;
        if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
        if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
            double ex = (below - above) * (spacingmult - 1) + spacingadd;
            final double ex = (below - above) * (spacingmult - 1) + spacingadd;
            if (ex >= 0) {
            if (ex >= 0) {
                extra = (int)(ex + EXTRA_ROUNDING);
                extra = (int)(ex + EXTRA_ROUNDING);
            } else {
            } else {
@@ -1031,8 +1051,6 @@ public class StaticLayout extends Layout {
            extra = 0;
            extra = 0;
        }
        }


        lines[off + START] = start;
        lines[off + TOP] = v;
        lines[off + DESCENT] = below + extra;
        lines[off + DESCENT] = below + extra;
        lines[off + EXTRA] = extra;
        lines[off + EXTRA] = extra;


@@ -1040,7 +1058,7 @@ public class StaticLayout extends Layout {
        // store the height as if it was ellipsized
        // store the height as if it was ellipsized
        if (!mEllipsized && currentLineIsTheLastVisibleOne) {
        if (!mEllipsized && currentLineIsTheLastVisibleOne) {
            // below calculation as if it was the last line
            // below calculation as if it was the last line
            int maxLineBelow = includePad ? bottom : below;
            final int maxLineBelow = includePad ? bottom : below;
            // similar to the calculation of v below, without the extra.
            // similar to the calculation of v below, without the extra.
            mMaxLineHeight = v + (maxLineBelow - above);
            mMaxLineHeight = v + (maxLineBelow - above);
        }
        }
@@ -1049,33 +1067,13 @@ public class StaticLayout extends Layout {
        lines[off + mColumns + START] = end;
        lines[off + mColumns + START] = end;
        lines[off + mColumns + TOP] = v;
        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++;
        mLineCount++;
        return v;
        return v;
    }
    }


    private void calculateEllipsis(int lineStart, int lineEnd,
    private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
                                   float[] widths, int widthStart,
            int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
                                   float avail, TextUtils.TruncateAt where,
            TextPaint paint, boolean forceEllipsis, int dir) {
                                   int line, float textWidth, TextPaint paint,
                                   boolean forceEllipsis) {
        avail -= getTotalInsets(line);
        if (textWidth <= avail && !forceEllipsis) {
        if (textWidth <= avail && !forceEllipsis) {
            // Everything fits!
            // Everything fits!
            mLines[mColumns * line + ELLIPSIS_START] = 0;
            mLines[mColumns * line + ELLIPSIS_START] = 0;
@@ -1083,11 +1081,53 @@ public class StaticLayout extends Layout {
            return;
            return;
        }
        }


        float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
        float tempAvail = avail;
        int ellipsisStart = 0;
        int numberOfTries = 0;
        int ellipsisCount = 0;
        boolean lineFits = false;
        int len = lineEnd - lineStart;
        mWorkPaint.set(paint);
        do {
            final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths,
                    widthStart, tempAvail, where, line, textWidth, 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;
    }


    // 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, float textWidth,
            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
        // We only support start ellipsis on a single line
        if (where == TextUtils.TruncateAt.START) {
        if (where == TextUtils.TruncateAt.START) {
            if (mMaximumVisibleLineCount == 1) {
            if (mMaximumVisibleLineCount == 1) {
@@ -1095,9 +1135,9 @@ public class StaticLayout extends Layout {
                int i;
                int i;


                for (i = len; i > 0; i--) {
                for (i = len; i > 0; i--) {
                    float w = widths[i - 1 + lineStart - widthStart];
                    final float w = widths[i - 1 + offset];
                    if (w + sum + ellipsisWidth > avail) {
                    if (w + sum + ellipsisWidth > avail) {
                        while (i < len && widths[i + lineStart - widthStart] == 0.0f) {
                        while (i < len && widths[i + offset] == 0.0f) {
                            i++;
                            i++;
                        }
                        }
                        break;
                        break;
@@ -1108,9 +1148,13 @@ public class StaticLayout extends Layout {


                ellipsisStart = 0;
                ellipsisStart = 0;
                ellipsisCount = i;
                ellipsisCount = i;
                // Strip the potential hyphenation at beginning of line.
                hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
            } else {
            } else {
                ellipsisStart = 0;
                ellipsisCount = 0;
                if (Log.isLoggable(TAG, Log.WARN)) {
                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 ||
        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
@@ -1119,7 +1163,7 @@ public class StaticLayout extends Layout {
            int i;
            int i;


            for (i = 0; i < len; i++) {
            for (i = 0; i < len; i++) {
                float w = widths[i + lineStart - widthStart];
                final float w = widths[i + offset];


                if (w + sum + ellipsisWidth > avail) {
                if (w + sum + ellipsisWidth > avail) {
                    break;
                    break;
@@ -1128,24 +1172,27 @@ public class StaticLayout extends Layout {
                sum += w;
                sum += w;
            }
            }


            ellipsisStart = i;
            if (forceEllipsis && i == len && len > 0) {
            ellipsisCount = len - i;
            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
                ellipsisStart = len - 1;
                ellipsisStart = len - 1;
                ellipsisCount = 1;
                ellipsisCount = 1;
            }
            } else {
            } else {
            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
                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.
            if (mMaximumVisibleLineCount == 1) {
            if (mMaximumVisibleLineCount == 1) {
                float lsum = 0, rsum = 0;
                float lsum = 0, rsum = 0;
                int left = 0, right = len;
                int left = 0, right = len;


                float ravail = (avail - ellipsisWidth) / 2;
                final float ravail = (avail - ellipsisWidth) / 2;
                for (right = len; right > 0; right--) {
                for (right = len; right > 0; right--) {
                    float w = widths[right - 1 + lineStart - widthStart];
                    final float w = widths[right - 1 + offset];


                    if (w + rsum > ravail) {
                    if (w + rsum > ravail) {
                        while (right < len && widths[right + lineStart - widthStart] == 0.0f) {
                        while (right < len && widths[right + offset] == 0.0f) {
                            right++;
                            right++;
                        }
                        }
                        break;
                        break;
@@ -1153,9 +1200,9 @@ public class StaticLayout extends Layout {
                    rsum += w;
                    rsum += w;
                }
                }


                float lavail = avail - ellipsisWidth - rsum;
                final float lavail = avail - ellipsisWidth - rsum;
                for (left = 0; left < right; left++) {
                for (left = 0; left < right; left++) {
                    float w = widths[left + lineStart - widthStart];
                    final float w = widths[left + offset];


                    if (w + lsum > lavail) {
                    if (w + lsum > lavail) {
                        break;
                        break;
@@ -1167,14 +1214,53 @@ public class StaticLayout extends Layout {
                ellipsisStart = left;
                ellipsisStart = left;
                ellipsisCount = right - left;
                ellipsisCount = right - left;
            } else {
            } else {
                ellipsisStart = 0;
                ellipsisCount = 0;
                if (Log.isLoggable(TAG, Log.WARN)) {
                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_START] = ellipsisStart;
        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
        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) {
    private float getTotalInsets(int line) {
@@ -1407,6 +1493,8 @@ public class StaticLayout extends Layout {
     */
     */
    private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
    private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;


    private TextPaint mWorkPaint = new TextPaint();

    private static final int COLUMNS_NORMAL = 5;
    private static final int COLUMNS_NORMAL = 5;
    private static final int COLUMNS_ELLIPSIZE = 7;
    private static final int COLUMNS_ELLIPSIZE = 7;
    private static final int START = 0;
    private static final int START = 0;
+114 −75

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Original line Diff line number Diff line
@@ -2747,7 +2747,7 @@ public class Paint {
     * @param offset index of caret position
     * @param offset index of caret position
     * @return width measurement between start and offset
     * @return width measurement between start and offset
     */
     */
    public float getRunAdvance(CharSequence text, int start, int end, int contextStart,
    public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart,
            int contextEnd, boolean isRtl, int offset) {
            int contextEnd, boolean isRtl, int offset) {
        if (text == null) {
        if (text == null) {
            throw new IllegalArgumentException("text cannot be null");
            throw new IllegalArgumentException("text cannot be null");