Skip to content

Commit

Permalink
[TextInputLayout] Add hintMaxLines attribute
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 685884472
  • Loading branch information
imhappi authored and pekingme committed Oct 15, 2024
1 parent 9bf5edd commit 7f01739
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 33 deletions.
1 change: 1 addition & 0 deletions docs/components/TextField.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ Element | Attribute | Related method(s)
**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))
**Collapsed (floating) color** | `app:hintTextColor` | `setHintTextColor`<br/>`getHintTextColor` | `?attr/colorPrimary`
**Typography** | `app:hintTextAppearance` | `setHintTextAppearance` | `?attr/textAppearanceBodySmall`
**Max number of lines** | `app:hintMaxLines` | `setHintMaxLines`<br/>`getHintMaxLines` | `1`

**Note:** The `android:hint` should always be set on the `TextInputLayout`
instead of on the `EditText` in order to avoid unintended behaviors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public CollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet
a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1);

if (a.hasValue(R.styleable.CollapsingToolbarLayout_maxLines)) {
collapsingTextHelper.setMaxLines(a.getInt(R.styleable.CollapsingToolbarLayout_maxLines, 1));
collapsingTextHelper.setExpandedMaxLines(a.getInt(R.styleable.CollapsingToolbarLayout_maxLines, 1));
}

if (a.hasValue(R.styleable.CollapsingToolbarLayout_titlePositionInterpolator)) {
Expand Down Expand Up @@ -616,15 +616,16 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

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

int lineCount = collapsingTextHelper.getExpandedLineCount();
if (lineCount > 1) {
// Add extra height based on the amount of height beyond the first line of title text.
int expandedTextHeight = Math.round(collapsingTextHelper.getExpandedTextFullHeight());
int expandedTextHeight =
Math.round(collapsingTextHelper.getExpandedTextFullSingleLineHeight());
extraMultilineHeight = expandedTextHeight * (lineCount - 1);
int newHeight = getMeasuredHeight() + extraMultilineHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
Expand Down Expand Up @@ -1407,7 +1408,7 @@ public void setExpandedTitleMarginBottom(int margin) {
*/
@RestrictTo(LIBRARY_GROUP)
public void setMaxLines(int maxLines) {
collapsingTextHelper.setMaxLines(maxLines);
collapsingTextHelper.setExpandedMaxLines(maxLines);
}

/**
Expand All @@ -1416,7 +1417,7 @@ public void setMaxLines(int maxLines) {
*/
@RestrictTo(LIBRARY_GROUP)
public int getMaxLines() {
return collapsingTextHelper.getMaxLines();
return collapsingTextHelper.getExpandedMaxLines();
}

/**
Expand Down
122 changes: 101 additions & 21 deletions lib/java/com/google/android/material/internal/CollapsingTextHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public final class CollapsingTextHelper {
private float currentShadowDx;
private float currentShadowDy;
private int currentShadowColor;
private int currentMaxLines;

private int[] state;

Expand Down Expand Up @@ -161,11 +162,16 @@ public final class CollapsingTextHelper {
private float collapsedTextBlend;
private float expandedTextBlend;
private CharSequence textToDrawCollapsed;
private int maxLines = 1;

private static final int ONE_LINE = 1;
private int expandedMaxLines = ONE_LINE;
private int collapsedMaxLines = ONE_LINE;
private float lineSpacingAdd = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_ADD;
private float lineSpacingMultiplier = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_MULTIPLIER;
private int hyphenationFrequency = StaticLayoutBuilderCompat.DEFAULT_HYPHENATION_FREQUENCY;
@Nullable private StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer;
private int collapsedHeight = -1;
private int expandedHeight = -1;

public CollapsingTextHelper(View view) {
this.view = view;
Expand All @@ -181,6 +187,13 @@ public CollapsingTextHelper(View view) {
maybeUpdateFontWeightAdjustment(view.getContext().getResources().getConfiguration());
}

public void setCollapsedMaxLines(int collapsedMaxLines) {
if (collapsedMaxLines != this.collapsedMaxLines) {
this.collapsedMaxLines = collapsedMaxLines;
recalculate();
}
}

public void setTextSizeInterpolator(TimeInterpolator interpolator) {
textSizeInterpolator = interpolator;
recalculate();
Expand Down Expand Up @@ -261,13 +274,27 @@ public void setCollapsedBounds(@NonNull Rect bounds) {
setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
}

public void getCollapsedTextActualBounds(@NonNull RectF bounds, int labelWidth, int textGravity) {
public void getCollapsedTextBottomTextBounds(
@NonNull RectF bounds, int labelWidth, int textGravity) {
isRtl = calculateIsRtl(text);
bounds.left = max(getCollapsedTextLeftBound(labelWidth, textGravity), collapsedBounds.left);
bounds.top = collapsedBounds.top;
bounds.right =
min(getCollapsedTextRightBound(bounds, labelWidth, textGravity), collapsedBounds.right);
bounds.bottom = collapsedBounds.top + getCollapsedTextHeight();
if (textLayout != null && !shouldTruncateCollapsedToSingleLine()) {
// If the text is not truncated to one line when collapsed, we want to return the width of the
// bottommost line, which is the textLayout's line width * the scale factor of the expanded
// text size to the collapsed text size.
float lineWidth =
textLayout.getLineWidth(textLayout.getLineCount() - 1)
* (collapsedTextSize / expandedTextSize);
if (isRtl) {
bounds.left = bounds.right - lineWidth;
} else {
bounds.right = bounds.left + lineWidth;
}
}
}

private float getCollapsedTextLeftBound(int width, int gravity) {
Expand All @@ -294,19 +321,45 @@ private float getCollapsedTextRightBound(@NonNull RectF bounds, int width, int g
}
}

public float getExpandedTextHeight() {
public float getExpandedTextSingleLineHeight() {
getTextPaintExpanded(tmpPaint);
// Return expanded height measured from the baseline.
return -tmpPaint.ascent();
}

public float getExpandedTextFullHeight() {
public float getExpandedTextFullSingleLineHeight() {
getTextPaintExpanded(tmpPaint);
// Return expanded height measured from the baseline.
return -tmpPaint.ascent() + tmpPaint.descent();
}

public void updateTextHeights(int availableWidth) {
// Set collapsed height
getTextPaintCollapsed(tmpPaint);
StaticLayout textLayout =
createStaticLayout(
collapsedMaxLines,
tmpPaint,
text,
availableWidth * (collapsedTextSize / expandedTextSize),
isRtl);
collapsedHeight = textLayout.getHeight();

// Set expanded height
getTextPaintExpanded(tmpPaint);
textLayout = createStaticLayout(expandedMaxLines, tmpPaint, text, availableWidth, isRtl);
expandedHeight = textLayout.getHeight();
}

public float getCollapsedTextHeight() {
return collapsedHeight != -1 ? collapsedHeight : getCollapsedSingleLineHeight();
}

public float getExpandedTextHeight() {
return expandedHeight != -1 ? expandedHeight : getExpandedTextSingleLineHeight();
}

public float getCollapsedSingleLineHeight() {
getTextPaintCollapsed(tmpPaint);
// Return collapsed height measured from the baseline.
return -tmpPaint.ascent();
Expand Down Expand Up @@ -707,12 +760,18 @@ private int getCurrentColor(@Nullable ColorStateList colorStateList) {
return colorStateList.getDefaultColor();
}

private boolean shouldTruncateCollapsedToSingleLine() {
return collapsedMaxLines == ONE_LINE;
}

private void calculateBaseOffsets(boolean forceRecalculate) {
// We then calculate the collapsed text size, using the same logic
calculateUsingTextSize(/* fraction= */ 1, forceRecalculate);
if (textToDraw != null && textLayout != null) {
textToDrawCollapsed =
TextUtils.ellipsize(textToDraw, textPaint, textLayout.getWidth(), titleTextEllipsize);
textToDrawCollapsed = shouldTruncateCollapsedToSingleLine()
? TextUtils.ellipsize(
textToDraw, textPaint, textLayout.getWidth(), titleTextEllipsize)
: textToDraw;
}
if (textToDrawCollapsed != null) {
collapsedTextWidth = measureTextWidth(textPaint, textToDrawCollapsed);
Expand Down Expand Up @@ -754,7 +813,7 @@ private void calculateBaseOffsets(boolean forceRecalculate) {
calculateUsingTextSize(/* fraction= */ 0, forceRecalculate);
float expandedTextHeight = textLayout != null ? textLayout.getHeight() : 0;
float expandedTextWidth = 0;
if (textLayout != null && maxLines > 1) {
if (textLayout != null && expandedMaxLines > 1) {
expandedTextWidth = textLayout.getWidth();
} else if (textToDraw != null) {
expandedTextWidth = measureTextWidth(textPaint, textToDraw);
Expand Down Expand Up @@ -847,6 +906,7 @@ public void draw(@NonNull Canvas canvas) {
}

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

private boolean shouldDrawMultiline() {
return maxLines > 1 && (!isRtl || fadeModeEnabled);
return (expandedMaxLines > 1 || collapsedMaxLines > 1) && (!isRtl || fadeModeEnabled);
}

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

if (isClose(fraction, /* targetValue= */ 1)) {
newTextSize = collapsedTextSize;
newLetterSpacing = collapsedLetterSpacing;
scale = 1f;
newTextSize = shouldTruncateCollapsedToSingleLine() ? collapsedTextSize : expandedTextSize;
newLetterSpacing =
shouldTruncateCollapsedToSingleLine() ? collapsedLetterSpacing : expandedLetterSpacing;
scale =
shouldTruncateCollapsedToSingleLine()
? 1f
: lerp(expandedTextSize, collapsedTextSize, fraction, textSizeInterpolator)
/ expandedTextSize;
availableWidth = shouldTruncateCollapsedToSingleLine() ? collapsedWidth : expandedWidth;
newTypeface = collapsedTypeface;
availableWidth = collapsedWidth;
} else {
newTextSize = expandedTextSize;
newLetterSpacing = expandedLetterSpacing;
Expand Down Expand Up @@ -1010,30 +1075,38 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula
// cap the available width so that when the expanded text scales down, it matches
// the collapsed width
// Otherwise we'll just use the expanded width

// If we are not truncating the collapsed text, when we are always scaling the expanded
// text, so we will always use the expanded width as the available width
availableWidth =
scaledDownWidth > collapsedWidth
scaledDownWidth > collapsedWidth && shouldTruncateCollapsedToSingleLine()
? min(collapsedWidth / textSizeRatio, expandedWidth)
: expandedWidth;
}
}

// Swap between the expanded and collapsed max lines depending on whether or not we're closer
// to being expanded or collapsed.
int maxLines = fraction < 0.5f ? expandedMaxLines : collapsedMaxLines;

boolean updateDrawText;
if (availableWidth > 0) {
boolean textSizeChanged = currentTextSize != newTextSize;
boolean letterSpacingChanged = currentLetterSpacing != newLetterSpacing;
boolean typefaceChanged = currentTypeface != newTypeface;
boolean availableWidthChanged = textLayout != null && availableWidth != textLayout.getWidth();
boolean maxLinesChanged = currentMaxLines != maxLines;
updateDrawText =
textSizeChanged
|| letterSpacingChanged
|| availableWidthChanged
|| typefaceChanged
|| maxLinesChanged
|| boundsChanged;
currentTextSize = newTextSize;
currentLetterSpacing = newLetterSpacing;
currentTypeface = newTypeface;
boundsChanged = false;
currentMaxLines = maxLines;
// Use linear text scaling if we're scaling the canvas
textPaint.setLinearText(scale != 1f);
} else {
Expand All @@ -1046,12 +1119,19 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula
textPaint.setLetterSpacing(currentLetterSpacing);

isRtl = calculateIsRtl(text);
textLayout = createStaticLayout(shouldDrawMultiline() ? maxLines : 1, availableWidth, isRtl);
textLayout =
createStaticLayout(
shouldDrawMultiline() ? maxLines : 1,
textPaint,
text,
availableWidth * (shouldTruncateCollapsedToSingleLine() ? 1 : scale),
isRtl);
textToDraw = textLayout.getText();
}
}

private StaticLayout createStaticLayout(int maxLines, float availableWidth, boolean isRtl) {
private StaticLayout createStaticLayout(
int maxLines, TextPaint textPaint, CharSequence text, float availableWidth, boolean isRtl) {
StaticLayout textLayout = null;
try {
// In multiline mode, the text alignment should be controlled by the static layout.
Expand Down Expand Up @@ -1120,15 +1200,15 @@ public CharSequence getText() {
return text;
}

public void setMaxLines(int maxLines) {
if (maxLines != this.maxLines) {
this.maxLines = maxLines;
public void setExpandedMaxLines(int expandedMaxLines) {
if (expandedMaxLines != this.expandedMaxLines) {
this.expandedMaxLines = expandedMaxLines;
recalculate();
}
}

public int getMaxLines() {
return maxLines;
public int getExpandedMaxLines() {
return expandedMaxLines;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
final class StaticLayoutBuilderCompat {
public final class StaticLayoutBuilderCompat {

static final int DEFAULT_HYPHENATION_FREQUENCY =
VERSION.SDK_INT >= VERSION_CODES.M ? StaticLayout.HYPHENATION_FREQUENCY_NORMAL : 0;
Expand Down Expand Up @@ -236,6 +236,7 @@ public StaticLayoutBuilderCompat setStaticLayoutBuilderConfigurer(
return this;
}

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

@NonNull
public StaticLayoutBuilderCompat setIsRtl(boolean isRtl) {
this.isRtl = isRtl;
return this;
}

static class StaticLayoutBuilderCompatException extends Exception {
/**
* Class representing a StaticLayoutBuilder exception from initializing a StaticLayout.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static class StaticLayoutBuilderCompatException extends Exception {

StaticLayoutBuilderCompatException(Throwable cause) {
super("Error thrown initializing StaticLayout " + cause.getMessage(), cause);
Expand Down
Loading

0 comments on commit 7f01739

Please sign in to comment.