Merge changes Ib039b1bd,Ibbe63244 into androidx-main
* changes:
Implement AutoSize for Text in ProtoLayout
Extend FontStyle#size to accept a list of sizes
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index f248a31..ab3c6c2 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -83,9 +83,29 @@
// The styling of a font (e.g. font size, and metrics).
message FontStyle {
- // The size of the font, in scaled pixels (sp). If not specified, defaults to
- // the size of the system's "body" font.
- SpProp size = 1;
+ // The available sizes of the font, in scaled pixels (sp). If not specified, defaults to the size
+ // of the system's "body" font.
+ //
+ // If more than one size is specified and this FontStyle is applied to a Text element with static
+ // text, the text size will be automatically picked from the provided sizes to try to perfectly
+ // fit within its parent bounds. In other words, the largest size from the specified preset sizes
+ // that can fit the most text within the parent bounds will be used.
+ //
+ // The specified sizes don't have to be sorted. The maximum number of sizes used is limited to 10.
+ //
+ // Note that, if multiple sizes are set, 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.
+ //
+ // If this FontStyle is set to any other element besides Text, or that Text element has dynamic
+ // field, only the last added size will be use.
+ //
+ // Any previously added values with this method or #setSize will be cleared.
+ //
+ // While this field is accessible from 1.0 as singular, it only accepts multiple values since
+ // version 1.3 and renderers supporting version 1.3 will use the multiple values to automatically
+ // scale text. Renderers who don't support this version will use the first size among multiple
+ // values.
+ repeated SpProp size = 1;
// Whether the text should be rendered in a italic typeface. If not specified,
// defaults to "false".
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 1c4789b..e03b58a 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
@@ -16,6 +16,8 @@
package androidx.wear.protolayout.renderer.inflater;
+import static android.util.TypedValue.COMPLEX_UNIT_SP;
+
import static androidx.core.util.Preconditions.checkNotNull;
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.FIRST_CHILD_INDEX;
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.ROOT_NODE_ID;
@@ -249,6 +251,8 @@
private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
private static final int TEXT_MAX_LINES_DEFAULT = 1;
+ @VisibleForTesting
+ static final int TEXT_AUTOSIZES_LIMIT = 10;
private static final int TEXT_MIN_LINES = 1;
private static final ContainerDimension CONTAINER_DIMENSION_DEFAULT =
@@ -1137,7 +1141,7 @@
private float toPx(SpProp spField) {
return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
+ COMPLEX_UNIT_SP,
spField.getValue(),
mUiContext.getResources().getDisplayMetrics());
}
@@ -1146,7 +1150,8 @@
FontStyle style,
TextView textView,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker,
+ boolean isAutoSizeAllowed) {
// Note: Underline must be applied as a Span to work correctly (as opposed to using
// TextPaint#setTextUnderline). This is applied in the caller instead.
@@ -1155,8 +1160,43 @@
// flags in Paint if they're not supported by the given typeface).
textView.setTypeface(createTypeface(style), fontStyleToTypefaceStyle(style));
- if (style.hasSize()) {
- textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.getSize().getValue());
+ if (fontStyleHasSize(style)) {
+ List<SpProp> sizes = style.getSizeList();
+ int sizesCnt = sizes.size();
+
+ if (sizesCnt == 1) {
+ // No autosizing needed.
+ textView.setTextSize(COMPLEX_UNIT_SP, sizes.get(0).getValue());
+ } else if (isAutoSizeAllowed && sizesCnt <= TEXT_AUTOSIZES_LIMIT) {
+ // Max size is needed so that TextView leaves enough space for it. Otherwise,
+ // the text won't be able to grow.
+ int maxSize =
+ sizes.stream().mapToInt(sp -> (int) sp.getValue()).max().getAsInt();
+ textView.setTextSize(COMPLEX_UNIT_SP, maxSize);
+
+ // No need for sorting, TextView does that.
+ textView.setAutoSizeTextTypeUniformWithPresetSizes(
+ sizes.stream().mapToInt(spProp -> (int) spProp.getValue()).toArray(),
+ COMPLEX_UNIT_SP);
+ } else {
+ // Fallback where multiple values can't be used and the last value would be used.
+ // This can happen in two cases.
+ if (!isAutoSizeAllowed) {
+ // There is more than 1 size specified, but autosizing is not allowed.
+ Log.w(
+ TAG,
+ "Trying to autosize text with multiple font sizes where it's not "
+ + "allowed. Ignoring all other sizes and using the last one.");
+ } else {
+ Log.w(
+ TAG,
+ "More than " + TEXT_AUTOSIZES_LIMIT + " sizes has been added for the "
+ + "text autosizing. Ignoring values after the "
+ + TEXT_AUTOSIZES_LIMIT + " one.");
+ }
+
+ textView.setTextSize(COMPLEX_UNIT_SP, sizes.get(sizesCnt - 1).getValue());
+ }
}
if (style.hasLetterSpacing()) {
@@ -1176,10 +1216,16 @@
// flags in Paint if they're not supported by the given typeface).
textView.setTypeface(createTypeface(style), fontStyleToTypefaceStyle(style));
- // underline. We can implement this later by drawing a line under the text ourselves though.
-
- if (style.hasSize()) {
- textView.setTextSize(toPx(style.getSize()));
+ if (fontStyleHasSize(style)) {
+ // We are using the first added size in the FontStyle because ArcText doesn't support
+ // autosizing. This is the same behaviour as it was before size has made repeated.
+ if (style.getSizeList().size() > 1) {
+ Log.w(
+ TAG,
+ "Font size with multiple values has been used on Arc Text. Ignoring "
+ + "all size except the first one.");
+ }
+ textView.setTextSize(toPx(style.getSize(style.getSizeCount() - 1)));
}
}
@@ -2225,12 +2271,24 @@
}
applyTextOverflow(textView, text.getOverflow(), text.getMarqueeParameters());
+ // Text auto size is not supported for dynamic text.
+ boolean isAutoSizeAllowed = !(text.hasText() && text.getText().hasDynamicValue());
// Setting colours **must** go after setting the Text Appearance, otherwise it will get
// immediately overridden.
if (text.hasFontStyle()) {
- applyFontStyle(text.getFontStyle(), textView, posId, pipelineMaker);
+ applyFontStyle(
+ text.getFontStyle(),
+ textView,
+ posId,
+ pipelineMaker,
+ isAutoSizeAllowed);
} else {
- applyFontStyle(FontStyle.getDefaultInstance(), textView, posId, pipelineMaker);
+ applyFontStyle(
+ FontStyle.getDefaultInstance(),
+ textView,
+ posId,
+ pipelineMaker,
+ isAutoSizeAllowed);
}
boolean excludeFontPadding = false;
@@ -2828,8 +2886,17 @@
private void applyStylesToSpan(
SpannableStringBuilder builder, int start, int end, FontStyle fontStyle) {
- if (fontStyle.hasSize()) {
- AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(toPx(fontStyle.getSize())));
+ if (fontStyleHasSize(fontStyle)) {
+ // We are using the first added size in the FontStyle because ArcText doesn't support
+ // autosizing. This is the same behaviour as it was before size has made repeated.
+ if (fontStyle.getSizeList().size() > 1) {
+ Log.w(
+ TAG,
+ "Font size with multiple values has been used on Span Text. Ignoring "
+ + "all size except the first one.");
+ }
+ AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(toPx(
+ fontStyle.getSize(fontStyle.getSizeCount() - 1))));
builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
}
@@ -2858,6 +2925,10 @@
builder.setSpan(colorSpan, start, end, Spanned.SPAN_MARK_MARK);
}
+ private static boolean fontStyleHasSize(FontStyle fontStyle) {
+ return !fontStyle.getSizeList().isEmpty();
+ }
+
private void applyModifiersToSpan(
SpannableStringBuilder builder, int start, int end, SpanModifiers modifiers) {
if (modifiers.hasClickable()) {
@@ -3002,7 +3073,12 @@
// Setting colours **must** go after setting the Text Appearance, otherwise it will get
// immediately overridden.
if (mApplyFontVariantBodyAsDefault) {
- applyFontStyle(FontStyle.getDefaultInstance(), tv, posId, pipelineMaker);
+ applyFontStyle(
+ FontStyle.getDefaultInstance(),
+ tv,
+ posId,
+ pipelineMaker,
+ /* isAutoSizeAllowed= */ false);
}
LayoutParams layoutParams = generateDefaultLayoutParams();
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
index a291354..520b0b4 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
@@ -124,7 +124,7 @@
private LayoutElementProto.FontStyle toProto() {
return LayoutElementProto.FontStyle.newBuilder()
- .setSize(sp(sizeSp))
+ .addSize(sp(sizeSp))
.setItalic(bool(italic))
.setColor(color(colorArgb))
.build();
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 fc19485..6c32f06 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
@@ -17,6 +17,7 @@
package androidx.wear.protolayout.renderer.inflater;
import static android.os.Looper.getMainLooper;
+import static android.util.TypedValue.COMPLEX_UNIT_SP;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption.SLIDE_PARENT_SNAP_TO_INSIDE;
@@ -31,6 +32,7 @@
import static androidx.wear.protolayout.renderer.helper.TestDsl.layout;
import static androidx.wear.protolayout.renderer.helper.TestDsl.row;
import static androidx.wear.protolayout.renderer.helper.TestDsl.text;
+import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.TEXT_AUTOSIZES_LIMIT;
import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getFrameLayoutGravity;
import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getRenderedMetadata;
import static androidx.wear.protolayout.renderer.test.R.drawable.android_animated_24dp;
@@ -57,6 +59,7 @@
import android.os.SystemClock;
import android.text.TextUtils.TruncateAt;
import android.util.Pair;
+import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
@@ -112,6 +115,7 @@
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
import androidx.wear.protolayout.proto.ColorProto.ColorProp;
+import androidx.wear.protolayout.proto.DimensionProto;
import androidx.wear.protolayout.proto.DimensionProto.ArcLineLength;
import androidx.wear.protolayout.proto.DimensionProto.ArcSpacerLength;
import androidx.wear.protolayout.proto.DimensionProto.ContainerDimension;
@@ -1124,6 +1128,31 @@
}
@Test
+ public void inflate_arc_withText_autoSize_notSet() {
+ int lastSize = 12;
+ FontStyle.Builder style = FontStyle.newBuilder()
+ .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setArc(
+ Arc.newBuilder()
+ .setAnchorAngle(degrees(0).build())
+ .addContents(
+ ArcLayoutElement.newBuilder()
+ .setText(
+ ArcText.newBuilder()
+ .setText(string("text1"))
+ .setFontStyle(style))))
+ .build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
+ CurvedTextView tv = (CurvedTextView) arcLayout.getChildAt(0);
+ assertThat(tv.getText()).isEqualTo("text1");
+ expect.that(tv.getTextSize()).isEqualTo(lastSize);
+ }
+
+ @Test
public void inflate_arc_withSpacer() {
LayoutElement root =
LayoutElement.newBuilder()
@@ -1934,6 +1963,137 @@
}
@Test
+ public void inflate_textView_autosize_set() {
+ String text = "Test text";
+ int[] presetSizes = new int[]{12, 20, 10};
+ List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
+
+ LayoutElement textElement =
+ LayoutElement.newBuilder()
+ .setText(
+ Text.newBuilder()
+ .setText(string(text))
+ .setFontStyle(
+ FontStyle.newBuilder()
+ .addAllSize(sizes)))
+ .build();
+ LayoutElement root =
+ LayoutElement.newBuilder().setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement)).build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
+ TextView tv = (TextView) firstChild.getChildAt(0);
+
+ // TextView sorts preset sizes.
+ Arrays.sort(presetSizes);
+ expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM);
+ expect.that(tv.getAutoSizeTextAvailableSizes()).isEqualTo(presetSizes);
+ expect.that(tv.getTextSize()).isEqualTo(20);
+ }
+
+ @Test
+ public void inflate_textView_autosize_setLimit_usesSingleSize() {
+ String text = "Test text";
+ int sizesLength = TEXT_AUTOSIZES_LIMIT + 5;
+ int[] presetSizes = new int[sizesLength];
+ int expectedLastSize = 120;
+ for (int i = 0; i < sizesLength - 1; i++) {
+ presetSizes[i] = i + 1;
+ }
+ presetSizes[sizesLength - 1] = expectedLastSize;
+ List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
+
+ LayoutElement textElement =
+ LayoutElement.newBuilder()
+ .setText(
+ Text.newBuilder()
+ .setText(string(text))
+ .setMaxLines(Int32Prop.newBuilder().setValue(4))
+ .setFontStyle(
+ FontStyle.newBuilder()
+ .addAllSize(sizes)))
+ .build();
+ LayoutElement root =
+ LayoutElement.newBuilder().setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement)).build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
+ TextView tv = (TextView) firstChild.getChildAt(0);
+ expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
+ expect.that(tv.getTextSize()).isEqualTo(expectedLastSize);
+ }
+
+ @Test
+ public void inflate_textView_autosize_notSet() {
+ String text = "Test text";
+ int size = 24;
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
+
+ LayoutElement textElement =
+ LayoutElement.newBuilder()
+ .setText(
+ Text.newBuilder()
+ .setText(string(text))
+ .setFontStyle(
+ FontStyle.newBuilder()
+ .addAllSize(sizes)))
+ .build();
+ LayoutElement root =
+ LayoutElement.newBuilder().setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement)).build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
+ TextView tv = (TextView) firstChild.getChildAt(0);
+ expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
+ expect.that(tv.getTextSize()).isEqualTo(size);
+ }
+
+ @Test
+ public void inflate_textView_autosize_setDynamic_noop() {
+ String text = "Test text";
+ int lastSize = 24;
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
+
+ LayoutElement textElement =
+ LayoutElement.newBuilder()
+ .setText(
+ Text.newBuilder()
+ .setText(dynamicString(text))
+ .setFontStyle(
+ FontStyle.newBuilder()
+ .addAllSize(sizes)))
+ .build();
+ LayoutElement root =
+ LayoutElement.newBuilder().setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement)).build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ArrayList<View> textChildren = new ArrayList<>();
+ rootLayout.findViewsWithText(textChildren, text, View.FIND_VIEWS_WITH_TEXT);
+ TextView tv = (TextView) textChildren.get(0);
+ expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
+ expect.that(tv.getTextSize()).isEqualTo(lastSize);
+ }
+
+ @Test
public void inflate_spannable_marqueeAnimation() {
String text = "Marquee Animation";
LayoutElement root =
@@ -1964,6 +2124,29 @@
}
@Test
+ public void inflate_spantext_ignoresMultipleSizes() {
+ String text = "Test text";
+ int firstSize = 12;
+ FontStyle.Builder style = FontStyle.newBuilder()
+ .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setSpannable(
+ Spannable.newBuilder()
+ .addSpans(
+ Span.newBuilder()
+ .setText(
+ SpanText.newBuilder()
+ .setText(string(text))
+ .setFontStyle(style))))
+ .build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ TextView tv = (TextView) rootLayout.getChildAt(0);
+ expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ }
+
+ @Test
public void inflate_spannable_marqueeAnimation_repeatLimit() {
String text = "Marquee Animation";
int marqueeIterations = 5;
@@ -4452,6 +4635,11 @@
}
@NonNull
+ private static DimensionProto.SpProp sp(float value) {
+ return DimensionProto.SpProp.newBuilder().setValue(value).build();
+ }
+
+ @NonNull
private static ContainerDimension.Builder expand() {
return ContainerDimension.newBuilder()
.setExpandedDimension(ExpandedDimensionProp.getDefaultInstance());
@@ -4480,8 +4668,33 @@
}
@NonNull
+ private static StringProp.Builder dynamicString(String value) {
+ return StringProp.newBuilder()
+ .setValue(value)
+ .setDynamicValue(
+ DynamicString.newBuilder()
+ .setFixed(FixedString.newBuilder().setValue(value)));
+ }
+
+ @NonNull
private static ImageDimension.Builder expandImage() {
return ImageDimension.newBuilder()
.setExpandedDimension(ExpandedDimensionProp.getDefaultInstance());
}
+
+ @NonNull
+ private static List<DimensionProto.SpProp> buildSizesList(int[] presetSizes) {
+ List<DimensionProto.SpProp> sizes = new ArrayList<>(3);
+ for (int s: presetSizes) {
+ sizes.add(sp(s));
+ }
+ return sizes;
+ }
+
+ private static float toPx(int spValue) {
+ return TypedValue.applyDimension(
+ COMPLEX_UNIT_SP,
+ spValue,
+ getApplicationContext().getResources().getDisplayMetrics());
+ }
}
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 180def5..72af032 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -553,6 +553,7 @@
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
method public androidx.wear.protolayout.DimensionBuilders.EmProp? getLetterSpacing();
method public androidx.wear.protolayout.DimensionBuilders.SpProp? getSize();
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public java.util.List<androidx.wear.protolayout.DimensionBuilders.SpProp!> getSizes();
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp? getVariant();
method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
@@ -566,6 +567,7 @@
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.protolayout.DimensionBuilders.EmProp);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.protolayout.DimensionBuilders.SpProp);
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSizes(androidx.wear.protolayout.DimensionBuilders.SpProp!...);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.protolayout.TypeBuilders.BoolProp);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setVariant(androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp);
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 180def5..72af032 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -553,6 +553,7 @@
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
method public androidx.wear.protolayout.DimensionBuilders.EmProp? getLetterSpacing();
method public androidx.wear.protolayout.DimensionBuilders.SpProp? getSize();
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public java.util.List<androidx.wear.protolayout.DimensionBuilders.SpProp!> getSizes();
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp? getVariant();
method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
@@ -566,6 +567,7 @@
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.protolayout.DimensionBuilders.EmProp);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.protolayout.DimensionBuilders.SpProp);
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSizes(androidx.wear.protolayout.DimensionBuilders.SpProp!...);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.protolayout.TypeBuilders.BoolProp);
method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setVariant(androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp);
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 80c73df..1cf6bce 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
@@ -27,6 +27,7 @@
import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.VisibleForTesting;
import androidx.wear.protolayout.ColorBuilders.Brush;
import androidx.wear.protolayout.ColorBuilders.ColorProp;
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
@@ -608,21 +609,6 @@
}
/**
- * Gets the size of the font, in scaled pixels (sp). If not specified, defaults to the size
- * of the system's "body" font.
- *
- * @since 1.0
- */
- @Nullable
- public SpProp getSize() {
- if (mImpl.hasSize()) {
- return SpProp.fromProto(mImpl.getSize());
- } else {
- return null;
- }
- }
-
- /**
* Gets whether the text should be rendered in a italic typeface. If not specified, defaults
* to "false".
*
@@ -713,6 +699,35 @@
}
}
+ /**
+ * Gets the size of the font, in scaled pixels (sp). If not specified, defaults to the size
+ * of the system's "body" font. If more than one size was originally added, it will
+ * return the last one.
+ *
+ * @since 1.0
+ */
+ @Nullable
+ public SpProp getSize() {
+ List<DimensionProto.SpProp> sizes = mImpl.getSizeList();
+ return !sizes.isEmpty() ? SpProp.fromProto(sizes.get(sizes.size() - 1)) : null;
+ }
+
+ /**
+ * Gets the available sizes of the font, in scaled pixels (sp). If not specified, defaults
+ * to the size of the system's "body" font.
+ *
+ * @since 1.3
+ */
+ @NonNull
+ @ProtoLayoutExperimental
+ public List<SpProp> getSizes() {
+ List<SpProp> list = new ArrayList<>();
+ for (DimensionProto.SpProp item : mImpl.getSizeList()) {
+ list.add(SpProp.fromProto(item));
+ }
+ return Collections.unmodifiableList(list);
+ }
+
/** Get the fingerprint for this object, or null if unknown. */
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
@@ -764,6 +779,7 @@
/** Builder for {@link FontStyle} */
public static final class Builder {
+ @VisibleForTesting static final int TEXT_SIZES_LIMIT = 10;
private final LayoutElementProto.FontStyle.Builder mImpl =
LayoutElementProto.FontStyle.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-374492482);
@@ -771,16 +787,48 @@
public Builder() {}
/**
- * Sets the size of the font, in scaled pixels (sp). If not specified, defaults to the
- * size of the system's "body" font.
+ * Sets the available sizes of the font, in scaled pixels (sp). If not specified,
+ * defaults to the size of the system's "body" font.
*
- * @since 1.0
+ * <p>If more than one size is specified and this {@link FontStyle} is applied to a
+ * {@link Text} element with static text, the text size will be automatically picked
+ * from the provided sizes to try to perfectly fit within its parent bounds. In other
+ * words, the largest size from the specified preset sizes that can fit the most text
+ * within the parent bounds will be used.
+ *
+ * <p>The specified sizes don't have to be sorted. The maximum number of sizes used is
+ * limited to 10.
+ *
+ * <p>Note that, if multiple sizes are set, 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>If this {@link FontStyle} is set to any other element besides {@link Text} or
+ * that {@link Text} element has dynamic field, only the last added size will be use.
+ *
+ * <p>Any previously added values with this method or {@link #setSize} will be cleared.
+ *
+ * <p>While this field is accessible from 1.0 as singular, it only accepts multiple
+ * values since version 1.3 and renderers supporting version 1.3 will use the multiple
+ * values to automatically scale text. Renderers who don't support this version will
+ * use the last size among multiple values.
+ *
+ * @throws IllegalArgumentException if the number of available sizes is larger than 10.
+ * @since 1.3
*/
@NonNull
- public Builder setSize(@NonNull SpProp size) {
- mImpl.setSize(size.toProto());
- mFingerprint.recordPropertyUpdate(
- 1, checkNotNull(size.getFingerprint()).aggregateValueAsInt());
+ @ProtoLayoutExperimental
+ public Builder setSizes(@NonNull SpProp... sizes) {
+ if (sizes.length > TEXT_SIZES_LIMIT) {
+ throw new IllegalArgumentException(
+ "Number of available sizes can't be larger than 10.");
+ }
+ mImpl.clearSize();
+ for (SpProp size: sizes) {
+ mImpl.addSize(size.toProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(size.getFingerprint()).aggregateValueAsInt());
+ }
return this;
}
@@ -830,6 +878,23 @@
}
/**
+ * Sets the size of the font, in scaled pixels (sp). If not specified, defaults to the
+ * size of the system's "body" font.
+ *
+ * <p>Any previously added values with this method or {@link #setSizes} will be cleared.
+ *
+ * @since 1.0
+ */
+ @NonNull
+ public Builder setSize(@NonNull SpProp size) {
+ mImpl.clearSize();
+ mImpl.addSize(size.toProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(size.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
* Sets the text color. If not defined, defaults to white.
*
* <p>While this field is statically accessible from 1.0, it's only bindable since
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
index a30c860..6f10e5a 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
@@ -16,6 +16,8 @@
package androidx.wear.protolayout;
+import static androidx.wear.protolayout.DimensionBuilders.sp;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
@@ -203,6 +205,84 @@
}
@Test
+ public void testFontStyleSetMultipleSizes() {
+ int size1 = 12;
+ int size2 = 30;
+ int lastSize = 20;
+ int[] expectedSizes = {size1, size2, lastSize};
+ LayoutElementBuilders.FontStyle fontStyle =
+ new LayoutElementBuilders.FontStyle.Builder()
+ .setSizes(sp(size1), sp(size2), sp(lastSize))
+ .build();
+
+ LayoutElementProto.FontStyle fontStyleProto = fontStyle.toProto();
+
+ assertThat(
+ fontStyleProto.getSizeList().stream().mapToInt(sp -> (int) sp.getValue()).toArray())
+ .isEqualTo(expectedSizes);
+ // Make sure that if 1 size is used than it's the last one.
+ assertThat(fontStyle.getSize().getValue()).isEqualTo(lastSize);
+ assertThat(
+ fontStyle.getSizes().stream().mapToInt(sp -> (int) sp.getValue()).toArray())
+ .isEqualTo(expectedSizes);
+ }
+
+ @Test
+ public void testFontStyleSetSize_moreTimes_usesLastOne() {
+ int lastSize = 20;
+ LayoutElementBuilders.FontStyle fontStyle =
+ new LayoutElementBuilders.FontStyle.Builder()
+ .setSize(sp(12))
+ .setSize(sp(30))
+ .setSize(sp(lastSize))
+ .build();
+
+ LayoutElementProto.FontStyle fontStyleProto = fontStyle.toProto();
+
+ assertThat(fontStyleProto.getSizeList().size()).isEqualTo(1);
+ assertThat(fontStyleProto.getSizeList().get(0).getValue()).isEqualTo(lastSize);
+ // Make sure that if 1 size is used than it's the last one.
+ assertThat(fontStyle.getSize().getValue()).isEqualTo(lastSize);
+ assertThat(fontStyle.getSizes().size()).isEqualTo(1);
+ assertThat(fontStyle.getSizes().get(0).getValue()).isEqualTo(lastSize);
+ }
+
+ @Test
+ public void testFontStyleSetSize_setSizes_overrides() {
+ int size1 = 12;
+ int size2 = 30;
+ int[] expectedSizes = {size1, size2};
+ LayoutElementBuilders.FontStyle fontStyle =
+ new LayoutElementBuilders.FontStyle.Builder()
+ .setSize(sp(20))
+ .setSizes(sp(size1), sp(size2))
+ .build();
+
+ LayoutElementProto.FontStyle fontStyleProto = fontStyle.toProto();
+
+ assertThat(
+ fontStyleProto.getSizeList().stream().mapToInt(sp -> (int) sp.getValue()).toArray())
+ .isEqualTo(expectedSizes);
+ // Make sure that if 1 size is used than it's the last one.
+ assertThat(fontStyle.getSize().getValue()).isEqualTo(size2);
+ assertThat(
+ fontStyle.getSizes().stream().mapToInt(sp -> (int) sp.getValue()).toArray())
+ .isEqualTo(expectedSizes);
+ }
+
+ @Test
+ public void testFontStyleSetSize_tooManySizes_throws() {
+ DimensionBuilders.SpProp[] sizes =
+ new DimensionBuilders.SpProp[
+ LayoutElementBuilders.FontStyle.Builder.TEXT_SIZES_LIMIT + 1];
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new LayoutElementBuilders.FontStyle.Builder()
+ .setSizes(sizes)
+ .build());
+ }
+
+ @Test
public void textSetText_withoutLayoutConstraint_throws() {
assertThrows(
IllegalStateException.class,
diff --git a/wear/tiles/tiles/lint-baseline.xml b/wear/tiles/tiles/lint-baseline.xml
index eb4c7b5..faa4a6b 100644
--- a/wear/tiles/tiles/lint-baseline.xml
+++ b/wear/tiles/tiles/lint-baseline.xml
@@ -1803,18 +1803,9 @@
<issue
id="RestrictedApiAndroidX"
- message="FontStyle.hasSize can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
- errorLine1=" if (mImpl.hasSize()) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/wear/tiles/LayoutElementBuilders.java"/>
- </issue>
-
- <issue
- id="RestrictedApiAndroidX"
- message="FontStyle.getSize can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
- errorLine1=" return DimensionBuilders.SpProp.fromProto(mImpl.getSize());"
- errorLine2=" ~~~~~~~">
+ message="FontStyle.getSizeList can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+ errorLine1=" List<DimensionProto.SpProp> sizes = mImpl.getSizeList();"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/androidx/wear/tiles/LayoutElementBuilders.java"/>
</issue>
@@ -1947,8 +1938,17 @@
<issue
id="RestrictedApiAndroidX"
- message="Builder.setSize can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
- errorLine1=" mImpl.setSize(size.toProto());"
+ message="Builder.clearSize can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+ errorLine1=" mImpl.clearSize();"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/wear/tiles/LayoutElementBuilders.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="Builder.addSize can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+ errorLine1=" mImpl.addSize(size.toProto());"
errorLine2=" ~~~~~~~">
<location
file="src/main/java/androidx/wear/tiles/LayoutElementBuilders.java"/>
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
index a089888..259bddf 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
@@ -31,6 +31,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.expression.Fingerprint;
import androidx.wear.protolayout.proto.AlignmentProto;
+import androidx.wear.protolayout.proto.DimensionProto;
import androidx.wear.protolayout.proto.FingerprintProto;
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
@@ -622,8 +623,9 @@
*/
@Nullable
public DimensionBuilders.SpProp getSize() {
- if (mImpl.hasSize()) {
- return DimensionBuilders.SpProp.fromProto(mImpl.getSize());
+ List<DimensionProto.SpProp> sizes = mImpl.getSizeList();
+ if (!sizes.isEmpty()) {
+ return DimensionBuilders.SpProp.fromProto(sizes.get(0));
} else {
return null;
}
@@ -744,7 +746,8 @@
*/
@NonNull
public Builder setSize(@NonNull DimensionBuilders.SpProp size) {
- mImpl.setSize(size.toProto());
+ mImpl.clearSize();
+ mImpl.addSize(size.toProto());
mFingerprint.recordPropertyUpdate(
1, checkNotNull(size.getFingerprint()).aggregateValueAsInt());
return this;