Merge "Revert "Make ellipsize retry if text doesn't fit""

This commit is contained in:
Roozbeh Pournader
2017-07-31 20:19:32 +00:00
committed by Android (Google) Code Review
3 changed files with 126 additions and 210 deletions

View File

@@ -2059,11 +2059,9 @@ public abstract class Layout {
final String ellipsisString = TextUtils.getEllipsisString(method);
final int ellipsisStringLen = ellipsisString.length();
// Use the ellipsis string only if there are that at least as many characters to replace.
final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen;
for (int i = 0; i < ellipsisCount; i++) {
final char c;
if (useEllipsisString && i < ellipsisStringLen) {
if (i < ellipsisStringLen && ellipsisCount <= ellipsisStringLen) {
c = ellipsisString.charAt(i);
} else {
c = TextUtils.ELLIPSIS_FILLER;

View File

@@ -720,7 +720,7 @@ public class StaticLayout extends Layout {
// TODO: Support more justification mode, e.g. letter spacing, stretching.
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE);
if (mLeftIndents != null || mRightIndents != null) {
// TODO(performance): it would be better to do this once per layout rather
// TODO(raph) 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
// interface.
int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
@@ -807,7 +807,7 @@ public class StaticLayout extends Layout {
width += widths[j];
}
}
flag |= flags[i] & TAB_MASK; // XXX May need to also have starting hyphen edit
flag |= flags[i] & TAB_MASK;
}
// Treat the last line and overflowed lines as a single line.
breaks[remainingLineCount - 1] = breaks[breakCount - 1];
@@ -961,25 +961,6 @@ public class StaticLayout extends Layout {
bottom = fm.bottom;
}
// Information about hyphenation, tabs, and directions are needed for determining
// ellipsization, so the values should be assigned before ellipsization.
// 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;
// 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] = DIRS_ALL_LEFT_TO_RIGHT;
} else {
mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
start - widthStart, end - start);
}
boolean firstLine = (j == 0);
boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
@@ -995,8 +976,8 @@ public class StaticLayout extends Layout {
ellipsize == TextUtils.TruncateAt.END);
if (doEllipsis) {
calculateEllipsis(start, end, widths, widthStart,
ellipsisWidth - getTotalInsets(j), ellipsize, j,
textWidth, paint, forceEllipsis, dir);
ellipsisWidth, ellipsize, j,
textWidth, paint, forceEllipsis);
}
}
@@ -1048,7 +1029,7 @@ public class StaticLayout extends Layout {
extra = 0;
}
lines[off + START] |= start;
lines[off + START] = start;
lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
lines[off + EXTRA] = extra;
@@ -1066,13 +1047,33 @@ 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;
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++;
return v;
}
private void calculateEllipsis(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;
@@ -1080,35 +1081,11 @@ public class StaticLayout extends Layout {
return;
}
float tempAvail = avail;
while (true) {
float ellipsizedWidth = guessEllipsis(lineStart, lineEnd, widths, widthStart, tempAvail,
where, line, textWidth, paint, forceEllipsis, dir);
if (ellipsizedWidth <= avail) {
mEllipsized = true;
return; // The ellipsized line fits.
} 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;
}
}
}
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.
private float guessEllipsis(int lineStart, int lineEnd, float[] widths, int widthStart,
float avail, TextUtils.TruncateAt where, int line, float textWidth, TextPaint paint,
boolean forceEllipsis, int dir) {
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) {
@@ -1116,9 +1093,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;
@@ -1129,13 +1106,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 ||
@@ -1144,7 +1117,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;
@@ -1153,27 +1126,24 @@ public class StaticLayout extends Layout {
sum += w;
}
if (forceEllipsis && i == len && len > 0) {
ellipsisStart = i;
ellipsisCount = len - i;
if (forceEllipsis && ellipsisCount == 0 && len > 0) {
ellipsisStart = len - 1;
ellipsisCount = 1;
} else {
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.
} 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;
@@ -1181,9 +1151,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;
@@ -1195,46 +1165,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 CharSequence text = getText();
final boolean hasTabs = getLineContainsTab(line);
final TabStops tabStops;
if (hasTabs && text instanceof Spanned) {
final TabStopSpan[] tabs = getParagraphSpans((Spanned) text, lineStart, lineEnd,
TabStopSpan.class);
if (tabs.length == 0) {
tabStops = null;
} else {
tabStops = new TabStops(TAB_INCREMENT, tabs);
}
} else {
tabStops = null;
}
final TextLine textline = TextLine.obtain();
paint.setHyphenEdit(hyphen);
textline.set(paint,
text, /* this is an instance of Ellipsizer or SpannedEllipsizer */
lineStart, lineEnd,
dir, getLineDirections(line),
hasTabs, tabStops);
final float hyphenatedWidth = textline.metrics(null);
paint.setHyphenEdit(0);
TextLine.recycle(textline);
return hyphenatedWidth;
}
private float getTotalInsets(int line) {

View File

@@ -89,8 +89,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;
}
@@ -1187,11 +1187,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);
}
@@ -1207,11 +1205,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,
@@ -1232,19 +1228,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) {
final int len = text.length();
final MeasuredText mt = MeasuredText.obtain();
MeasuredText resultMt = null;
int len = text.length();
MeasuredText mt = MeasuredText.obtain();
try {
float width = setPara(mt, paint, text, 0, text.length(), textDir);
@@ -1252,88 +1245,74 @@ public class TextUtils {
if (callback != null) {
callback.ellipsized(0, 0);
}
return text;
}
resultMt = MeasuredText.obtain();
// First estimate of effective width of ellipsis.
float ellipsisWidth = paint.measureText(ellipsis);
while (true) { // Repeat until the ellipsized text fits.
final float remainingWidth = avail - ellipsisWidth;
final int start, end;
if (remainingWidth < 0) {
// Even the ellipsis can't fit. So it all goes.
start = 0;
end = len;
} else if (where == TruncateAt.START) {
start = 0;
end = len - mt.breakText(len, false /* backwards */, remainingWidth);
} else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
start = mt.breakText(len, true /* forwards */, remainingWidth);
end = len;
} else {
end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2);
start = mt.breakText(end, true /* forwards */,
remainingWidth - mt.measure(end, len));
}
// XXX assumes ellipsis string does not require shaping and
// is unaffected by style
float ellipsiswid = paint.measureText(ellipsis);
avail -= ellipsiswid;
final char[] buf = mt.mChars;
final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
final CharSequence result;
final int removed = end - start;
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;
}
final String s = new String(buf, 0, len);
if (sp == null) {
result = s;
} else {
final SpannableString ss = new SpannableString(s);
copySpansFrom(sp, 0, len, Object.class, ss, 0);
result = 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;
}
}
width = setPara(resultMt, paint, result, 0, result.length(), textDir);
if (width <= avail || remaining == 0) { // Text fits or all text is gone.
if (callback != null) {
callback.ellipsized(start, end);
}
return result;
} else {
// Adjust the width of the ellipsis by adding the amount 'width' is still over.
ellipsisWidth += width - avail;
}
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) {
left = mt.breakText(len, true, avail);
} else {
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);
}
char[] buf = mt.mChars;
Spanned sp = text instanceof Spanned ? (Spanned) text : null;
final int removed = right - left;
final int remaining = len - removed;
if (preserveLength) {
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] = ELLIPSIS_FILLER;
}
String s = new String(buf, 0, len);
if (sp == null) {
return s;
}
SpannableString ss = new SpannableString(s);
copySpansFrom(sp, 0, len, Object.class, ss, 0);
return ss;
}
if (remaining == 0) {
return "";
}
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();
}
SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(text, 0, left);
ssb.append(ellipsis);
ssb.append(text, right, len);
return ssb;
} finally {
MeasuredText.recycle(mt);
if (resultMt != null) {
MeasuredText.recycle(resultMt);
}
}
}
@@ -1364,6 +1343,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,