Fix height calculation for label in OutlinedTextField
Also updates some binary conditions to lerps for a
smoother transition between states.
This uncovered a bug in TestFieldDecorationTest where
the text fields were never properly focused before testing,
which has been fixed as well.
Bug: b/274660399
Test: added
Change-Id: Ibfb2d930d34f51e60761a0a531ae8d52373ee7e7
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index b791476..8425818 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -425,6 +425,28 @@
}
@Test
+ fun testOutlinedTextField_labelHeight_contributesToTextFieldMeasurements_whenUnfocused() {
+ val tfSize = Ref<IntSize>()
+ val labelHeight = 200.dp
+ rule.setMaterialContent {
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.testTag(TextfieldTag).onGloballyPositioned {
+ tfSize.value = it.size
+ },
+ label = {
+ Box(Modifier.size(width = 50.dp, height = labelHeight))
+ },
+ )
+ }
+
+ rule.runOnIdleWithDensity {
+ assertThat(tfSize.value!!.height).isAtLeast(labelHeight.roundToPx())
+ }
+ }
+
+ @Test
fun testOutlinedTextField_labelWidth_isNotAffectedByTrailingIcon_whenFocused() {
val textFieldWidth = 100.dp
val labelRequestedWidth = 65.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
index f3fecc2..777adeb 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldDecorationBoxTest.kt
@@ -38,6 +38,8 @@
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@@ -523,6 +525,7 @@
) {
var size: IntSize? = null
var position: Offset? = null
+ val focusRequester = FocusRequester()
rule.setMaterialContent {
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection,
@@ -534,6 +537,7 @@
BasicTextField(
value = value,
onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester),
singleLine = singleLine,
interactionSource = interactionSource
) {
@@ -560,6 +564,10 @@
}
}
+ rule.runOnUiThread {
+ focusRequester.requestFocus()
+ }
+
rule.runOnIdle {
with(Density) {
assertThat(size).isNotNull()
@@ -624,6 +632,7 @@
) {
var size: IntSize? = null
var position: Offset? = null
+ val focusRequester = FocusRequester()
rule.setMaterialContent {
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection,
@@ -635,6 +644,7 @@
BasicTextField(
value = value,
onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester),
singleLine = singleLine,
interactionSource = interactionSource
) {
@@ -664,6 +674,10 @@
}
}
+ rule.runOnUiThread {
+ focusRequester.requestFocus()
+ }
+
rule.runOnIdle {
with(Density) {
assertThat(size).isNotNull()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 6555ac1..5f3120a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -589,16 +589,15 @@
)
// measure label
- val isLabelInMiddleSection = animationProgress < 1f
val labelHorizontalPaddingOffset =
paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +
paddingValues.calculateRightPadding(layoutDirection).roundToPx()
val labelConstraints = relaxedConstraints.offset(
- horizontal = if (isLabelInMiddleSection) {
- -occupiedSpaceHorizontally - labelHorizontalPaddingOffset
- } else {
- -labelHorizontalPaddingOffset
- },
+ horizontal = lerp(
+ -occupiedSpaceHorizontally - labelHorizontalPaddingOffset,
+ -labelHorizontalPaddingOffset,
+ animationProgress,
+ ),
vertical = -bottomPadding
)
val labelPlaceable =
@@ -633,21 +632,22 @@
textFieldPlaceableWidth = textFieldPlaceable.width,
labelPlaceableWidth = widthOrZero(labelPlaceable),
placeholderPlaceableWidth = widthOrZero(placeholderPlaceable),
- isLabelInMiddleSection = isLabelInMiddleSection,
+ animationProgress = animationProgress,
constraints = constraints,
density = density,
paddingValues = paddingValues,
)
val height =
calculateHeight(
- heightOrZero(leadingPlaceable),
- heightOrZero(trailingPlaceable),
- textFieldPlaceable.height,
- heightOrZero(labelPlaceable),
- heightOrZero(placeholderPlaceable),
- constraints,
- density,
- paddingValues
+ leadingPlaceableHeight = heightOrZero(leadingPlaceable),
+ trailingPlaceableHeight = heightOrZero(trailingPlaceable),
+ textFieldPlaceableHeight = textFieldPlaceable.height,
+ labelPlaceableHeight = heightOrZero(labelPlaceable),
+ placeholderPlaceableHeight = heightOrZero(placeholderPlaceable),
+ animationProgress = animationProgress,
+ constraints = constraints,
+ density = density,
+ paddingValues = paddingValues,
)
val borderPlaceable = measurables.first { it.layoutId == BorderId }.measure(
@@ -738,7 +738,7 @@
textFieldPlaceableWidth = textFieldWidth,
labelPlaceableWidth = labelWidth,
placeholderPlaceableWidth = placeholderWidth,
- isLabelInMiddleSection = animationProgress < 1f,
+ animationProgress = animationProgress,
constraints = ZeroConstraints,
density = density,
paddingValues = paddingValues,
@@ -770,6 +770,7 @@
textFieldPlaceableHeight = textFieldHeight,
labelPlaceableHeight = labelHeight,
placeholderPlaceableHeight = placeholderHeight,
+ animationProgress = animationProgress,
constraints = ZeroConstraints,
density = density,
paddingValues = paddingValues
@@ -787,27 +788,25 @@
textFieldPlaceableWidth: Int,
labelPlaceableWidth: Int,
placeholderPlaceableWidth: Int,
- isLabelInMiddleSection: Boolean,
+ animationProgress: Float,
constraints: Constraints,
density: Float,
paddingValues: PaddingValues,
): Int {
val middleSection = maxOf(
textFieldPlaceableWidth,
- if (isLabelInMiddleSection) labelPlaceableWidth else 0,
+ lerp(labelPlaceableWidth, 0, animationProgress),
placeholderPlaceableWidth
)
val wrappedWidth =
leadingPlaceableWidth + middleSection + trailingPlaceableWidth
+
+ // Actual LayoutDirection doesn't matter; we only need the sum
+ val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
+ paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density
val focusedLabelWidth =
- if (!isLabelInMiddleSection) {
- // Actual LayoutDirection doesn't matter; we only need the sum
- val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
- paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density
- labelPlaceableWidth + labelHorizontalPadding.roundToInt()
- } else {
- 0
- }
+ ((labelPlaceableWidth + labelHorizontalPadding) * animationProgress).roundToInt()
+
return maxOf(wrappedWidth, focusedLabelWidth, constraints.minWidth)
}
@@ -821,23 +820,25 @@
textFieldPlaceableHeight: Int,
labelPlaceableHeight: Int,
placeholderPlaceableHeight: Int,
+ animationProgress: Float,
constraints: Constraints,
density: Float,
paddingValues: PaddingValues
): Int {
- // middle section is defined as a height of the text field or placeholder ( whichever is
- // taller) plus 16.dp or half height of the label if it is taller, given that the label
- // is vertically centered to the top edge of the resulting text field's container
- val inputFieldHeight = max(
+ val inputFieldHeight = maxOf(
textFieldPlaceableHeight,
- placeholderPlaceableHeight
+ placeholderPlaceableHeight,
+ lerp(labelPlaceableHeight, 0, animationProgress),
)
val topPadding = paddingValues.calculateTopPadding().value * density
- val bottomPadding = paddingValues.calculateBottomPadding().value * density
- val middleSectionHeight = inputFieldHeight + bottomPadding + max(
+ val actualTopPadding = lerp(
topPadding,
- labelPlaceableHeight / 2f
+ max(topPadding, labelPlaceableHeight / 2f),
+ animationProgress,
)
+ val bottomPadding = paddingValues.calculateBottomPadding().value * density
+ val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding
+
return max(
constraints.minHeight,
maxOf(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
index 7a40dbd..0399416 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
@@ -395,6 +395,28 @@
}
@Test
+ fun testOutlinedTextField_labelHeight_contributesToTextFieldMeasurements_whenUnfocused() {
+ val tfSize = Ref<IntSize>()
+ val labelHeight = 200.dp
+ rule.setMaterialContent(lightColorScheme()) {
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.testTag(TextFieldTag).onGloballyPositioned {
+ tfSize.value = it.size
+ },
+ label = {
+ Box(Modifier.size(width = 50.dp, height = labelHeight))
+ },
+ )
+ }
+
+ rule.runOnIdleWithDensity {
+ assertThat(tfSize.value!!.height).isAtLeast(labelHeight.roundToPx())
+ }
+ }
+
+ @Test
fun testOutlinedTextField_labelWidth_isNotAffectedByTrailingIcon_whenFocused() {
val textFieldWidth = 100.dp
val labelRequestedWidth = 65.dp
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
index 89dbd0d..1938541 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
@@ -30,6 +30,8 @@
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@@ -605,6 +607,7 @@
) {
var size: IntSize? = null
var position: Offset? = null
+ val focusRequester = FocusRequester()
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection,
@@ -616,6 +619,7 @@
BasicTextField(
value = value,
onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester),
singleLine = singleLine,
interactionSource = interactionSource
) {
@@ -656,6 +660,10 @@
}
}
+ rule.runOnUiThread {
+ focusRequester.requestFocus()
+ }
+
rule.runOnIdle {
with(Density) {
assertThat(size).isNotNull()
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index b81bc65..05c8c52 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -671,16 +671,15 @@
occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(suffixPlaceable))
// measure label
- val isLabelInMiddleSection = animationProgress < 1f
val labelHorizontalPaddingOffset =
paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +
paddingValues.calculateRightPadding(layoutDirection).roundToPx()
val labelConstraints = relaxedConstraints.offset(
- horizontal = if (isLabelInMiddleSection) {
- -occupiedSpaceHorizontally - labelHorizontalPaddingOffset
- } else {
- -labelHorizontalPaddingOffset
- },
+ horizontal = lerp(
+ -occupiedSpaceHorizontally - labelHorizontalPaddingOffset, // label in middle
+ -labelHorizontalPaddingOffset, // label at top
+ animationProgress,
+ ),
vertical = -bottomPadding
)
val labelPlaceable =
@@ -724,7 +723,7 @@
textFieldPlaceableWidth = textFieldPlaceable.width,
labelPlaceableWidth = widthOrZero(labelPlaceable),
placeholderPlaceableWidth = widthOrZero(placeholderPlaceable),
- isLabelInMiddleSection = isLabelInMiddleSection,
+ animationProgress = animationProgress,
constraints = constraints,
density = density,
paddingValues = paddingValues,
@@ -740,14 +739,15 @@
val totalHeight =
calculateHeight(
- leadingPlaceableHeight = heightOrZero(leadingPlaceable),
- trailingPlaceableHeight = heightOrZero(trailingPlaceable),
- prefixPlaceableHeight = heightOrZero(prefixPlaceable),
- suffixPlaceableHeight = heightOrZero(suffixPlaceable),
- textFieldPlaceableHeight = textFieldPlaceable.height,
- labelPlaceableHeight = heightOrZero(labelPlaceable),
- placeholderPlaceableHeight = heightOrZero(placeholderPlaceable),
- supportingPlaceableHeight = heightOrZero(supportingPlaceable),
+ leadingHeight = heightOrZero(leadingPlaceable),
+ trailingHeight = heightOrZero(trailingPlaceable),
+ prefixHeight = heightOrZero(prefixPlaceable),
+ suffixHeight = heightOrZero(suffixPlaceable),
+ textFieldHeight = textFieldPlaceable.height,
+ labelHeight = heightOrZero(labelPlaceable),
+ placeholderHeight = heightOrZero(placeholderPlaceable),
+ supportingHeight = heightOrZero(supportingPlaceable),
+ animationProgress = animationProgress,
constraints = constraints,
density = density,
paddingValues = paddingValues,
@@ -853,7 +853,7 @@
textFieldPlaceableWidth = textFieldWidth,
labelPlaceableWidth = labelWidth,
placeholderPlaceableWidth = placeholderWidth,
- isLabelInMiddleSection = animationProgress < 1f,
+ animationProgress = animationProgress,
constraints = ZeroConstraints,
density = density,
paddingValues = paddingValues,
@@ -889,14 +889,15 @@
intrinsicMeasurer(it, width)
} ?: 0
return calculateHeight(
- leadingPlaceableHeight = leadingHeight,
- trailingPlaceableHeight = trailingHeight,
- prefixPlaceableHeight = prefixHeight,
- suffixPlaceableHeight = suffixHeight,
- textFieldPlaceableHeight = textFieldHeight,
- labelPlaceableHeight = labelHeight,
- placeholderPlaceableHeight = placeholderHeight,
- supportingPlaceableHeight = supportingHeight,
+ leadingHeight = leadingHeight,
+ trailingHeight = trailingHeight,
+ prefixHeight = prefixHeight,
+ suffixHeight = suffixHeight,
+ textFieldHeight = textFieldHeight,
+ labelHeight = labelHeight,
+ placeholderHeight = placeholderHeight,
+ supportingHeight = supportingHeight,
+ animationProgress = animationProgress,
constraints = ZeroConstraints,
density = density,
paddingValues = paddingValues
@@ -915,7 +916,7 @@
textFieldPlaceableWidth: Int,
labelPlaceableWidth: Int,
placeholderPlaceableWidth: Int,
- isLabelInMiddleSection: Boolean,
+ animationProgress: Float,
constraints: Constraints,
density: Float,
paddingValues: PaddingValues,
@@ -925,19 +926,16 @@
textFieldPlaceableWidth + affixTotalWidth,
placeholderPlaceableWidth + affixTotalWidth,
// Prefix/suffix does not get applied to label
- if (isLabelInMiddleSection) labelPlaceableWidth else 0,
+ lerp(labelPlaceableWidth, 0, animationProgress),
)
val wrappedWidth =
leadingPlaceableWidth + middleSection + trailingPlaceableWidth
+
+ // Actual LayoutDirection doesn't matter; we only need the sum
+ val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
+ paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density
val focusedLabelWidth =
- if (!isLabelInMiddleSection) {
- // Actual LayoutDirection doesn't matter; we only need the sum
- val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
- paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density
- labelPlaceableWidth + labelHorizontalPadding.roundToInt()
- } else {
- 0
- }
+ ((labelPlaceableWidth + labelHorizontalPadding) * animationProgress).roundToInt()
return maxOf(wrappedWidth, focusedLabelWidth, constraints.minWidth)
}
@@ -947,40 +945,38 @@
* inside the text field.
*/
private fun calculateHeight(
- leadingPlaceableHeight: Int,
- trailingPlaceableHeight: Int,
- prefixPlaceableHeight: Int,
- suffixPlaceableHeight: Int,
- textFieldPlaceableHeight: Int,
- labelPlaceableHeight: Int,
- placeholderPlaceableHeight: Int,
- supportingPlaceableHeight: Int,
+ leadingHeight: Int,
+ trailingHeight: Int,
+ prefixHeight: Int,
+ suffixHeight: Int,
+ textFieldHeight: Int,
+ labelHeight: Int,
+ placeholderHeight: Int,
+ supportingHeight: Int,
+ animationProgress: Float,
constraints: Constraints,
density: Float,
paddingValues: PaddingValues
): Int {
- // middle section is defined as a height of the text field or placeholder (whichever is
- // taller) plus 16.dp or half height of the label if it is taller, given that the label
- // is vertically centered to the top edge of the resulting text field's container
- val inputFieldHeight = max(
- textFieldPlaceableHeight,
- placeholderPlaceableHeight
+ val inputFieldHeight = maxOf(
+ textFieldHeight,
+ placeholderHeight,
+ lerp(labelHeight, 0, animationProgress)
)
val topPadding = paddingValues.calculateTopPadding().value * density
+ val actualTopPadding = lerp(topPadding, max(topPadding, labelHeight / 2f), animationProgress)
val bottomPadding = paddingValues.calculateBottomPadding().value * density
- val middleSectionHeight = inputFieldHeight + bottomPadding + max(
- topPadding,
- labelPlaceableHeight / 2f
- )
+ val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding
+
return max(
constraints.minHeight,
maxOf(
- leadingPlaceableHeight,
- trailingPlaceableHeight,
- prefixPlaceableHeight,
- suffixPlaceableHeight,
+ leadingHeight,
+ trailingHeight,
+ prefixHeight,
+ suffixHeight,
middleSectionHeight.roundToInt()
- ) + supportingPlaceableHeight
+ ) + supportingHeight
)
}