Skip to content

Commit 7f01739

Browse files
imhappipekingme
authored andcommitted
[TextInputLayout] Add hintMaxLines attribute
PiperOrigin-RevId: 685884472
1 parent 9bf5edd commit 7f01739

File tree

8 files changed

+247
-33
lines changed

8 files changed

+247
-33
lines changed

docs/components/TextField.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ Element | Attribute | Related method(s)
663663
**Color** | `android:textColorHint` | `setDefaultHintTextColor`<br/>`getDefaultHintTextColor` | `?attr/colorOnSurfaceVariant` (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_label_color.xml))
664664
**Collapsed (floating) color** | `app:hintTextColor` | `setHintTextColor`<br/>`getHintTextColor` | `?attr/colorPrimary`
665665
**Typography** | `app:hintTextAppearance` | `setHintTextAppearance` | `?attr/textAppearanceBodySmall`
666+
**Max number of lines** | `app:hintMaxLines` | `setHintMaxLines`<br/>`getHintMaxLines` | `1`
666667

667668
**Note:** The `android:hint` should always be set on the `TextInputLayout`
668669
instead of on the `EditText` in order to avoid unintended behaviors.

lib/java/com/google/android/material/appbar/CollapsingToolbarLayout.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ public CollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet
300300
a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1);
301301

302302
if (a.hasValue(R.styleable.CollapsingToolbarLayout_maxLines)) {
303-
collapsingTextHelper.setMaxLines(a.getInt(R.styleable.CollapsingToolbarLayout_maxLines, 1));
303+
collapsingTextHelper.setExpandedMaxLines(a.getInt(R.styleable.CollapsingToolbarLayout_maxLines, 1));
304304
}
305305

306306
if (a.hasValue(R.styleable.CollapsingToolbarLayout_titlePositionInterpolator)) {
@@ -616,15 +616,16 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
616616
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
617617
}
618618

619-
if (extraMultilineHeightEnabled && collapsingTextHelper.getMaxLines() > 1) {
619+
if (extraMultilineHeightEnabled && collapsingTextHelper.getExpandedMaxLines() > 1) {
620620
// Need to update title and bounds in order to calculate line count and text height.
621621
updateTitleFromToolbarIfNeeded();
622622
updateTextBounds(0, 0, getMeasuredWidth(), getMeasuredHeight(), /* forceRecalculate= */ true);
623623

624624
int lineCount = collapsingTextHelper.getExpandedLineCount();
625625
if (lineCount > 1) {
626626
// Add extra height based on the amount of height beyond the first line of title text.
627-
int expandedTextHeight = Math.round(collapsingTextHelper.getExpandedTextFullHeight());
627+
int expandedTextHeight =
628+
Math.round(collapsingTextHelper.getExpandedTextFullSingleLineHeight());
628629
extraMultilineHeight = expandedTextHeight * (lineCount - 1);
629630
int newHeight = getMeasuredHeight() + extraMultilineHeight;
630631
heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
@@ -1407,7 +1408,7 @@ public void setExpandedTitleMarginBottom(int margin) {
14071408
*/
14081409
@RestrictTo(LIBRARY_GROUP)
14091410
public void setMaxLines(int maxLines) {
1410-
collapsingTextHelper.setMaxLines(maxLines);
1411+
collapsingTextHelper.setExpandedMaxLines(maxLines);
14111412
}
14121413

14131414
/**
@@ -1416,7 +1417,7 @@ public void setMaxLines(int maxLines) {
14161417
*/
14171418
@RestrictTo(LIBRARY_GROUP)
14181419
public int getMaxLines() {
1419-
return collapsingTextHelper.getMaxLines();
1420+
return collapsingTextHelper.getExpandedMaxLines();
14201421
}
14211422

14221423
/**

lib/java/com/google/android/material/internal/CollapsingTextHelper.java

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public final class CollapsingTextHelper {
131131
private float currentShadowDx;
132132
private float currentShadowDy;
133133
private int currentShadowColor;
134+
private int currentMaxLines;
134135

135136
private int[] state;
136137

@@ -161,11 +162,16 @@ public final class CollapsingTextHelper {
161162
private float collapsedTextBlend;
162163
private float expandedTextBlend;
163164
private CharSequence textToDrawCollapsed;
164-
private int maxLines = 1;
165+
166+
private static final int ONE_LINE = 1;
167+
private int expandedMaxLines = ONE_LINE;
168+
private int collapsedMaxLines = ONE_LINE;
165169
private float lineSpacingAdd = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_ADD;
166170
private float lineSpacingMultiplier = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_MULTIPLIER;
167171
private int hyphenationFrequency = StaticLayoutBuilderCompat.DEFAULT_HYPHENATION_FREQUENCY;
168172
@Nullable private StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer;
173+
private int collapsedHeight = -1;
174+
private int expandedHeight = -1;
169175

170176
public CollapsingTextHelper(View view) {
171177
this.view = view;
@@ -181,6 +187,13 @@ public CollapsingTextHelper(View view) {
181187
maybeUpdateFontWeightAdjustment(view.getContext().getResources().getConfiguration());
182188
}
183189

190+
public void setCollapsedMaxLines(int collapsedMaxLines) {
191+
if (collapsedMaxLines != this.collapsedMaxLines) {
192+
this.collapsedMaxLines = collapsedMaxLines;
193+
recalculate();
194+
}
195+
}
196+
184197
public void setTextSizeInterpolator(TimeInterpolator interpolator) {
185198
textSizeInterpolator = interpolator;
186199
recalculate();
@@ -261,13 +274,27 @@ public void setCollapsedBounds(@NonNull Rect bounds) {
261274
setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
262275
}
263276

264-
public void getCollapsedTextActualBounds(@NonNull RectF bounds, int labelWidth, int textGravity) {
277+
public void getCollapsedTextBottomTextBounds(
278+
@NonNull RectF bounds, int labelWidth, int textGravity) {
265279
isRtl = calculateIsRtl(text);
266280
bounds.left = max(getCollapsedTextLeftBound(labelWidth, textGravity), collapsedBounds.left);
267281
bounds.top = collapsedBounds.top;
268282
bounds.right =
269283
min(getCollapsedTextRightBound(bounds, labelWidth, textGravity), collapsedBounds.right);
270284
bounds.bottom = collapsedBounds.top + getCollapsedTextHeight();
285+
if (textLayout != null && !shouldTruncateCollapsedToSingleLine()) {
286+
// If the text is not truncated to one line when collapsed, we want to return the width of the
287+
// bottommost line, which is the textLayout's line width * the scale factor of the expanded
288+
// text size to the collapsed text size.
289+
float lineWidth =
290+
textLayout.getLineWidth(textLayout.getLineCount() - 1)
291+
* (collapsedTextSize / expandedTextSize);
292+
if (isRtl) {
293+
bounds.left = bounds.right - lineWidth;
294+
} else {
295+
bounds.right = bounds.left + lineWidth;
296+
}
297+
}
271298
}
272299

273300
private float getCollapsedTextLeftBound(int width, int gravity) {
@@ -294,19 +321,45 @@ private float getCollapsedTextRightBound(@NonNull RectF bounds, int width, int g
294321
}
295322
}
296323

297-
public float getExpandedTextHeight() {
324+
public float getExpandedTextSingleLineHeight() {
298325
getTextPaintExpanded(tmpPaint);
299326
// Return expanded height measured from the baseline.
300327
return -tmpPaint.ascent();
301328
}
302329

303-
public float getExpandedTextFullHeight() {
330+
public float getExpandedTextFullSingleLineHeight() {
304331
getTextPaintExpanded(tmpPaint);
305332
// Return expanded height measured from the baseline.
306333
return -tmpPaint.ascent() + tmpPaint.descent();
307334
}
308335

336+
public void updateTextHeights(int availableWidth) {
337+
// Set collapsed height
338+
getTextPaintCollapsed(tmpPaint);
339+
StaticLayout textLayout =
340+
createStaticLayout(
341+
collapsedMaxLines,
342+
tmpPaint,
343+
text,
344+
availableWidth * (collapsedTextSize / expandedTextSize),
345+
isRtl);
346+
collapsedHeight = textLayout.getHeight();
347+
348+
// Set expanded height
349+
getTextPaintExpanded(tmpPaint);
350+
textLayout = createStaticLayout(expandedMaxLines, tmpPaint, text, availableWidth, isRtl);
351+
expandedHeight = textLayout.getHeight();
352+
}
353+
309354
public float getCollapsedTextHeight() {
355+
return collapsedHeight != -1 ? collapsedHeight : getCollapsedSingleLineHeight();
356+
}
357+
358+
public float getExpandedTextHeight() {
359+
return expandedHeight != -1 ? expandedHeight : getExpandedTextSingleLineHeight();
360+
}
361+
362+
public float getCollapsedSingleLineHeight() {
310363
getTextPaintCollapsed(tmpPaint);
311364
// Return collapsed height measured from the baseline.
312365
return -tmpPaint.ascent();
@@ -707,12 +760,18 @@ private int getCurrentColor(@Nullable ColorStateList colorStateList) {
707760
return colorStateList.getDefaultColor();
708761
}
709762

763+
private boolean shouldTruncateCollapsedToSingleLine() {
764+
return collapsedMaxLines == ONE_LINE;
765+
}
766+
710767
private void calculateBaseOffsets(boolean forceRecalculate) {
711768
// We then calculate the collapsed text size, using the same logic
712769
calculateUsingTextSize(/* fraction= */ 1, forceRecalculate);
713770
if (textToDraw != null && textLayout != null) {
714-
textToDrawCollapsed =
715-
TextUtils.ellipsize(textToDraw, textPaint, textLayout.getWidth(), titleTextEllipsize);
771+
textToDrawCollapsed = shouldTruncateCollapsedToSingleLine()
772+
? TextUtils.ellipsize(
773+
textToDraw, textPaint, textLayout.getWidth(), titleTextEllipsize)
774+
: textToDraw;
716775
}
717776
if (textToDrawCollapsed != null) {
718777
collapsedTextWidth = measureTextWidth(textPaint, textToDrawCollapsed);
@@ -754,7 +813,7 @@ private void calculateBaseOffsets(boolean forceRecalculate) {
754813
calculateUsingTextSize(/* fraction= */ 0, forceRecalculate);
755814
float expandedTextHeight = textLayout != null ? textLayout.getHeight() : 0;
756815
float expandedTextWidth = 0;
757-
if (textLayout != null && maxLines > 1) {
816+
if (textLayout != null && expandedMaxLines > 1) {
758817
expandedTextWidth = textLayout.getWidth();
759818
} else if (textToDraw != null) {
760819
expandedTextWidth = measureTextWidth(textPaint, textToDraw);
@@ -847,6 +906,7 @@ public void draw(@NonNull Canvas canvas) {
847906
}
848907

849908
if (shouldDrawMultiline()
909+
&& shouldTruncateCollapsedToSingleLine()
850910
&& (!fadeModeEnabled || expandedFraction > fadeModeThresholdFraction)) {
851911
drawMultilineTransition(canvas, currentDrawX - textLayout.getLineStart(0), y);
852912
} else {
@@ -859,7 +919,7 @@ public void draw(@NonNull Canvas canvas) {
859919
}
860920

861921
private boolean shouldDrawMultiline() {
862-
return maxLines > 1 && (!isRtl || fadeModeEnabled);
922+
return (expandedMaxLines > 1 || collapsedMaxLines > 1) && (!isRtl || fadeModeEnabled);
863923
}
864924

865925
private void drawMultilineTransition(@NonNull Canvas canvas, float currentExpandedX, float y) {
@@ -975,11 +1035,16 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula
9751035
Typeface newTypeface;
9761036

9771037
if (isClose(fraction, /* targetValue= */ 1)) {
978-
newTextSize = collapsedTextSize;
979-
newLetterSpacing = collapsedLetterSpacing;
980-
scale = 1f;
1038+
newTextSize = shouldTruncateCollapsedToSingleLine() ? collapsedTextSize : expandedTextSize;
1039+
newLetterSpacing =
1040+
shouldTruncateCollapsedToSingleLine() ? collapsedLetterSpacing : expandedLetterSpacing;
1041+
scale =
1042+
shouldTruncateCollapsedToSingleLine()
1043+
? 1f
1044+
: lerp(expandedTextSize, collapsedTextSize, fraction, textSizeInterpolator)
1045+
/ expandedTextSize;
1046+
availableWidth = shouldTruncateCollapsedToSingleLine() ? collapsedWidth : expandedWidth;
9811047
newTypeface = collapsedTypeface;
982-
availableWidth = collapsedWidth;
9831048
} else {
9841049
newTextSize = expandedTextSize;
9851050
newLetterSpacing = expandedLetterSpacing;
@@ -1010,30 +1075,38 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula
10101075
// cap the available width so that when the expanded text scales down, it matches
10111076
// the collapsed width
10121077
// Otherwise we'll just use the expanded width
1013-
1078+
// If we are not truncating the collapsed text, when we are always scaling the expanded
1079+
// text, so we will always use the expanded width as the available width
10141080
availableWidth =
1015-
scaledDownWidth > collapsedWidth
1081+
scaledDownWidth > collapsedWidth && shouldTruncateCollapsedToSingleLine()
10161082
? min(collapsedWidth / textSizeRatio, expandedWidth)
10171083
: expandedWidth;
10181084
}
10191085
}
10201086

1087+
// Swap between the expanded and collapsed max lines depending on whether or not we're closer
1088+
// to being expanded or collapsed.
1089+
int maxLines = fraction < 0.5f ? expandedMaxLines : collapsedMaxLines;
1090+
10211091
boolean updateDrawText;
10221092
if (availableWidth > 0) {
10231093
boolean textSizeChanged = currentTextSize != newTextSize;
10241094
boolean letterSpacingChanged = currentLetterSpacing != newLetterSpacing;
10251095
boolean typefaceChanged = currentTypeface != newTypeface;
10261096
boolean availableWidthChanged = textLayout != null && availableWidth != textLayout.getWidth();
1097+
boolean maxLinesChanged = currentMaxLines != maxLines;
10271098
updateDrawText =
10281099
textSizeChanged
10291100
|| letterSpacingChanged
10301101
|| availableWidthChanged
10311102
|| typefaceChanged
1103+
|| maxLinesChanged
10321104
|| boundsChanged;
10331105
currentTextSize = newTextSize;
10341106
currentLetterSpacing = newLetterSpacing;
10351107
currentTypeface = newTypeface;
10361108
boundsChanged = false;
1109+
currentMaxLines = maxLines;
10371110
// Use linear text scaling if we're scaling the canvas
10381111
textPaint.setLinearText(scale != 1f);
10391112
} else {
@@ -1046,12 +1119,19 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula
10461119
textPaint.setLetterSpacing(currentLetterSpacing);
10471120

10481121
isRtl = calculateIsRtl(text);
1049-
textLayout = createStaticLayout(shouldDrawMultiline() ? maxLines : 1, availableWidth, isRtl);
1122+
textLayout =
1123+
createStaticLayout(
1124+
shouldDrawMultiline() ? maxLines : 1,
1125+
textPaint,
1126+
text,
1127+
availableWidth * (shouldTruncateCollapsedToSingleLine() ? 1 : scale),
1128+
isRtl);
10501129
textToDraw = textLayout.getText();
10511130
}
10521131
}
10531132

1054-
private StaticLayout createStaticLayout(int maxLines, float availableWidth, boolean isRtl) {
1133+
private StaticLayout createStaticLayout(
1134+
int maxLines, TextPaint textPaint, CharSequence text, float availableWidth, boolean isRtl) {
10551135
StaticLayout textLayout = null;
10561136
try {
10571137
// In multiline mode, the text alignment should be controlled by the static layout.
@@ -1120,15 +1200,15 @@ public CharSequence getText() {
11201200
return text;
11211201
}
11221202

1123-
public void setMaxLines(int maxLines) {
1124-
if (maxLines != this.maxLines) {
1125-
this.maxLines = maxLines;
1203+
public void setExpandedMaxLines(int expandedMaxLines) {
1204+
if (expandedMaxLines != this.expandedMaxLines) {
1205+
this.expandedMaxLines = expandedMaxLines;
11261206
recalculate();
11271207
}
11281208
}
11291209

1130-
public int getMaxLines() {
1131-
return maxLines;
1210+
public int getExpandedMaxLines() {
1211+
return expandedMaxLines;
11321212
}
11331213

11341214
/**

lib/java/com/google/android/material/internal/StaticLayoutBuilderCompat.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
* @hide
5353
*/
5454
@RestrictTo(Scope.LIBRARY_GROUP)
55-
final class StaticLayoutBuilderCompat {
55+
public final class StaticLayoutBuilderCompat {
5656

5757
static final int DEFAULT_HYPHENATION_FREQUENCY =
5858
VERSION.SDK_INT >= VERSION_CODES.M ? StaticLayout.HYPHENATION_FREQUENCY_NORMAL : 0;
@@ -236,6 +236,7 @@ public StaticLayoutBuilderCompat setStaticLayoutBuilderConfigurer(
236236
return this;
237237
}
238238

239+
@NonNull
239240
/** A method that allows to create a StaticLayout with maxLines on all supported API levels. */
240241
public StaticLayout build() throws StaticLayoutBuilderCompatException {
241242
if (source == null) {
@@ -361,12 +362,19 @@ private void createConstructorWithReflection() throws StaticLayoutBuilderCompatE
361362
}
362363
}
363364

365+
@NonNull
364366
public StaticLayoutBuilderCompat setIsRtl(boolean isRtl) {
365367
this.isRtl = isRtl;
366368
return this;
367369
}
368370

369-
static class StaticLayoutBuilderCompatException extends Exception {
371+
/**
372+
* Class representing a StaticLayoutBuilder exception from initializing a StaticLayout.
373+
*
374+
* @hide
375+
*/
376+
@RestrictTo(Scope.LIBRARY_GROUP)
377+
public static class StaticLayoutBuilderCompatException extends Exception {
370378

371379
StaticLayoutBuilderCompatException(Throwable cause) {
372380
super("Error thrown initializing StaticLayout " + cause.getMessage(), cause);

0 commit comments

Comments
 (0)