Android 使用 InlineTextContent 组合 TextField 和图像

问题描述 投票:0回答:1

我正在使用 Jetpack Compose 开发 Android 应用程序。

我想实现一个文本字段。当输入特定令牌时,此文本字段应显示一个小表情符号或图像而不是文本。

您可以在Slack应用程序中找到这样的案例。除了常规表情符号之外,Slack 应用程序还支持自定义表情符号。

我发现了 InlineTextContent 作为提示。

val inlineContentMap = mapOf(
    "ab_12" to emojiImage("https://c.tenor.com/Rd6ULrCRvlQAAAAd/tenor.gif")
)

var inputText by remember { mutableStateOf("") }
BasicTextField(
    value = inputText,
    onValueChange = {
        inputText = it
    },
    modifier = Modifier
        .fillMaxWidth()
        .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))
        .padding(8.dp),
    textStyle = TextStyle(fontSize = 16.sp).copy(color = Color.Transparent),
    visualTransformation = EmojiVisualTransformation,
    decorationBox = { innerTextField ->
        Box(
            modifier = Modifier,
            contentAlignment = Alignment.TopStart
        ) {
            val annotatedString = replaceTokens(inputText)
            Text(
                text = annotatedString,
                style = TextStyle(fontSize = 16.sp),
                inlineContent = inlineContentMap
            )
            innerTextField()
        }
    }
)

还有其他功能:

@Composable
private fun emojiImage(imgUrl: String) =
    InlineTextContent(Placeholder(16.sp, 16.sp, PlaceholderVerticalAlign.TextCenter)) {
        val context = LocalContext.current
        val imageLoader = ImageLoader.Builder(context)
            .components {
                add(ImageDecoderDecoder.Factory())
            }
            .build()
        Image(
            painter = rememberAsyncImagePainter(
                model = ImageRequest.Builder(context)
                    .data(data = imgUrl)
                    .build(),
                imageLoader = imageLoader
            ),
            contentDescription = null,
            modifier = Modifier.size(16.dp),
        )
    }

private fun replaceTokens(input: String): AnnotatedString {
    val annotatedString = buildAnnotatedString {
        val regex = Regex("\\{:(\\w+_\\d+):\\}")
        var currentIndex = 0
        regex.findAll(input).forEach { matchResult ->
            val token = matchResult.groupValues[1]
            val tokenIndex = matchResult.range.first
            val tokenLength = matchResult.value.length
            append(input.substring(currentIndex, tokenIndex))
            appendInlineContent(id = token)
            currentIndex = tokenIndex + tokenLength
        }
        append(input.substring(currentIndex, input.length))
    }
    return annotatedString
}

这是视觉转换:

object EmojiVisualTransformation: VisualTransformation {

    override fun filter(text: AnnotatedString): TransformedText {
        val originText = text.text

        val annotatedString = replaceTokens(originText)

        return TransformedText(annotatedString, EmojiOffsetMapping(originText, annotatedString.text))
    }
}

data class EmojiOffsetMapping(
    private val origin: String,
    private val transformed: String,
): OffsetMapping {

    private val tokenPositionRanges = mutableListOf<IntRange>()

    init {
        val regex = Regex("\\{:(\\w+_\\d+):\\}")
        regex.findAll(origin).forEach { matchResult ->
            val tokenPosition = matchResult.range
            tokenPositionRanges.add(tokenPosition)
        }
    }

    private fun getTransformedOffset(offset: Int): Int {
        if (offset == 0) {
            return 0
        }

        var newOffset = offset
        tokenPositionRanges.forEachIndexed { index, tokenRange ->
            if (tokenRange.first + 1 <= offset && offset <= tokenRange.last + 1) {
                return index + 1
            }

            newOffset = newOffset - tokenRange.count() + 1
        }

        return newOffset
    }

    override fun originalToTransformed(offset: Int): Int {
//        return getTransformedOffset(offset)
        return offset
    }

    override fun transformedToOriginal(offset: Int): Int {
        return offset
    }
}

此代码仅在非常简单的情况下有效,并且在大多数情况下经常崩溃。

虽然 InlineTextContent 可以轻松地在字符串之间显示图像,但问题在于光标。

就我以 {:d_1:}、{:ab_10:} 格式使用的标记而言,它们占用 7 到 9 个或更多字符,导致出现不可见的空格,即使它们看起来被单个图像替换。

这使得光标行为非常不稳定。

事实上,我不确定使用 InlineTextContent 和 VisualTransformation 是否是正确的方法。

如何有效地实现这个 TextField?”


还有一个问题:

在 Slack 应用程序中,将光标直接放在表情符号后面并按退格键可删除该表情符号。

但是,在当前的实现中,按退格键不会删除表情符号,而是删除令牌的最后一个字符,从而导致表情符号恢复并暴露令牌。

例如,不是删除整个“{:ab_12:}”标记,而是仅删除最后一个花括号“}”,留下“{:ab_12:”暴露。

按退格键时我们应该如何处理删除整个令牌?

android android-jetpack-compose
1个回答
0
投票

EmojiOffsetMapping
中,对于您的
originalToTransformed
方法,您应该计算转换文本中的光标位置,这意味着考虑到替换的图像。对于每个标记,无论原始标记有多少个字符,长度都会减少到 1(因为图像只占据一个位置)。

Cursor Mapping: Original ↔ Transformed
┌────────────────────┐        ┌────────────────────┐
│ Original Text      │        │ Transformed Text   │
│ {:ab_12:} hello... │   ↔    │ 📷 hello....      │
│          ↑         │        │   ↑                │
└────────────────────┘        └────────────────────┘
Cursor at token end          Cursor at image end
override fun originalToTransformed(offset: Int): Int {
    var transformedOffset = offset
    for (tokenRange in tokenPositionRanges) {
        if (offset > tokenRange.last) {
            // Cursor is beyond the current token, subtract the token's length
            // minus one because it is replaced by a single image.
            transformedOffset -= tokenRange.count() - 1
        } else if (offset > tokenRange.first) {
            // Cursor is within the current token, place it right after the image.
            return tokenRange.first + 1
        }
    }
    return transformedOffset
}

transformedToOriginal
中,您需要执行相反的操作,当光标在转换后的文本中移动时找到原始文本中的正确位置。

override fun transformedToOriginal(offset: Int): Int {
    var originalOffset = offset
    var addedLength = 0
    for (tokenRange in tokenPositionRanges) {
        val tokenLength = tokenRange.count()
        if (originalOffset <= tokenRange.first + addedLength) {
            break
        }
        // Add back the length of the tokens replaced by images.
        addedLength += tokenLength - 1
    }
    return originalOffset + addedLength
}
© www.soinside.com 2019 - 2024. All rights reserved.