稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Jetpack Compose中的富文本输入

本文译自「Rich Content in Text Input in Jetpack Compose」,原文链接https://medium.com/proandroiddev/rich-content-in-text-input-in-jetpack-compose-0f4c1e8e830f,由Oğuzhan Aslan发布于2026年3月23日。

引言

在当今的移动环境中,文本输入不再仅仅局限于文本。用户已经习惯了丰富且富有表现力的沟通方式,而不仅仅局限于简单的字符。无论是发送有趣的 GIF、分享贴纸,还是从其他应用拖放图片,富文本支持都已成为用户的默认期望。

大多数流行的即时通讯和社交媒体应用都能无缝支持这些功能。然而,对于 Android 开发者来说,支持这种“默认”行为需要特定的实现步骤。标准的文本字段无法自动处理从 Gboard 发送的 GIF 或从剪贴板粘贴的图片。

“默认”体验(以及它为何失败)

如果你只是将一个 TextField 拖放到 Compose UI 中,它就能完美地处理文本。但是,如果你尝试从键盘插入 GIF 或粘贴图片,却发现……没有任何反应。运气好的话,键盘可能会提交一个 URI 字符串;但更常见的情况是,输入会被直接忽略,因为文本字段只接受文本。

为了解决这个问题,Jetpack Compose 引入了一个强大的修饰符:contentReceiver

解决方案:contentReceiver

contentReceiver 修饰符是一个统一的 API,旨在处理来自多个来源的富文本内容:

  1. 软键盘:来自 Gboard 等应用的 GIF 和贴纸。

  2. 剪贴板:粘贴的图片或富文本。

  3. 拖放:从其他应用(在分屏模式下)或同一应用内拖拽的内容。

contentReceiver 为我们提供了一个单一的入口点来处理所有 TransferableContent,而无需为键盘管理 OnCommitContentListener,也无需为拖放监听器单独管理。

逐步实现

让我们构建一个可以接收和显示图片的文本输入区域。

步骤 1:ViewModel 逻辑

首先,我们需要一种方法来处理传入的内容。我们将创建一个 RichContentViewModel 来处理 TransferableContent。核心逻辑涉及使用 .consume 函数,该函数允许我们提取可以处理的内容部分(例如图片 URI),并返回其余部分(如果已处理完所有内容,则返回 null)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class RichContentViewModel : ViewModel() {
    // State to hold our received images  
    var selectedImages by mutableStateOf<List<Uri>>(emptyList())

    @OptIn(ExperimentalFoundationApi::class)
    fun handleContent(
        transferableContent: TransferableContent
    ): TransferableContent? {
        val newUris = mutableListOf<Uri>()

        // consume returns the parts of the content we didn't use.  
        // inside the lambda, we return true if we consumed the item.  
        val remaining = transferableContent.consume { item ->
            // If the item has a URI, we take it  
            if (item.uri != null) {
                newUris += item.uri
                true // Consumed  
            } else {
                false // Not consumed, leave it for other handlers  
            }
        }

        // Update our state with the new images  
        selectedImages = newUris

        return remaining
    }
}

步骤 2:UI 实现

现在,让我们将其应用到 UI 中。contentReceiver 的一个关键优势是灵活性。我们不必将其直接应用于 TextField。通过将其应用于父容器(例如 Column),我们可以将整个区域转换为拖放操作的“放置区”,从而改善用户体验。

以下是我们的 RichContentTextField 可组合组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RichContentTextField(
    richContentViewModel: RichContentViewModel = viewModel()
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .border(1.dp, color = Color.Black)
            // Attach the content receiver here!  
            // This delegates the handling logic to our ViewModel  
            .contentReceiver(richContentViewModel::handleContent)
    ) {
        // 1. Display received images  
        LazyRow(
            modifier = Modifier.fillMaxWidth()
        ) {
            items(richContentViewModel.selectedImages) { imageUri ->
                AsyncImage(
                    model = imageUri,
                    contentDescription = null,
                    modifier = Modifier
                        .size(64.dp)
                        .padding(end = 4.dp)
                        .clip(RoundedCornerShape(8.dp)),
                    contentScale = ContentScale.Crop
                )
            }
        }
        // 2. The actual text input  
        val count = rememberTextFieldState()

        // Optional: Update text state based on images (just for demo purposes)  
        LaunchedEffect(richContentViewModel.selectedImages) {
            if (richContentViewModel.selectedImages.isNotEmpty()) {
                 count.setTextAndPlaceCursorAtEnd(richContentViewModel.selectedImages.size.toString())
            }
        }
        TextField(
            state = count,
            placeholder = { Text("Rich Content Field") },
        )
    }
}

代码要点:

  1. **contentReceiver** 在父级上:我们将修饰符放在了 Column 上。这意味着如果用户将图像拖到此框中的任何位置,它都会被捕获。

  2. **AsyncImage**:我们使用 Coil 的 AsyncImage 来渲染提取的 URI。

  3. 关注点分离:UI 只负责显示状态(selectedImages),而 ViewModel 则处理 TransferableContent 的复杂性。

结果

只需几行代码,我们就将一个基本的文本输入框转换成了一个富媒体接收器。用户现在可以从键盘插入 GIF 动画,或者直接将照片拖到应用中。

结论

支持富媒体内容不再是“额外”功能,而是现代 Android 应用开发的核心组成部分。Jetpack Compose 中的 contentReceiver 修饰符极大地简化了这一过程,将原本三个独立的 API 统一到一个简洁的声明式接口中。通过实现这一点,你可以确保你的应用拥有原生应用般的体验,并且能够响应用户如今的实际交互方式。

Comments