我正在使用 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:”暴露。
按退格键时我们应该如何处理删除整个令牌?
在
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
}