From 9ea756fa7047a70ade7bc9d3c06d00605addf211 Mon Sep 17 00:00:00 2001 From: Roozbeh Pournader Date: Tue, 25 Jul 2017 11:20:29 -0700 Subject: [PATCH] Refactor ellipsis attributes and methods Mostly changed to avoid repetition of code and remove the assumptions about ellipsis being one code unit. The code for multi-code unit ellipsis is not triggered yet, but is done in preparation for potential future locale-dependent cases. 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: adb shell am instrument -w -e package android.text com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner Change-Id: Id1dfdc503f87fabed2447d55ab2107eee0eccd08 --- core/java/android/text/Layout.java | 32 ++++------ core/java/android/text/StaticLayout.java | 8 +-- core/java/android/text/TextUtils.java | 41 ++++++++----- .../android/internal/app/LocaleHelper.java | 2 +- .../src/android/text/TextUtilsTest.java | 60 +++++++++++++++++++ 5 files changed, 101 insertions(+), 42 deletions(-) diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index a233ba118e7dc..0f910cc20d120 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -2036,35 +2036,27 @@ public abstract class Layout { } } - private char getEllipsisChar(TextUtils.TruncateAt method) { - return (method == TextUtils.TruncateAt.END_SMALL) ? - TextUtils.ELLIPSIS_TWO_DOTS[0] : - TextUtils.ELLIPSIS_NORMAL[0]; - } - private void ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method) { - int ellipsisCount = getEllipsisCount(line); - + final int ellipsisCount = getEllipsisCount(line); if (ellipsisCount == 0) { return; } + final int ellipsisStart = getEllipsisStart(line); + final int lineStart = getLineStart(line); - int ellipsisStart = getEllipsisStart(line); - int linestart = getLineStart(line); - - for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { - char c; - - if (i == ellipsisStart) { - c = getEllipsisChar(method); // ellipsis + final String ellipsisString = TextUtils.getEllipsisString(method); + final int ellipsisStringLen = ellipsisString.length(); + for (int i = 0; i < ellipsisCount; i++) { + final char c; + if (i < ellipsisStringLen && ellipsisCount <= ellipsisStringLen) { + c = ellipsisString.charAt(i); } else { - c = '\uFEFF'; // 0-width space + c = TextUtils.ELLIPSIS_FILLER; } - int a = i + linestart; - - if (a >= start && a < end) { + final int a = i + ellipsisStart + lineStart; + if (start <= a && a < end) { dest[destoff + a - start] = c; } } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index a8c6aa6cbbbb7..c5bd29e2a4331 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -781,8 +781,8 @@ public class StaticLayout extends Layout { && (ellipsize == TextUtils.TruncateAt.END || (mMaximumVisibleLineCount == 1 && ellipsize != TextUtils.TruncateAt.MARQUEE)); - if (remainingLineCount > 0 && remainingLineCount < breakCount && - ellipsisMayBeApplied) { + if (0 < remainingLineCount && remainingLineCount < breakCount + && ellipsisMayBeApplied) { // Calculate width and flag. float width = 0; int flag = 0; @@ -1053,9 +1053,7 @@ public class StaticLayout extends Layout { return; } - float ellipsisWidth = paint.measureText( - (where == TextUtils.TruncateAt.END_SMALL) ? - TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1); + float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); int ellipsisStart = 0; int ellipsisCount = 0; int len = lineEnd - lineStart; diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 440c88e8cabb1..ba5eaec9f9f2d 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -77,12 +77,21 @@ import java.util.regex.Pattern; public class TextUtils { private static final String TAG = "TextUtils"; - /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." - /** {@hide} */ - public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL); + // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved. + /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE + + // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps + // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word + // being ellipsized and not the locale. + private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…) + private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥) + + /** {@hide} */ + @NonNull + public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) { + return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL; + } - /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." - private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS); private TextUtils() { /* cannot be instantiated */ } @@ -1190,10 +1199,10 @@ public class TextUtils { TextPaint paint, float avail, TruncateAt where, boolean preserveLength, - EllipsizeCallback callback) { + @Nullable EllipsizeCallback callback) { return ellipsize(text, paint, avail, where, preserveLength, callback, TextDirectionHeuristics.FIRSTSTRONG_LTR, - (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING); + getEllipsisString(where)); } /** @@ -1213,7 +1222,7 @@ public class TextUtils { TextPaint paint, float avail, TruncateAt where, boolean preserveLength, - EllipsizeCallback callback, + @Nullable EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis) { int len = text.length(); @@ -1256,13 +1265,15 @@ public class TextUtils { char[] buf = mt.mChars; Spanned sp = text instanceof Spanned ? (Spanned) text : null; - int remaining = len - (right - left); + final int removed = right - left; + final int remaining = len - removed; if (preserveLength) { - if (remaining > 0) { // else eliminate the ellipsis too - buf[left++] = ellipsis.charAt(0); - } + 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] = ZWNBS_CHAR; + buf[i] = ELLIPSIS_FILLER; } String s = new String(buf, 0, len); if (sp == null) { @@ -1362,7 +1373,7 @@ public class TextUtils { final int remainingElements = totalLen - i - 1; if (remainingElements > 0) { CharSequence morePiece = (res == null) ? - ELLIPSIS_STRING : + ELLIPSIS_NORMAL : res.getQuantityString(moreId, remainingElements, remainingElements); morePiece = bidiFormatter.unicodeWrap(morePiece); output.append(morePiece); @@ -2029,6 +2040,4 @@ public class TextUtils { private static char[] sTemp = null; private static String[] EMPTY_STRING_ARRAY = new String[]{}; - - private static final char ZWNBS_CHAR = '\uFEFF'; } diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java index d26be91958252..386aa84bfdbe3 100644 --- a/core/java/com/android/internal/app/LocaleHelper.java +++ b/core/java/com/android/internal/app/LocaleHelper.java @@ -181,7 +181,7 @@ public class LocaleHelper { // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two // problems: it's expensive to extract it, and in case the output string becomes // automatically ellipsized, it can result in weird output. - localeNames[maxLocales] = TextUtils.ELLIPSIS_STRING; + localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END); } ListFormatter lfn = ListFormatter.getInstance(dispLocale); diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java index 472b3e2cd20d7..4e155bd8075dd 100644 --- a/core/tests/coretests/src/android/text/TextUtilsTest.java +++ b/core/tests/coretests/src/android/text/TextUtilsTest.java @@ -340,6 +340,66 @@ public class TextUtilsTest { } } + @Test + public void testEllipsize_multiCodepoint() { + final TextPaint paint = new TextPaint(); + final float wordWidth = paint.measureText("MMMM"); + + // Establish the ground rules first, for single-codepoint cases. + final String ellipsis = "."; // one full stop character + assertEquals( + "MM.\uFEFF", + TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth, + TextUtils.TruncateAt.END, true /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + ellipsis)); + assertEquals( + "MM.", + TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth, + TextUtils.TruncateAt.END, false /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + ellipsis)); + assertEquals( + "M.", + TextUtils.ellipsize("MM", paint, 0.45f * wordWidth, + TextUtils.TruncateAt.END, true /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + ellipsis)); + assertEquals( + "M.", + TextUtils.ellipsize("MM", paint, 0.45f * wordWidth, + TextUtils.TruncateAt.END, false /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + ellipsis)); + + // Now check the differences for multi-codepoint ellipsis. + final String longEllipsis = ".."; // two full stop characters + assertEquals( + "MM..", + TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth, + TextUtils.TruncateAt.END, true /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + longEllipsis)); + assertEquals( + "MM..", + TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth, + TextUtils.TruncateAt.END, false /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + longEllipsis)); + assertEquals( + "M\uFEFF", + TextUtils.ellipsize("MM", paint, 0.45f * wordWidth, + TextUtils.TruncateAt.END, true /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + longEllipsis)); + assertEquals( + "M..", + TextUtils.ellipsize("MM", paint, 0.45f * wordWidth, + TextUtils.TruncateAt.END, false /* preserve length */, + null /* no callback */, TextDirectionHeuristics.LTR, + longEllipsis)); + } + @Test public void testDelimitedStringContains() { assertFalse(TextUtils.delimitedStringContains("", ',', null));