在Jetpack Compose中指定文本的最小行数

26

由于各种原因,一个文本应该始终具有至少等于x行文本的高度,即使它少于x行文本也是如此。 TextBasicText Composables只有maxLines参数,没有minLines

我已经尝试了以下方法(其中x = 3):

Text(
    modifier = Modifier.sizeIn(minHeight = with(LocalDensity.current) {
       (42*3).sp.toDp()
    }),
    color = MaterialTheme.colors.onPrimary,
    text = "Sample", textAlign = TextAlign.Center,
    style = MaterialTheme.typography.h2 /* fontSize = 42 */,
    lineHeight = 42.sp
)

结果高度小于文本包含3行的情况

回到View World Android,我们可以简单地使用minLines=3,那么在Jetpack Compose中我们如何实现呢?


哦,所以你指定了 lineHeight,然后 "minLines" 被设置为修饰符的最小高度,其值为 lineHeight *(行数) - EpicPandaForce
https://developer.android.com/jetpack/compose/layouts/intrinsic-measurements - Sayok Majumder
6个回答

12

您的代码几乎正确,只需将lineHeight设置为fontSize*4/3

var lineHeight = MaterialTheme.typography.h2.fontSize*4/3

Text(
    modifier = Modifier.sizeIn(minHeight = with(LocalDensity.current) {
       (lineHeight*3).toDp()
    }),
    color = MaterialTheme.colors.onPrimary,
    text = "Sample", textAlign = TextAlign.Center,
    style = MaterialTheme.typography.h2,
    lineHeight = lineHeight
)

但是你可以使用onTextLayout回调函数,而不需要进行计算就能实现类似的功能:

fun main() = Window {
    var text by remember { mutableStateOf("Hello, World!") }
    var lines by remember { mutableStateOf(0) }

    MaterialTheme {
        Button(onClick = {
            text += "\nnew line"
        }) {
            Column {
                Text(text,
                    maxLines = 5,
                    style = MaterialTheme.typography.h2,
                    onTextLayout = { res -> lines = res.lineCount })
                for (i in lines..2) {
                    Text(" ", style = MaterialTheme.typography.h2)
                }
            }
        }
    }
}

4/3的值从哪里来? - M. Wojcik
@M.Wojcik ptpx 的转换 1pt = 4/3px - Spatz
和我一起工作,谢谢。 - undefined

7
从 M2 1.4.0-alpha02 和 M3 1.1.0-alpha02 开始,您可以在 Text 中使用 minLines 属性:
   Text(
       text = "MinLines = 3",
       modifier = Modifier.fillMaxWidth().background(Yellow),
       minLines = 3
   )

enter image description here

请注意,minLines 是以最小可见行数为单位的最小高度。要求 1 <= minLines <= maxLines
您可以在 M2 和 M3 中使用它。

2
FYI:此功能请求的Google跟踪器 https://issuetracker.google.com/issues/122476634 - RedSIght

7

虽然我们正在等待Google实现此功能,但您可以使用以下解决方法:

@Preview
@Composable
fun MinLinesPreview() {
    lateinit var textLayoutResult: TextLayoutResult

    val text = "Title\ntitle\nTITLE\nTitle"
//    val text = "title\ntitle\ntitle\ntitle"
//    val text = "title\ntitle"
//    val text = "title"

    Text(
        modifier = Modifier.fillMaxWidth(),
        text = text.addEmptyLines(3), // ensures string has at least N lines,
        textAlign = TextAlign.Center,
        maxLines = 4,
    )
}

fun String.addEmptyLines(lines: Int) = this + "\n".repeat(lines)

现在你的文本具有相同的高度,不管字符串内容如何:

1行文字高度 2行文字高度 4行文字高度 大写锁定状态下的4行文字高度

这种解决方案比在onTextLayout中基于行高计算Text的底部偏移要简单得多(剧透:第一行、中心行和最后一行的高度不同)。


1
好主意!然而,这种方法存在两个问题。使用省略号时,即使文本实际上可以适合4行,它也会添加“...”。此外,\n行的高度不等于常规行,导致Text元素的高度小于所有占用4行的常规字符的“常规”元素。 - ubuntudroid

3
如果您可以接受对文本进行一个额外的重新排版,则可以利用TextonTextLayout回调作为一种解决方法,直到Google官方支持最小行数为止。
val minLineCount = 4
var text by remember { mutableStateOf(description) }
Text(
    text = text,
    maxLines = minLineCount, // optional, if you want the Text to always be exactly 4 lines long
    overflow = TextOverflow.Ellipsis, // optional, if you want ellipsizing
    textAlign = TextAlign.Center,
    onTextLayout = { textLayoutResult ->
        // the following causes a recomposition if there isn't enough text to occupy the minimum number of lines!
        if ((textLayoutResult.lineCount) < minLineCount) {
            // don't forget the space after the line break, otherwise empty lines won't get full height!
            text = description + "\n ".repeat(minLineCount - textLayoutResult.lineCount) 
        }
    },
    modifier = Modifier.fillMaxWidth()
)

它还可以与省略号和任何类型的字体填充、行高样式等设置完美配合使用,以满足您的需求。

一个“虚假”的四行文本(例如,在末尾有两个空行)将具有与一个“真正”的四行文本相同的高度,该文本中的四行都被完全占用。在水平放置多个wrap_content高度的卡片时,文本(与maxLines结合使用)应确定卡片的高度,而所有卡片的高度都应相同(并且它应该在常规和高语言(如缅甸语)中均可用)。

请注意,这在Android Studio的预览中无法工作。我猜测是因为Studio不允许在预览中进行重新组合,以避免影响性能。


什么是exactLineCount? - Skizo-ozᴉʞS ツ
1
@Skizo-ozᴉʞS 抱歉,我已经修复了示例。它应该实际上说“minLineCount”。 - ubuntudroid

1
以下是我想出的解决方案,可以将高度设置为特定行数(您可以调整修改器使其成为minLines)。它受到了从compose SDK中找到的代码的启发。
// Inspiration: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt#L38
fun Modifier.minLinesHeight(
minLines: Int,
textStyle: TextStyle
) = composed {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current

val resolvedStyle = remember(textStyle, layoutDirection) {
    resolveDefaults(textStyle, layoutDirection)
}
val resourceLoader = LocalFontLoader.current

val heightOfTextLines = remember(
    density,
    textStyle,
    layoutDirection
) {
    val lines = (EmptyTextReplacement + "\n").repeat(minLines - 1)

    computeSizeForDefaultText(
        style = resolvedStyle,
        density = density,
        text = lines,
        maxLines = minLines,
        resourceLoader
    ).height
}

val heightInDp: Dp = with(density) { heightOfTextLines.toDp() }
val heightToSet = heightInDp + OutlinedTextBoxDecoration

Modifier.height(heightToSet)
}

// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt#L61
fun computeSizeForDefaultText(
style: TextStyle,
density: Density,
text: String = EmptyTextReplacement,
maxLines: Int = 1,
resourceLoader: Font.ResourceLoader
): IntSize {
val paragraph = Paragraph(
    paragraphIntrinsics = ParagraphIntrinsics(
        text = text,
        style = style,
        density = density,
        resourceLoader = resourceLoader
    ),

    maxLines = maxLines,
    ellipsis = false,
    width = Float.POSITIVE_INFINITY
)

return IntSize(paragraph.minIntrinsicWidth.ceilToIntPx(), paragraph.height.ceilToIntPx())
}

// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt#L47
internal const val DefaultWidthCharCount = 10
internal val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount)

// Needed because paragraph only calculates the height to display the text and not the entire height
// to display the decoration of the TextField Widget
internal val OutlinedTextBoxDecoration = 40.dp

// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt#L296
internal fun Float.ceilToIntPx(): Int = ceil(this).roundToInt()

关于这个实现和其他选项的更多讨论可以在这里找到:

https://kotlinlang.slack.com/archives/CJLTWPH7S/p1621789835172600


谢谢!在TextField上运行得很好,但是在Text上不太理想。我将不得不实现其他解决方案来处理Text。 - intips
1
@intips - 谷歌已经更新了Compose存储库以提供类似的支持 - 也许这对您有用 https://github.com/androidx/androidx/blob/7c52194062c6035fe0685da48d239116d41c859a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt#L46 - dazza5000
太好了,非常感谢!现在这个适用于纯文本。但是我的问题已经解决了。我认为这应该是被采纳的答案。 - intips

0

创建自定义文本

在 @Preview 中无法工作,但在运行时可以。


@Composable
fun MinLineText(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    minLines: Int = 0,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    var mText by remember { mutableStateOf(text) }

    Text(
        mText,
        modifier,
        color,
        fontSize,
        fontStyle,
        fontWeight,
        fontFamily,
        letterSpacing,
        textDecoration,
        textAlign,
        lineHeight,
        overflow,
        softWrap,
        maxLines,
        {
            if (it.lineCount < minLines) {
                mText = text + "\n".repeat(minLines - it.lineCount)
            }
            onTextLayout(it)
        },
        style,
    )

}

使用方法

MinLineText(
    text = "a sample text",
    minLines = 2,
)


对我来说不起作用。 - Skizo-ozᴉʞS ツ
@Skizo-ozᴉʞS,有什么问题吗?你在运行时测试过了吗? - Atras VidA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接