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
This commit is contained in:
Roozbeh Pournader
2017-07-25 11:20:29 -07:00
parent 34dc4c1f07
commit 9ea756fa70
5 changed files with 101 additions and 42 deletions

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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';
}

View File

@@ -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);

View File

@@ -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));