StaticLayout visual fix for maxLines
When maxLines is set on StaticLayout, the height calculation includes
the lineSpacing for the lastLine, which causes the ellipsized version
and non-ellipsized version to have different heights. With this CL:
* maxLines is always set on StaticLayout
* the correct line count for a given text is preserved, in other words a
text that would be n lines will not be cut at maxLines.
* The visual height for StaticLayout for ellipsized and non-ellipsized
cases are the same when maxLines is set.
Bug: 28988744
Bug: 18864800
Change-Id: I1e1cae31cf33d503a8cf1c942f422893efc480bb
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 0bd5071..9761a0c 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -616,6 +616,17 @@
}
/**
+ * Return the total height of this layout.
+ *
+ * @param cap if true and max lines is set, returns the height of the layout at the max lines.
+ *
+ * @hide
+ */
+ public int getHeight(boolean cap) {
+ return getHeight();
+ }
+
+ /**
* Return the base alignment of this layout.
*/
public final Alignment getAlignment() {
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index bb131a0..c58894f 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -836,7 +836,7 @@
here = endPos;
breakIndex++;
- if (mLineCount >= mMaximumVisibleLineCount) {
+ if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
return;
}
}
@@ -920,7 +920,25 @@
boolean firstLine = (j == 0);
boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
- boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
+
+ if (ellipsize != null) {
+ // If there is only one line, then do any type of ellipsis except when it is MARQUEE
+ // if there are multiple lines, just allow END ellipsis on the last line
+ boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
+
+ boolean doEllipsis =
+ (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
+ ellipsize != TextUtils.TruncateAt.MARQUEE) ||
+ (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
+ ellipsize == TextUtils.TruncateAt.END);
+ if (doEllipsis) {
+ calculateEllipsis(start, end, widths, widthStart,
+ ellipsisWidth, ellipsize, j,
+ textWidth, paint, forceEllipsis);
+ }
+ }
+
+ boolean lastLine = mEllipsized || (end == bufEnd);
if (firstLine) {
if (trackPad) {
@@ -944,7 +962,6 @@
}
}
-
if (needMultiply && !lastLine) {
double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
@@ -960,6 +977,15 @@
lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
+ // special case for non-ellipsized last visible line when maxLines is set
+ // store the height as if it was ellipsized
+ if (!mEllipsized && currentLineIsTheLastVisibleOne) {
+ // below calculation as if it was the last line
+ int maxLineBelow = includePad ? bottom : below;
+ // similar to the calculation of v below, without the extra.
+ mMaxLineHeight = v + (maxLineBelow - above);
+ }
+
v += (below - above) + extra;
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
@@ -981,23 +1007,6 @@
start - widthStart, end - start);
}
- if (ellipsize != null) {
- // If there is only one line, then do any type of ellipsis except when it is MARQUEE
- // if there are multiple lines, just allow END ellipsis on the last line
- boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
-
- boolean doEllipsis =
- (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
- ellipsize != TextUtils.TruncateAt.MARQUEE) ||
- (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
- ellipsize == TextUtils.TruncateAt.END);
- if (doEllipsis) {
- calculateEllipsis(start, end, widths, widthStart,
- ellipsisWidth, ellipsize, j,
- textWidth, paint, forceEllipsis);
- }
- }
-
mLineCount++;
return v;
}
@@ -1101,7 +1110,7 @@
}
}
}
-
+ mEllipsized = true;
mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
}
@@ -1239,6 +1248,25 @@
return mEllipsizedWidth;
}
+ /**
+ * Return the total height of this layout.
+ *
+ * @param cap if true and max lines is set, returns the height of the layout at the max lines.
+ *
+ * @hide
+ */
+ public int getHeight(boolean cap) {
+ if (cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight == -1 &&
+ Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "maxLineHeight should not be -1. "
+ + " maxLines:" + mMaximumVisibleLineCount
+ + " lineCount:" + mLineCount);
+ }
+
+ return cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight != -1 ?
+ mMaxLineHeight : super.getHeight();
+ }
+
private static native long nNewBuilder();
private static native void nFreeBuilder(long nativePtr);
private static native void nFinishBuilder(long nativePtr);
@@ -1277,6 +1305,21 @@
private int mColumns;
private int mEllipsizedWidth;
+ /**
+ * Keeps track if ellipsize is applied to the text.
+ */
+ private boolean mEllipsized;
+
+ /**
+ * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
+ * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
+ * starting from the top of the layout. If maxLines is not set its value will be -1.
+ *
+ * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
+ * more than maxLines is contained.
+ */
+ private int mMaxLineHeight = -1;
+
private static final int COLUMNS_NORMAL = 4;
private static final int COLUMNS_ELLIPSIZE = 6;
private static final int START = 0;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 72bfc88..fce5d74 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5030,7 +5030,7 @@
* call {@link InputMethodManager#restartInput(View)}.</p>
* @param hintLocales List of the languages that the user is supposed to switch to no matter
* what input method subtype is currently used. Set {@code null} to clear the current "hint".
- * @see #getImeHIntLocales()
+ * @see #getImeHintLocales()
* @see android.view.inputmethod.EditorInfo#hintLocales
*/
public void setImeHintLocales(@Nullable LocaleList hintLocales) {
@@ -6857,11 +6857,11 @@
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
- .setHyphenationFrequency(mHyphenationFrequency);
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
- .setEllipsizedWidth(ellipsisWidth)
- .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ .setEllipsizedWidth(ellipsisWidth);
}
mHintLayout = builder.build();
}
@@ -6948,11 +6948,11 @@
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
- .setHyphenationFrequency(mHyphenationFrequency);
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
- .setEllipsizedWidth(ellipsisWidth)
- .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ .setEllipsizedWidth(ellipsisWidth);
}
// TODO: explore always setting maxLines
result = builder.build();
@@ -7222,9 +7222,11 @@
return 0;
}
- int linecount = layout.getLineCount();
- int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
- int desired = layout.getLineTop(linecount);
+ /*
+ * Don't cap the hint to a certain number of lines.
+ * (Do cap it, though, if we have a maximum pixel height.)
+ */
+ int desired = layout.getHeight(cap);
final Drawables dr = mDrawables;
if (dr != null) {
@@ -7232,31 +7234,14 @@
desired = Math.max(desired, dr.mDrawableHeightRight);
}
- desired += pad;
+ desired += getCompoundPaddingTop() + getCompoundPaddingBottom();
- if (mMaxMode == LINES) {
- /*
- * Don't cap the hint to a certain number of lines.
- * (Do cap it, though, if we have a maximum pixel height.)
- */
- if (cap) {
- if (linecount > mMaximum) {
- desired = layout.getLineTop(mMaximum);
-
- if (dr != null) {
- desired = Math.max(desired, dr.mDrawableHeightLeft);
- desired = Math.max(desired, dr.mDrawableHeightRight);
- }
-
- desired += pad;
- linecount = mMaximum;
- }
- }
- } else {
+ if (mMaxMode != LINES) {
desired = Math.min(desired, mMaximum);
}
if (mMinMode == LINES) {
+ int linecount = layout.getLineCount();
if (linecount < mMinimum) {
desired += getLineHeight() * (mMinimum - linecount);
}