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
     )
 }