Refactor FilledTextfield to use a single layout node
Also implemented a new spec proposal - to use internal text field's bounding box height and paddings to define the height of the material text field itself.
Fixes: 158763491
Test: material tests + checked demo
Change-Id: I9d8da2be6b4002ca2d3b16617090e90e53082805
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/FilledTextFieldTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/FilledTextFieldTest.kt
index 51d1105..5f00a61 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/FilledTextFieldTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/FilledTextFieldTest.kt
@@ -89,7 +89,7 @@
private val ExpectedPadding = 16.dp
private val IconPadding = 12.dp
private val ExpectedBaselineOffset = 20.dp
- private val TopPaddingFilledTextfield = 3.dp
+ private val TopPaddingFilledTextfield = 4.dp
private val IconColorAlpha = 0.54f
private val TextfieldTag = "textField"
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index 287bd91..c86e2f4 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -47,6 +47,7 @@
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.constrain
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@@ -624,117 +625,32 @@
lineWidth = indicatorWidth,
color = indicatorColor
),
- textField = @Composable {
- // places input field, label, placeholder
- TextFieldWithLabelLayout(
- animationProgress = labelProgress,
- modifier = Modifier
- .padding(
- start = TextFieldPadding,
- end = TextFieldPadding
- ),
- placeholder = decoratedPlaceholder,
- label = decoratedLabel,
- textField = decoratedTextField
- )
- },
+ textField = decoratedTextField,
+ placeholder = decoratedPlaceholder,
+ label = decoratedLabel,
leading = leading,
trailing = trailing,
leadingColor = leadingColor,
- trailingColor = trailingColor
+ trailingColor = trailingColor,
+ animationProgress = labelProgress
)
}
/**
- * Layout of the text field, label and placeholder part in [TextField]
- */
-@Composable
-private fun TextFieldWithLabelLayout(
- animationProgress: Float,
- modifier: Modifier,
- placeholder: @Composable (() -> Unit)?,
- label: @Composable () -> Unit,
- textField: @Composable (Modifier) -> Unit
-) {
- Layout(
- children = {
- if (placeholder != null) {
- Box(modifier = Modifier.layoutId(PlaceholderId), children = placeholder)
- }
- Box(modifier = Modifier.layoutId(LabelId), children = label)
- textField(Modifier.layoutId(TextFieldId))
- },
- modifier = modifier
- ) { measurables, constraints ->
- val placeholderPlaceable =
- measurables.find { it.id == PlaceholderId }
- ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
-
- val baseLineOffset = FirstBaselineOffset.toIntPx()
- val lastBaselineOffset = LastBaselineOffset.toIntPx()
- val padding = FilledTextFieldTopPadding.toIntPx()
-
- val labelConstraints = constraints
- .offset(vertical = -LastBaselineOffset.toIntPx())
- .copy(minWidth = 0, minHeight = 0)
- val labelPlaceable = measurables.first { it.id == LabelId }.measure(labelConstraints)
- val labelBaseline = labelPlaceable[LastBaseline].let {
- if (it != AlignmentLine.Unspecified) it else labelPlaceable.height
- }
- val labelEndPosition = (baseLineOffset - labelBaseline).coerceAtLeast(0)
- // to support cases where label is not a text
- val effectiveLabelBaseline = max(labelBaseline, baseLineOffset)
-
- val textFieldConstraints = constraints
- .offset(vertical = -lastBaselineOffset - padding - effectiveLabelBaseline)
- .copy(minHeight = 0)
- val textFieldPlaceable = measurables
- .first { it.id == TextFieldId }
- .measure(textFieldConstraints)
- val textFieldLastBaseline = textFieldPlaceable[LastBaseline]
- require(textFieldLastBaseline != AlignmentLine.Unspecified) { "No text last baseline." }
-
- val width = max(textFieldPlaceable.width, constraints.minWidth)
- val height = max(
- effectiveLabelBaseline + padding + textFieldLastBaseline + lastBaselineOffset,
- constraints.minHeight
- )
-
- layout(width, height) {
- // Text field and label are placed with respect to the baseline offsets.
- // But if label is empty, then the text field should be centered vertically.
- if (labelPlaceable.width != 0) {
- placeLabelAndTextfield(
- width,
- height,
- textFieldPlaceable,
- labelPlaceable,
- placeholderPlaceable,
- labelEndPosition,
- effectiveLabelBaseline + padding,
- animationProgress
- )
- } else {
- placeTextfield(height, textFieldPlaceable, placeholderPlaceable)
- }
- }
- }
-}
-
-/**
- * Layout of the leading and trailing icons and the text field part in [TextField].
- * It differs from the Row as it does not lose the minHeight constraint which is needed to
- * correctly place the text field and label.
- * Should be revisited if b/154202249 is fixed so that Row could be used instead
+ * Layout of the leading and trailing icons and the input field, label and placeholder in
+ * [TextField].
*/
@Composable
private fun IconsWithTextFieldLayout(
modifier: Modifier = Modifier,
- textField: @Composable () -> Unit,
+ textField: @Composable (Modifier) -> Unit,
+ label: @Composable () -> Unit,
+ placeholder: @Composable (() -> Unit)?,
leading: @Composable (() -> Unit)?,
trailing: @Composable (() -> Unit)?,
leadingColor: Color,
- trailingColor: Color
+ trailingColor: Color,
+ animationProgress: Float
) {
Layout(
children = {
@@ -754,73 +670,189 @@
)
}
}
- textField()
+ val padding = Modifier.padding(horizontal = TextFieldPadding)
+ if (placeholder != null) {
+ Box(
+ modifier = Modifier.layoutId(PlaceholderId).then(padding),
+ children = placeholder
+ )
+ }
+ Box(
+ modifier = Modifier
+ .layoutId(LabelId)
+ .iconPadding(
+ start = TextFieldPadding,
+ end = TextFieldPadding
+ ),
+ children = label
+ )
+ textField(Modifier.layoutId(TextFieldId).then(padding))
},
modifier = modifier
) { measurables, incomingConstraints ->
- val constraints =
- incomingConstraints.copy(minWidth = 0, minHeight = 0)
- var occupiedSpace = 0
+ val baseLineOffset = FirstBaselineOffset.toIntPx()
+ val bottomPadding = LastBaselineOffset.toIntPx()
+ val topPadding = FilledTextFieldTopPadding.toIntPx()
+ var occupiedSpaceHorizontally = 0
- val leadingPlaceable = measurables.find { it.id == "leading" }?.measure(constraints)
- occupiedSpace += widthOrZero(
+ // measure leading icon
+ val constraints = incomingConstraints.copy(minWidth = 0, minHeight = 0)
+ val leadingPlaceable =
+ measurables.find { it.id == "leading" }?.measure(constraints)
+ occupiedSpaceHorizontally += widthOrZero(
leadingPlaceable
)
+ // measure trailing icon
val trailingPlaceable = measurables.find { it.id == "trailing" }
- ?.measure(constraints.offset(horizontal = -occupiedSpace))
- occupiedSpace += widthOrZero(
+ ?.measure(constraints.offset(horizontal = -occupiedSpaceHorizontally))
+ occupiedSpaceHorizontally += widthOrZero(
trailingPlaceable
)
- // represents the layout that holds textfield, label and placeholder
- val textFieldPlaceable = measurables.first {
- it.id != "leading" && it.id != "trailing"
- }.measure(incomingConstraints.offset(horizontal = -occupiedSpace))
- occupiedSpace += textFieldPlaceable.width
+ // measure label
+ val labelConstraints = constraints
+ .offset(
+ vertical = -bottomPadding,
+ horizontal = -occupiedSpaceHorizontally
+ )
+ val labelPlaceable =
+ measurables.first { it.id == LabelId }.measure(labelConstraints)
+ val lastBaseline = labelPlaceable[LastBaseline].let {
+ if (it != AlignmentLine.Unspecified) it else labelPlaceable.height
+ }
+ val effectiveLabelBaseline = max(lastBaseline, baseLineOffset)
- val width = max(occupiedSpace, incomingConstraints.minWidth)
- val height = max(
- heightOrZero(
- listOf(
+ // measure input field
+ val textFieldConstraints = incomingConstraints
+ .copy(minHeight = 0)
+ .offset(
+ vertical = -bottomPadding - topPadding - effectiveLabelBaseline,
+ horizontal = -occupiedSpaceHorizontally
+ )
+ val textFieldPlaceable = measurables
+ .first { it.id == TextFieldId }
+ .measure(textFieldConstraints)
+
+ // measure placeholder
+ val placeholderConstraints = textFieldConstraints.copy(minWidth = 0)
+ val placeholderPlaceable = measurables
+ .find { it.id == PlaceholderId }
+ ?.measure(placeholderConstraints)
+
+ val width = calculateWidth(
+ leadingPlaceable,
+ trailingPlaceable,
+ textFieldPlaceable,
+ labelPlaceable,
+ placeholderPlaceable,
+ incomingConstraints
+ )
+ val height = calculateHeight(
+ textFieldPlaceable,
+ effectiveLabelBaseline,
+ leadingPlaceable,
+ trailingPlaceable,
+ placeholderPlaceable,
+ incomingConstraints,
+ density
+ )
+
+ layout(width, height) {
+ if (labelPlaceable.width != 0) {
+ val labelEndPosition =
+ (baseLineOffset - lastBaseline).coerceAtLeast(0)
+ place(
+ width,
+ height,
+ textFieldPlaceable,
+ labelPlaceable,
+ placeholderPlaceable,
leadingPlaceable,
trailingPlaceable,
- textFieldPlaceable
- ).maxByOrNull { heightOrZero(it) }
- ),
- incomingConstraints.minHeight
- )
- layout(width, height) {
- leadingPlaceable?.place(
- 0,
- Alignment.CenterVertically.align(height - leadingPlaceable.height)
- )
- textFieldPlaceable.place(
- leadingPlaceable?.width ?: 0,
- Alignment.CenterVertically.align(height - textFieldPlaceable.height)
- )
- trailingPlaceable?.place(
- width - trailingPlaceable.width,
- Alignment.CenterVertically.align(height - trailingPlaceable.height)
- )
+ labelEndPosition,
+ effectiveLabelBaseline + topPadding,
+ animationProgress
+ )
+ } else {
+ // text field should be centered vertically if there is no label
+ placeWithoutLabel(
+ width,
+ height,
+ textFieldPlaceable,
+ placeholderPlaceable,
+ leadingPlaceable,
+ trailingPlaceable
+ )
+ }
}
}
}
+private fun calculateWidth(
+ leadingPlaceable: Placeable?,
+ trailingPlaceable: Placeable?,
+ textFieldPlaceable: Placeable,
+ labelPlaceable: Placeable,
+ placeholderPlaceable: Placeable?,
+ constraints: Constraints
+): Int {
+ val middleSection = maxOf(
+ textFieldPlaceable.width,
+ labelPlaceable.width,
+ widthOrZero(placeholderPlaceable)
+ )
+ val wrappedWidth =
+ widthOrZero(leadingPlaceable) + middleSection + widthOrZero(
+ trailingPlaceable
+ )
+ return max(wrappedWidth, constraints.minWidth)
+}
+
+private fun calculateHeight(
+ textFieldPlaceable: Placeable,
+ labelBaseline: Int,
+ leadingPlaceable: Placeable?,
+ trailingPlaceable: Placeable?,
+ placeholderPlaceable: Placeable?,
+ constraints: Constraints,
+ density: Float
+): Int {
+ val bottomPadding = LastBaselineOffset.value * density
+ val topPadding = FilledTextFieldTopPadding.value * density
+ val inputFieldHeight = max(textFieldPlaceable.height, heightOrZero(placeholderPlaceable))
+ val middleSectionHeight = labelBaseline + topPadding + inputFieldHeight + bottomPadding
+ return maxOf(
+ middleSectionHeight.roundToInt(),
+ max(heightOrZero(leadingPlaceable), heightOrZero(trailingPlaceable)),
+ constraints.minHeight
+ )
+}
+
/**
* Places the provided text field, placeholder and label with respect to the baseline offsets in
* [TextField]
*/
-private fun Placeable.PlacementScope.placeLabelAndTextfield(
+private fun Placeable.PlacementScope.place(
width: Int,
height: Int,
textfieldPlaceable: Placeable,
labelPlaceable: Placeable,
placeholderPlaceable: Placeable?,
+ leadingPlaceable: Placeable?,
+ trailingPlaceable: Placeable?,
labelEndPosition: Int,
textPosition: Int,
animationProgress: Float
) {
+ leadingPlaceable?.place(
+ 0,
+ Alignment.CenterVertically.align(height - leadingPlaceable.height)
+ )
+ trailingPlaceable?.place(
+ width - trailingPlaceable.width,
+ Alignment.CenterVertically.align(height - trailingPlaceable.height)
+ )
val labelCenterPosition = Alignment.CenterStart.align(
IntSize(
width - labelPlaceable.width,
@@ -830,26 +862,37 @@
val labelDistance = labelCenterPosition.y - labelEndPosition
val labelPositionY =
labelCenterPosition.y - (labelDistance * animationProgress).roundToInt()
- labelPlaceable.place(0, labelPositionY)
+ labelPlaceable.place(widthOrZero(leadingPlaceable), labelPositionY)
- textfieldPlaceable.place(0, textPosition)
- placeholderPlaceable?.place(0, textPosition)
+ textfieldPlaceable.place(widthOrZero(leadingPlaceable), textPosition)
+ placeholderPlaceable?.place(widthOrZero(leadingPlaceable), textPosition)
}
/**
* Places the provided text field and placeholder center vertically in [TextField]
*/
-private fun Placeable.PlacementScope.placeTextfield(
+private fun Placeable.PlacementScope.placeWithoutLabel(
+ width: Int,
height: Int,
textPlaceable: Placeable,
- placeholderPlaceable: Placeable?
+ placeholderPlaceable: Placeable?,
+ leadingPlaceable: Placeable?,
+ trailingPlaceable: Placeable?
) {
- textPlaceable.place(
+ leadingPlaceable?.place(
0,
+ Alignment.CenterVertically.align(height - leadingPlaceable.height)
+ )
+ trailingPlaceable?.place(
+ width - trailingPlaceable.width,
+ Alignment.CenterVertically.align(height - trailingPlaceable.height)
+ )
+ textPlaceable.place(
+ widthOrZero(leadingPlaceable),
Alignment.CenterVertically.align(height - textPlaceable.height)
)
placeholderPlaceable?.place(
- 0,
+ widthOrZero(leadingPlaceable),
Alignment.CenterVertically.align(height - placeholderPlaceable.height)
)
}
@@ -871,6 +914,6 @@
}
private val FirstBaselineOffset = 20.dp
-private val LastBaselineOffset = 16.dp
-private val FilledTextFieldTopPadding = 3.dp
+private val LastBaselineOffset = 10.dp
+private val FilledTextFieldTopPadding = 4.dp
private const val ContainerAlpha = 0.12f
\ No newline at end of file