Change MaxLinesHeightModifier to also handle minLines
Rename MaxLinesHeightModifier to HeightInLinesModifier and add a minLines parameter.
Rename MaxLinesHeightModifierTest to HeightInLinesModifierTest and add tests for the minLines functionality.
Bug: 122476634
Test: HeightInLinesModifierTest.kt
Test: ./gradlew :compose:ui:ui-text:cAT
Relnote: N/A
Change-Id: I7433ede0096f950a89c494084e194157a4bbaaab
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
new file mode 100644
index 0000000..7c40323
--- /dev/null
+++ b/compose/foundation/foundation/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
+ Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index df13635..4fc8b33 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -909,6 +909,9 @@
public final class CoreTextKt {
}
+ public final class HeightInLinesModifierKt {
+ }
+
@androidx.compose.runtime.Immutable public final class InlineTextContent {
ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -982,9 +985,6 @@
public final class LongPressTextDragObserverKt {
}
- public final class MaxLinesHeightModifierKt {
- }
-
public final class StringHelpersKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 1bcf19f..b514e7d 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -1153,6 +1153,9 @@
public final class CoreTextKt {
}
+ public final class HeightInLinesModifierKt {
+ }
+
@androidx.compose.runtime.Immutable public final class InlineTextContent {
ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -1229,9 +1232,6 @@
public final class LongPressTextDragObserverKt {
}
- public final class MaxLinesHeightModifierKt {
- }
-
public final class StringHelpersKt {
}
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
new file mode 100644
index 0000000..7c40323
--- /dev/null
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
+ Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index df13635..4fc8b33 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -909,6 +909,9 @@
public final class CoreTextKt {
}
+ public final class HeightInLinesModifierKt {
+ }
+
@androidx.compose.runtime.Immutable public final class InlineTextContent {
ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -982,9 +985,6 @@
public final class LongPressTextDragObserverKt {
}
- public final class MaxLinesHeightModifierKt {
- }
-
public final class StringHelpersKt {
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
similarity index 69%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt
rename to compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
index 60b4ec6..c278e7b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
@@ -22,7 +22,7 @@
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.text.CoreTextField
import androidx.compose.foundation.text.TEST_FONT
-import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.heightInLines
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
@@ -63,7 +63,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-class MaxLinesHeightModifierTest {
+class HeightInLinesModifierTest {
private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
@@ -87,40 +87,7 @@
}
@Test
- fun maxLinesHeight_shortInputText() {
- val (textLayoutResult, height) = setTextFieldWithMaxLines(TextFieldValue("abc"), 5)
-
- rule.runOnIdle {
- assertThat(textLayoutResult).isNotNull()
- assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
- assertThat(textLayoutResult.size.height).isEqualTo(height)
- }
- }
-
- @Test
- fun maxLinesHeight_notApplied_infiniteMaxLines() {
- val (textLayoutResult, height) =
- setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
-
- rule.runOnIdle {
- assertThat(textLayoutResult).isNotNull()
- assertThat(textLayoutResult!!.size.height).isEqualTo(height)
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun maxLinesHeight_invalidValue() {
- rule.setContent {
- CoreTextField(
- value = TextFieldValue(),
- onValueChange = {},
- modifier = Modifier.maxLinesHeight(0, TextStyle.Default)
- )
- }
- }
-
- @Test
- fun maxLinesHeight_longInputText() {
+ fun minLines_shortInputText() {
var subjectLayout: TextLayoutResult? = null
var subjectHeight: Int? = null
var twoLineHeight: Int? = null
@@ -136,8 +103,8 @@
onTextLayoutResult = {
subjectLayout = it
},
- TextFieldValue(longText),
- 2
+ textFieldValue = TextFieldValue("abc"),
+ minLines = 2
)
HeightObservingText(
onGlobalHeightPositioned = {
@@ -145,8 +112,125 @@
twoLinePositionedLatch.countDown()
},
onTextLayoutResult = {},
- TextFieldValue("1\n2"),
- 2
+ textFieldValue = TextFieldValue("1\n2"),
+ minLines = 2
+ )
+ }
+ assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+ assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ rule.runOnIdle {
+ assertThat(subjectLayout).isNotNull()
+ assertThat(subjectLayout!!.lineCount).isEqualTo(1)
+ assertThat(subjectHeight!!).isEqualTo(twoLineHeight)
+ }
+ }
+
+ @Test
+ fun maxLines_shortInputText() {
+ val (textLayoutResult, height) = setTextFieldWithMaxLines(
+ TextFieldValue("abc"),
+ maxLines = 5
+ )
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+ assertThat(textLayoutResult.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test
+ fun maxLines_notApplied_infiniteMaxLines() {
+ val (textLayoutResult, height) =
+ setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun minLines_invalidValue() {
+ rule.setContent {
+ CoreTextField(
+ value = TextFieldValue(),
+ onValueChange = {},
+ modifier = Modifier.heightInLines(textStyle = TextStyle.Default, minLines = 0)
+ )
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun maxLines_invalidValue() {
+ rule.setContent {
+ CoreTextField(
+ value = TextFieldValue(),
+ onValueChange = {},
+ modifier = Modifier.heightInLines(textStyle = TextStyle.Default, maxLines = 0)
+ )
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun minLines_greaterThan_maxLines_invalidValue() {
+ rule.setContent {
+ CoreTextField(
+ value = TextFieldValue(),
+ onValueChange = {},
+ modifier = Modifier.heightInLines(
+ textStyle = TextStyle.Default,
+ minLines = 2,
+ maxLines = 1
+ )
+ )
+ }
+ }
+
+ @Test
+ fun minLines_longInputText() {
+ val (textLayoutResult, height) = setTextFieldWithMaxLines(
+ TextFieldValue(longText),
+ minLines = 2
+ )
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ // should be in the 20s, but use this to create invariant for the next assertion
+ assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+ assertThat(textLayoutResult.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test
+ fun maxLines_longInputText() {
+ var subjectLayout: TextLayoutResult? = null
+ var subjectHeight: Int? = null
+ var twoLineHeight: Int? = null
+ val positionedLatch = CountDownLatch(1)
+ val twoLinePositionedLatch = CountDownLatch(1)
+
+ rule.setContent {
+ HeightObservingText(
+ onGlobalHeightPositioned = {
+ subjectHeight = it
+ positionedLatch.countDown()
+ },
+ onTextLayoutResult = {
+ subjectLayout = it
+ },
+ textFieldValue = TextFieldValue(longText),
+ maxLines = 2
+ )
+ HeightObservingText(
+ onGlobalHeightPositioned = {
+ twoLineHeight = it
+ twoLinePositionedLatch.countDown()
+ },
+ onTextLayoutResult = {},
+ textFieldValue = TextFieldValue("1\n2"),
+ maxLines = 2
)
}
assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
@@ -216,9 +300,14 @@
@Test
fun testInspectableValue() {
- val modifier = Modifier.maxLinesHeight(10, TextStyle.Default) as InspectableValue
- assertThat(modifier.nameFallback).isEqualTo("maxLinesHeight")
+ val modifier = Modifier.heightInLines(
+ textStyle = TextStyle.Default,
+ minLines = 5,
+ maxLines = 10
+ ) as InspectableValue
+ assertThat(modifier.nameFallback).isEqualTo("heightInLines")
assertThat(modifier.inspectableElements.asIterable()).containsExactly(
+ ValueElement("minLines", 5),
ValueElement("maxLines", 10),
ValueElement("textStyle", TextStyle.Default)
)
@@ -226,7 +315,8 @@
private fun setTextFieldWithMaxLines(
textFieldValue: TextFieldValue,
- maxLines: Int
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE
): Pair<TextLayoutResult?, Int?> {
var textLayoutResult: TextLayoutResult? = null
var height: Int? = null
@@ -241,8 +331,9 @@
onTextLayoutResult = {
textLayoutResult = it
},
- textFieldValue,
- maxLines
+ textFieldValue = textFieldValue,
+ minLines = minLines,
+ maxLines = maxLines
)
}
assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
@@ -255,7 +346,8 @@
onGlobalHeightPositioned: (Int) -> Unit,
onTextLayoutResult: (TextLayoutResult) -> Unit,
textFieldValue: TextFieldValue,
- maxLines: Int,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
textStyle: TextStyle = TextStyle.Default
) {
Box(
@@ -269,7 +361,11 @@
textStyle = textStyle,
modifier = Modifier
.requiredWidth(100.dp)
- .maxLinesHeight(maxLines, textStyle),
+ .heightInLines(
+ textStyle = textStyle,
+ minLines = minLines,
+ maxLines = maxLines
+ ),
onTextLayout = onTextLayoutResult
)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index 50b289d..99b70f41 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -33,7 +33,7 @@
import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.TextFieldScrollerPosition
import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.heightInLines
import androidx.compose.foundation.text.selection.isSelectionHandle
import androidx.compose.foundation.text.textFieldScroll
import androidx.compose.foundation.text.textFieldScrollable
@@ -706,7 +706,7 @@
softWrap = isVertical,
modifier = modifier
.testTag(TextfieldTag)
- .maxLinesHeight(resolvedMaxLines, TextStyle.Default)
+ .heightInLines(textStyle = TextStyle.Default, maxLines = resolvedMaxLines)
.textFieldScrollable(scrollerPosition)
.textFieldScroll(
remember { scrollerPosition },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index ac6cbcf..cc57d5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -548,7 +548,10 @@
// min height is set for maxLines == 1 in order to prevent text cuts for single line
// TextFields
.heightIn(min = state.minHeightForSingleLineField)
- .maxLinesHeight(maxLines, textStyle)
+ .heightInLines(
+ textStyle = textStyle,
+ maxLines = maxLines
+ )
.textFieldScroll(
scrollerPosition,
value,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
similarity index 72%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
index 1ebda22..2092f2d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
@@ -30,26 +30,39 @@
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.unit.Dp
/**
- * Constraint the height of the text field so that it vertically occupies no more than [maxLines]
- * number of lines.
+ * The default minimum height in terms of minimum number of visible lines.
*/
-internal fun Modifier.maxLinesHeight(
- /*@IntRange(from = 1)*/
- maxLines: Int,
- textStyle: TextStyle
+internal const val DefaultMinLines: Int = 1
+
+/**
+ * Constraint the height of the text field so that it vertically occupies at least [minLines]
+ * number of lines and at most [maxLines] number of lines.
+ */
+internal fun Modifier.heightInLines(
+ textStyle: TextStyle,
+ minLines: Int = DefaultMinLines,
+ maxLines: Int = Int.MAX_VALUE
) = composed(
inspectorInfo = debugInspectorInfo {
- name = "maxLinesHeight"
+ name = "heightInLines"
+ properties["minLines"] = minLines
properties["maxLines"] = maxLines
properties["textStyle"] = textStyle
}
) {
+ require(minLines > 0) {
+ "minLines must be greater than 0"
+ }
require(maxLines > 0) {
"maxLines must be greater than 0"
}
- if (maxLines == Int.MAX_VALUE) return@composed Modifier
+ require(minLines <= maxLines) {
+ "minLines $minLines must be lower than or equal to maxLines $maxLines"
+ }
+ if (minLines == DefaultMinLines && maxLines == Int.MAX_VALUE) return@composed Modifier
val density = LocalDensity.current
val fontFamilyResolver = LocalFontFamilyResolver.current
@@ -61,7 +74,7 @@
resolveDefaults(textStyle, layoutDirection)
}
val typeface by remember(fontFamilyResolver, resolvedStyle) {
- fontFamilyResolver.resolve(
+ fontFamilyResolver.resolve(
resolvedStyle.fontFamily,
resolvedStyle.fontWeight ?: FontWeight.Normal,
resolvedStyle.fontStyle ?: FontStyle.Normal,
@@ -102,9 +115,15 @@
).height
}
val lineHeight = firstTwoLinesHeight - firstLineHeight
- val precomputedMaxLinesHeight = firstLineHeight + lineHeight * (maxLines - 1)
+ val precomputedMinLinesHeight =
+ if (minLines == DefaultMinLines) null else firstLineHeight + lineHeight * (minLines - 1)
+ val precomputedMaxLinesHeight =
+ if (maxLines == Int.MAX_VALUE) null else firstLineHeight + lineHeight * (maxLines - 1)
- Modifier.heightIn(
- max = with(density) { precomputedMaxLinesHeight.toDp() }
- )
+ with(density) {
+ Modifier.heightIn(
+ min = precomputedMinLinesHeight?.toDp() ?: Dp.Unspecified,
+ max = precomputedMaxLinesHeight?.toDp() ?: Dp.Unspecified
+ )
+ }
}
\ No newline at end of file