kotlin Jetpack合成中的可扩展文本

wlzqhblo  于 2023-01-05  发布在  Kotlin
关注(0)|答案(7)|浏览(175)

所以我用了一个Text()可组合函数,如下所示:

Text(
    text = "this is some sample text that is long and so it is 
            ellipsized",
    maxLines = 1,
    overflow = TextOverflow.Ellipsis
)

它会正确地省略掉文本:

问题是我想在省略号的末尾添加一个See More标记,提示用户展开可见文本框,我该如何添加它呢?

x8diyxa7

x8diyxa71#

要解决这个问题,需要使用onTextLayout来获得TextLayoutResult:它包含关于绘制文本的状态的所有信息。
使它适用于多行是一项棘手的任务。要做到这一点,你需要计算省略号文本和“...查看更多”文本的大小,然后,当你有两个值时,你需要计算多少文本需要删除,以便“...查看更多”完全适合行尾:

@Composable
fun ExpandableText(
    text: String,
    modifier: Modifier = Modifier,
    minimizedMaxLines: Int = 1,
) {
    var cutText by remember(text) { mutableStateOf<String?>(null) }
    var expanded by remember { mutableStateOf(false) }
    val textLayoutResultState = remember { mutableStateOf<TextLayoutResult?>(null) }
    val seeMoreSizeState = remember { mutableStateOf<IntSize?>(null) }
    val seeMoreOffsetState = remember { mutableStateOf<Offset?>(null) }

    // getting raw values for smart cast
    val textLayoutResult = textLayoutResultState.value
    val seeMoreSize = seeMoreSizeState.value
    val seeMoreOffset = seeMoreOffsetState.value

    LaunchedEffect(text, expanded, textLayoutResult, seeMoreSize) {
        val lastLineIndex = minimizedMaxLines - 1
        if (!expanded && textLayoutResult != null && seeMoreSize != null
            && lastLineIndex + 1 == textLayoutResult.lineCount
            && textLayoutResult.isLineEllipsized(lastLineIndex)
        ) {
            var lastCharIndex = textLayoutResult.getLineEnd(lastLineIndex, visibleEnd = true) + 1
            var charRect: Rect
            do {
                lastCharIndex -= 1
                charRect = textLayoutResult.getCursorRect(lastCharIndex)
            } while (
                charRect.left > textLayoutResult.size.width - seeMoreSize.width
            )
            seeMoreOffsetState.value = Offset(charRect.left, charRect.bottom - seeMoreSize.height)
            cutText = text.substring(startIndex = 0, endIndex = lastCharIndex)
        }
    }
    
    Box(modifier) {
        Text(
            text = cutText ?: text,
            maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
            overflow = TextOverflow.Ellipsis,
            onTextLayout = { textLayoutResultState.value = it },
        )
        if (!expanded) {
            val density = LocalDensity.current
            Text(
                "... See more",
                onTextLayout = { seeMoreSizeState.value = it.size },
                modifier = Modifier
                    .then(
                        if (seeMoreOffset != null)
                            Modifier.offset(
                                x = with(density) { seeMoreOffset.x.toDp() },
                                y = with(density) { seeMoreOffset.y.toDp() },
                            )
                        else
                            Modifier
                    )
                    .clickable {
                        expanded = true
                        cutText = null
                    }
                    .alpha(if (seeMoreOffset != null) 1f else 0f)
            )
        }
    }
}

ncgqoxb0

ncgqoxb02#

我的简单实现,希望对大家有用:

const val DEFAULT_MINIMUM_TEXT_LINE = 3

@Composable
fun ExpandableText(
    modifier: Modifier = Modifier,
    textModifier: Modifier = Modifier,
    style: TextStyle = LocalTextStyle.current,
    fontStyle: FontStyle? = null,
    text: String,
    collapsedMaxLine: Int = DEFAULT_MINIMUM_TEXT_LINE,
    showMoreText: String = "... Show More",
    showMoreStyle: SpanStyle = SpanStyle(fontWeight = FontWeight.W500),
    showLessText: String = " Show Less",
    showLessStyle: SpanStyle = showMoreStyle,
    textAlign: TextAlign? = null
) {
    var isExpanded by remember { mutableStateOf(false) }
    var clickable by remember { mutableStateOf(false) }
    var lastCharIndex by remember { mutableStateOf(0) }
    Box(modifier = Modifier
        .clickable(clickable) {
            isExpanded = !isExpanded
        }
        .then(modifier)
    ) {
        Text(
            modifier = textModifier
                .fillMaxWidth()
                .animateContentSize(),
            text = buildAnnotatedString {
                if (clickable) {
                    if (isExpanded) {
                        append(text)
                        withStyle(style = showLessStyle) { append(showLessText) }
                    } else {
                        val adjustText = text.substring(startIndex = 0, endIndex = lastCharIndex)
                            .dropLast(showMoreText.length)
                            .dropLastWhile { Character.isWhitespace(it) || it == '.' }
                        append(adjustText)
                        withStyle(style = showMoreStyle) { append(showMoreText) }
                    }
                } else {
                    append(text)
                }
            },
            maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLine,
            fontStyle = fontStyle,
            onTextLayout = { textLayoutResult ->
                if (!isExpanded && textLayoutResult.hasVisualOverflow) {
                    clickable = true
                    lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLine - 1)
                }
            },
            style = style,
            textAlign = textAlign
        )
    }

}

a7qyws3x

a7qyws3x3#

@Composable
fun ExpandedText(
    text: String,
    expandedText: String,
    expandedTextButton: String,
    shrinkTextButton: String,
    modifier: Modifier = Modifier,
    softWrap: Boolean = true,
    textStyle: TextStyle = LocalTextStyle.current,
    expandedTextStyle: TextStyle = LocalTextStyle.current,
    expandedTextButtonStyle: TextStyle = LocalTextStyle.current,
    shrinkTextButtonStyle: TextStyle = LocalTextStyle.current,
) {
    
    var isExpanded by remember { mutableStateOf(false) }
    
    val textHandler = "${if (isExpanded) expandedText else text} ${if (isExpanded) shrinkTextButton else expandedTextButton}"
    
    val annotatedString = buildAnnotatedString {
        withStyle(
            if (isExpanded) expandedTextStyle.toSpanStyle() else textStyle.toSpanStyle()
        ) {
            append(if (isExpanded) expandedText else text)
        }
        
        append("  ")
        
        withStyle(
            if (isExpanded) shrinkTextButtonStyle.toSpanStyle() else expandedTextButtonStyle.toSpanStyle()
        ) {
            append(if (isExpanded) shrinkTextButton else expandedTextButton)
        }
        
        addStringAnnotation(
            tag = "expand_shrink_text_button",
            annotation = if (isExpanded) shrinkTextButton else expandedTextButton,
            start = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton),
            end = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton) + if (isExpanded) expandedTextButton.length else shrinkTextButton.length
        )
    }
    
    ClickableText(
        text = annotatedString,
        softWrap = softWrap,
        modifier = modifier,
        onClick = {
            annotatedString
                .getStringAnnotations(
                    "expand_shrink_text_button",
                    it,
                    it
                )
                .firstOrNull()?.let { stringAnnotation ->
                    isExpanded = stringAnnotation.item == expandedTextButton
                }
        }
    )
}

用法

ExpandedText(
            text = food.content,
            expandedText = food.contentFull,
            expandedTextButton = " more",
            shrinkTextButton = " less",
            textStyle = typographySkModernist().body1.copy(
                color = black.copy(alpha = 0.8f)
            ),
            expandedTextStyle = typographySkModernist().body1.copy(
                color = black.copy(alpha = 0.8f)
            ),
            expandedTextButtonStyle = typographySkModernist().body1.copy(
                color = orange,
            ),
            shrinkTextButtonStyle = typographySkModernist().body1.copy(
                color = orange,
            ),
            modifier = Modifier
                .padding(top = 32.dp, start = 24.dp, end = 16.dp)
        )

xkrw2x1b

xkrw2x1b4#

一个简单的实现:

@Composable
fun ExpandableText(
    modifier: Modifier = Modifier,
    text: String,
    minimizedMaxLines: Int,
    style: TextStyle
) {
    var expanded by remember { mutableStateOf(false) }
    var hasVisualOverflow by remember { mutableStateOf(false) }
    Box(modifier = modifier) {
        Text(
            text = text,
            maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
            onTextLayout = { hasVisualOverflow = it.hasVisualOverflow },
            style = style
        )
        if (hasVisualOverflow) {
            Row(
                modifier = Modifier.align(Alignment.BottomEnd),
                verticalAlignment = Alignment.Bottom
            ) {
                val lineHeightDp: Dp = with(LocalDensity.current) { style.lineHeight.toDp() }
                Spacer(
                    modifier = Modifier
                        .width(48.dp)
                        .height(lineHeightDp)
                        .background(
                            brush = Brush.horizontalGradient(
                                colors = listOf(Color.Transparent, Color.White)
                            )
                        )
                )
                Text(
                    modifier = Modifier
                        .background(Color.White)
                        .padding(start = 4.dp)
                        .clickable(
                            indication = null,
                            interactionSource = remember { MutableInteractionSource() },
                            onClick = { expanded = !expanded }
                        ),
                    text = "Show More",
                    color = MaterialTheme.colors.primary,
                    style = style
                )
            }
        }
    }
}
6tr1vspr

6tr1vspr5#

我发现那些贴出来的解决方案有点矫枉过正。下面是一个简单的解决方案:

var showMore by remember { mutableStateOf(false) }
val text =
    "Space Exploration Technologies Corp. (doing business as SpaceX) is an American aerospace manufacturer, space transportation services and communications corporation headquartered in Hawthorne, California. SpaceX was founded in 2002 by Elon Musk with the goal of reducing space transportation costs to enable the colonization of Mars. SpaceX manufactures the Falcon 9 and Falcon Heavy launch vehicles, several rocket engines, Cargo Dragon, crew spacecraft and Starlink communications satellites."

Column(modifier = Modifier.padding(20.dp)) {
    Column(modifier = Modifier
        .animateContentSize(animationSpec = tween(100))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) { showMore = !showMore }) {

        if (showMore) {
            Text(text = text)
        } else {
            Text(text = text, maxLines = 3, overflow = TextOverflow.Ellipsis)
        }
    }
}
jw5wzhpr

jw5wzhpr6#

我想要一个更灵活的

package {packageName}.core.presentation.components

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import Constants.MINIMIZED_MAX_LINES

/**
 * @param modifier use this to add padding and such
 * @param longText is the Text that is to long and need to be displayed that has more than [minimizedMaxLines]
 * @param minimizedMaxLines (optional) the minimum amount of text lines to be visible in non-expanded state
 * @param textAlign (optional) defaults to [TextAlign.Start] unless overridden, try [TextAlign.Justify]
 * @param expandHint (optional) this text is appended to the [longText] before expanding and become clickable
 * @param shrinkHint (optional) this text is appended to the [longText] after expanding and become clickable
 * @param clickColor (optional) denotes the color of the clickable [expandHint] & [shrinkHint] strings
 * */
@Composable
fun AppExpandingText(
    modifier: Modifier = Modifier,
    longText: String,
    minimizedMaxLines: Int = 3,
    textAlign: TextAlign = TextAlign.Start,
    expandHint: String = "… Show More",
    shrinkHint: String = "… Show Less",
    clickColor: Color = Color.Unspecified
) {
    var isExpanded by remember { mutableStateOf(value = false) }
    var textLayoutResultState by remember { mutableStateOf<TextLayoutResult?>(value = null) }
    var adjustedText by remember { mutableStateOf(value = longText) }
    val overflow = textLayoutResultState?.hasVisualOverflow ?: false
    val showOverflow = remember { mutableStateOf(value = false) }
    val showMore = " $expandHint"
    val showLess = " $shrinkHint"
    
    LaunchedEffect(textLayoutResultState) {
        if (textLayoutResultState == null) return@LaunchedEffect
        if (!isExpanded && overflow) {
            showOverflow.value = true
            val lastCharIndex = textLayoutResultState!!.getLineEnd(lineIndex = minimizedMaxLines - 1)
            adjustedText = longText
                .substring(startIndex = 0, endIndex = lastCharIndex)
                .dropLast(showMore.length)
                .dropLastWhile { it == ' ' || it == '.' }
        }
    }
    val annotatedText = buildAnnotatedString {
        if (isExpanded) {
            append(longText)
            withStyle(
                style = SpanStyle(
                    color = MaterialTheme.colors.onSurface,
                    fontSize = 14.sp
                )
            ) {
                pushStringAnnotation(tag = "showLess", annotation = "showLess")
                append(showLess)
                addStyle(
                    style = SpanStyle(
                        color = clickColor,
                        fontSize = 14.sp
                    ),
                    start = longText.length,
                    end = longText.length + showMore.length
                )
                pop()
            }
        } else {
            append(adjustedText)
            withStyle(
                style = SpanStyle(
                    color = MaterialTheme.colors.onSurface,
                    fontSize = 14.sp
                )
            ) {
                if (showOverflow.value) {
                    pushStringAnnotation(tag = "showMore", annotation = "showMore")
                    append(showMore)
                    addStyle(
                        style = SpanStyle(
                            color = clickColor,
                            fontSize = 14.sp
                        ),
                        start = adjustedText.length,
                        end = adjustedText.length + showMore.length
                    )
                    pop()
                }
            }
        }
        
    }
    Box(modifier = modifier) {
        ClickableText(
            text = annotatedText,
            style = (MaterialTheme.typography.body1.copy(textAlign = textAlign)),
            maxLines = if (isExpanded) Int.MAX_VALUE else MINIMIZED_MAX_LINES,
            onTextLayout = { textLayoutResultState = it },
            onClick = { offset ->
                annotatedText.getStringAnnotations(
                    tag = "showLess",
                    start = offset,
                    end = offset + showLess.length
                ).firstOrNull()?.let {
                    isExpanded = !isExpanded
                }
                annotatedText.getStringAnnotations(
                    tag = "showMore",
                    start = offset,
                    end = offset + showMore.length
                ).firstOrNull()?.let {
                    isExpanded = !isExpanded
                }
            }
        )
    }
}
    • 样品:**

第一节第一节第一节第一节第一次

yyyllmsg

yyyllmsg7#

我有一个here的实现,就像其他人说的,我们应该使用onTextLayout来获取必要的度量,比如文本宽度等。在我的示例中,我尝试通过记住必要的值来最小化重新组合
重新组合和跳过计数:https://user-images.githubusercontent.com/23420470/210508144-3f8de75a-bc5f-4410-b0dc-ba5c4fb399a7.mov

相关问题