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
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index a233ba1..0f910cc 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -2036,35 +2036,27 @@
}
}
- 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 a8c6aa6..c5bd29e 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -781,8 +781,8 @@
&& (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 @@
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 440c88e..ba5eaec 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -77,12 +77,21 @@
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
- /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
- private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
+ // 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;
+ }
+
private TextUtils() { /* cannot be instantiated */ }
@@ -1190,10 +1199,10 @@
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 @@
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 @@
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 @@
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 @@
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 d26be91..386aa84 100644
--- a/core/java/com/android/internal/app/LocaleHelper.java
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -181,7 +181,7 @@
// 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 472b3e2..4e155bd 100644
--- a/core/tests/coretests/src/android/text/TextUtilsTest.java
+++ b/core/tests/coretests/src/android/text/TextUtilsTest.java
@@ -341,6 +341,66 @@
}
@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));
assertFalse(TextUtils.delimitedStringContains(null, ',', ""));