Add renderer support for TEXT_OVERFLOW_ELLIPSIZE
This option should ellipsize text at the place where
it stops becoming visible, even if that means that
maxLines is not yet reached.
The existing TEXT_OVERFLOW_ELLIPSIZE_END will
ellipsize text but only on its last line. So if text
has maxLines set to 5 and only 2 of the are shown,
text won't have ... at the end of visible part.
Bug: 302531877
Test: Added
Relnote: "Renderer now supports TEXT_OVERFLOW_ELLIPSIZE option."
Change-Id: I7f085fb841240511cd136c1e095d9c2ff0d60205
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 0ccec7c..6c65576 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -334,6 +334,47 @@
testCases.put(
"overflow_text_golden" + goldenSuffix,
new Text.Builder(context, "abcdeabcdeabcde").build());
+ testCases.put(
+ "overflow_ellipsize_maxlines_notreached" + goldenSuffix,
+ new Box.Builder()
+ .setWidth(dp(100))
+ .setHeight(dp(42))
+ .setModifiers(buildBackgroundColorModifier(Color.YELLOW))
+ .addContent(
+ new Text.Builder(
+ context,
+ "Very long text that won't fit in its parent box so it"
+ + "needs to be ellipsized correctly before its "
+ + "last line")
+ // Line height = 20sp
+ .setTypography(Typography.TYPOGRAPHY_BODY1)
+ .setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE)
+ .setMultilineAlignment(
+ LayoutElementBuilders.TEXT_ALIGN_START)
+ .setMaxLines(6)
+ .build())
+ .build());
+ testCases.put(
+ "overflow_ellipsize_end_maxlines_notreached" + goldenSuffix,
+ new Box.Builder()
+ .setWidth(dp(100))
+ .setHeight(dp(42))
+ .setModifiers(buildBackgroundColorModifier(Color.YELLOW))
+ .addContent(
+ new Text.Builder(
+ context,
+ "Very long text that won't fit in its parent box so it"
+ + "needs to be ellipsized correctly before its "
+ + "last line")
+ // Line height = 20sp
+ .setTypography(Typography.TYPOGRAPHY_BODY1)
+ .setOverflow(
+ LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE_END)
+ .setMultilineAlignment(
+ LayoutElementBuilders.TEXT_ALIGN_START)
+ .setMaxLines(6)
+ .build())
+ .build());
return testCases;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index 96fc634..223346e 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -167,6 +167,8 @@
// Note that, when this is used, the parent of the Text element this
// corresponds to shouldn't have its width and height set to wrapped, as it
// can lead to unexpected results.
+ // <p>Note that, on SpanText, this will behave exactly the same way as
+ // TEXT_OVERFLOW_ELLIPSIZE_END.
TEXT_OVERFLOW_ELLIPSIZE = 4;
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 993dd38..baf27ab 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -69,6 +69,7 @@
import android.view.ViewGroup.LayoutParams;
import android.view.ViewOutlineProvider;
import android.view.ViewParent;
+import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.AlphaAnimation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
@@ -1873,13 +1874,12 @@
// A null TruncateAt disables adding an ellipsis.
return null;
case TEXT_OVERFLOW_ELLIPSIZE_END:
+ case TEXT_OVERFLOW_ELLIPSIZE:
return TruncateAt.END;
case TEXT_OVERFLOW_MARQUEE:
return TruncateAt.MARQUEE;
case TEXT_OVERFLOW_UNDEFINED:
case UNRECOGNIZED:
- // TODO(b/302531877): Implement ellipsize.
- case TEXT_OVERFLOW_ELLIPSIZE:
return TEXT_OVERFLOW_DEFAULT;
}
@@ -2550,7 +2550,14 @@
} else {
textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
}
- applyTextOverflow(textView, text.getOverflow(), text.getMarqueeParameters());
+
+ TextOverflowProp overflow = text.getOverflow();
+ applyTextOverflow(textView, overflow, text.getMarqueeParameters());
+
+ if (overflow.getValue() == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE
+ && !text.getText().hasDynamicValue()) {
+ adjustMaxLinesForEllipsize(textView);
+ }
// Text auto size is not supported for dynamic text.
boolean isAutoSizeAllowed = !(text.hasText() && text.getText().hasDynamicValue());
@@ -2642,6 +2649,52 @@
}
/**
+ * Sorts out what maxLines should be if the text could possibly be truncated before maxLines is
+ * reached.
+ *
+ * <p>Should be only called for the {@link TextOverflow#TEXT_OVERFLOW_ELLIPSIZE} option which
+ * ellipsizes the text even before the last line, if there's no space for all lines. This is
+ * different than what TEXT_OVERFLOW_ELLIPSIZE_END does, as that option just ellipsizes the last
+ * line of text.
+ */
+ private void adjustMaxLinesForEllipsize(@NonNull TextView textView) {
+ textView
+ .getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ ViewParent maybeParent = textView.getParent();
+ if (!(maybeParent instanceof View)) {
+ Log.d(
+ TAG,
+ "Couldn't adjust max lines for ellipsizing as"
+ + "there's no View/ViewGroup parent.");
+ return false;
+ }
+
+ textView.getViewTreeObserver().removeOnPreDrawListener(this);
+
+ View parent = (View) maybeParent;
+ int availableHeight = parent.getHeight();
+ int oneLineHeight = textView.getLineHeight();
+ // This is what was set in proto, we shouldn't exceed it.
+ int maxMaxLines = textView.getMaxLines();
+ // Avoid having maxLines as 0 in case the space is really tight.
+ int availableLines = max(availableHeight / oneLineHeight, 1);
+
+ // Update only if changed.
+ if (availableLines < maxMaxLines) {
+ textView.setMaxLines(availableLines);
+ }
+
+ // Cancel the current drawing pass.
+ return false;
+ }
+ });
+ }
+
+ /**
* Sets whether the padding is included or not. If font padding is not included, sets the
* correct padding to the TextView to avoid clipping taller languages.
*/
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 4fa4807..950e73f 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -2765,6 +2765,70 @@
}
@Test
+ public void inflate_textView_ellipsize() {
+ String textContents = "Text that is very large so it will go to many lines";
+ Text.Builder text1 =
+ Text.newBuilder()
+ .setLineHeight(sp(16))
+ .setText(string(textContents))
+ .setFontStyle(FontStyle.newBuilder().addSize(sp(16)))
+ .setMaxLines(Int32Prop.newBuilder().setValue(6))
+ .setOverflow(
+ TextOverflowProp.newBuilder().setValue(
+ TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ Layout layout1 =
+ fingerprintedLayout(
+ LayoutElement.newBuilder()
+ .setBox(buildFixedSizeBoxWIthText(text1)).build());
+
+ Text.Builder text2 =
+ Text.newBuilder()
+ .setText(string(textContents))
+ // Diff
+ .setLineHeight(sp(4))
+ .setFontStyle(FontStyle.newBuilder().addSize(sp(4)))
+ .setMaxLines(Int32Prop.newBuilder().setValue(6))
+ .setOverflow(
+ TextOverflowProp.newBuilder().setValue(
+ TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ Layout layout2 =
+ fingerprintedLayout(
+ LayoutElement.newBuilder()
+ .setBox(buildFixedSizeBoxWIthText(text2)).build());
+
+ // Initial layout.
+ Renderer renderer = renderer(layout1);
+ ViewGroup inflatedViewParent = renderer.inflate();
+ TextView textView1 = (TextView) ((ViewGroup) inflatedViewParent
+ .getChildAt(0)).getChildAt(0);
+
+ // Apply the mutation.
+ ViewGroupMutation mutation =
+ renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
+ assertThat(mutation).isNotNull();
+ assertThat(mutation.isNoOp()).isFalse();
+ boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
+ assertThat(mutationResult).isTrue();
+
+ // This contains layout after the mutation.
+ TextView textView2 = (TextView) ((ViewGroup) inflatedViewParent
+ .getChildAt(0)).getChildAt(0);
+
+ expect.that(textView1.getEllipsize()).isEqualTo(TruncateAt.END);
+ expect.that(textView1.getMaxLines()).isEqualTo(2);
+
+ expect.that(textView2.getEllipsize()).isEqualTo(TruncateAt.END);
+ expect.that(textView2.getMaxLines()).isEqualTo(3);
+ }
+
+ private static Box.Builder buildFixedSizeBoxWIthText(Text.Builder content) {
+ return Box.newBuilder()
+ .setWidth(ContainerDimension.newBuilder().setLinearDimension(dp(100)))
+ .setHeight(ContainerDimension.newBuilder().setLinearDimension(dp(120)))
+ .addContents(LayoutElement.newBuilder().setText(content));
+ }
+
+ @Test
public void inflate_textView_marquee_animationsDisabled() {
String textContents = "Marquee Animation";
LayoutElement root =
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index 3e84390..ff9990c 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -222,6 +222,9 @@
* parent container. Note that, when this is used, the parent of the {@link Text} element this
* corresponds to shouldn't have its width and height set to wrapped, as it can lead to
* unexpected results.
+ *
+ * <p>Note that, on {@link SpanText}, this will behave exactly the same way as
+ * TEXT_OVERFLOW_ELLIPSIZE_END.
*/
@RequiresSchemaVersion(major = 1, minor = 300)
public static final int TEXT_OVERFLOW_ELLIPSIZE = 4;