手记

用 Jetpack Compose 控制用户输入:藏在明处的文本字段功能

发现 Android 中 Compose 文本输入框的真正潜力

这张图片是DALL·E 3帮忙制作的

介绍

六个月的休息期之后,我又开始写作了,感觉非常兴奋,分享一些使用文本字段的高级和有趣的例子。

文本输入框是构建交互式和动态UI组件的重要组成部分,而Jetpack Compose提供了一系列功能,使这些组件不仅功能强大,而且外观吸引人,而且高度交互。

在这篇文章中,我们将从实现一个简单的文本字段开始,逐渐走向更高级的功能。我们将讨论各种增强功能,包括渐变文字装饰框,以及有趣的文本样式

我们将探讨一些实际用例,例如带有遮罩的文本字段用于输入数据和实时用户标记。最后,我们还将涵盖丰富的媒体内容支持触感反馈,以确保可访问性并提升用户体验。

读完这篇文章后,您将深入了解 Compose 提供的强大功能,并学会如何利用这些功能创建吸引用户且易于使用的应用。Compose 在文本字段方面提供了这些功能。

1 基础知识(基础部分)

在深入探讨 “更多内容” 之前,先用一个简单的例子来介绍 Jetpack Compose 中文本字段的基本概念。

基本文本框例子

    @Composable  
    fun BasicTextFieldExample() {  
        var text by remember { mutableStateOf("初始文本") }  
        TextField(  
            value = text,  
            onValueChange = { text = it },  
            label = { Text("标签") }  
        }
解释说明
  • value 参数绑定到 text 状态变量,确保文本字段显示当前的文本内容。
  • 每当用户在文本字段中输入时,会触发 onValueChange 事件,用新的文本更新状态变量。
2) 渐变文本字段:

渐变输入框和渐变指针

    @Composable  // @Composable 注解表示该函数可以作为可组合项使用
    fun GradientTextField() {  // GradientTextField 函数用于创建一个带有渐变效果的文本输入框
        var text by remember { mutableStateOf("") }  // mutableStateOf 创建一个可变状态
        BasicTextField(  // BasicTextField 是一个基本的文本输入框
            value = text,  
            onValueChange = { text = it },  
            textStyle = TextStyle(  // textStyle 设置文本的样式
                brush = Brush.linearGradient(  // linearGradient 设置线性渐变
                    colors = listOf(Color.Red, Color.Blue, Color.Green, Color.Magenta)  
                ),  
                fontSize = 32.sp  
            ),  
            cursorBrush = Brush.verticalGradient(  // verticalGradient 设置垂直渐变
                colors = listOf(Color.Blue, Color.Cyan, Color.Red, Color.Magenta)  
            ),  
        )  
    }
说明
  • textStyle 参数用于将渐变样式应用于文本。
  • Brush.linearGradient 函数使用颜色列表创建线性渐变。在本示例中,渐变依次过渡到红色、蓝色、绿色和洋红色。
  • cursorBrush 参数用于将垂直渐变效果应用到鼠标光标。
  • Brush.verticalGradient 函数使用颜色列表为鼠标光标创建垂直渐变效果。在本示例中,渐变依次过渡到蓝色、青色、红色和洋红色。

装饰盒

装饰盒的使用展示

    @Composable  
    fun DecoratedTextField() {  
        var text by remember { mutableStateOf("") }  

        BasicTextField(  
            value = text,  
            onValueChange = { text = it },  
            decorationBox = { innerTextField ->  
                Row(  
                    Modifier  
                        .padding(horizontal = 16.dp, vertical = 50.dp)  
                        .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                        .padding(8.dp),  
                    verticalAlignment = Alignment.CenterVertically  
                ) {  
                    Icon(Icons.Default.Email, contentDescription = "Email")  
                    Spacer(modifier = Modifier.width(8.dp))  
                    Box(  
                        modifier = Modifier.weight(1f)  
                    ) {  
                        if (text.isBlank()) {  
                            Text(  
                                text = "输入电子邮件",  
                                style = TextStyle(color = Color.Gray)  
                            )  
                        }  
                        innerTextField()  
                    }  
                    if (!text.isBlank()) {  
                        IconButton(onClick = { text = "" }) {  
                            Icon(Icons.Default.Clear, contentDescription = "清空输入")  
                        }  
                    }  
                }  
            },  
            textStyle = TextStyle(  
                color = Color.Black,  
                fontSize = 16.sp  
            )  
        )  
    }

解释

  • decorationBox 参数允许你在 BasicTextField 周围添加自定义装饰元素。
  • 在这个示例中,我们利用一个 Row 可组合项将电子邮件图标、占位符和清除按钮放置在 BasicTextField 旁边,并添加内边距和边框,从而实现更好的视觉分离效果。
  • text 状态为空时,会显示“输入电子邮件”作为占位符文本。
  • 这是通过将 innerTextField 包裹在 Box 中并在满足条件时显示占位符文本来实现的。
4) 让我们跳得放克一些

让我们试着创建一些有创意的文字,并学习一些单独使用时可能会很有用的选项。

潮酷的文字

    @Composable  
    fun FunkyExample() {  
        var text by remember { mutableStateOf("") }  

        BasicTextField(  
            modifier = Modifier.padding(vertical = 50.dp),  
            onValueChange = { text = it },  
            value = text,  
            textStyle = TextStyle(  
                fontSize = 24.sp,  
                baselineShift = BaselineShift.Superscript,  
                background = Color.Yellow,  
                textDecoration = TextDecoration.Underline,  
                lineHeight = 32.sp,  
                textGeometricTransform = TextGeometricTransform(  
                    scaleX = 3f,  
                    skewX = 0.5f  
                ),  
                drawStyle = Stroke(  
                    width = 10f,  
                ),  
                hyphens = Hyphens.Auto,  
                lineBreak = LineBreak.Paragraph,  
                textMotion = TextMotion.Animated  
            )  
        )  
    }
快速解释
  • 基线偏移量 — 将文本移位以产生上标效果。
  • 文本装饰 — 为文本添加下划线。
  • 文本几何变换 — 将文本水平放大3倍并倾斜。
  • 描边样式 — 使用提供的描边宽度为文本描边
  • 连字符和换行 — 启用自动连字符和简单的换行,以更好地格式化文本。
  • 文本动画 — 动画化文本的位置和样式。
5), 遮罩文本字段

    @Composable  
    fun CreditCardTextField() {  
        var text by remember { mutableStateOf("") }  
        val visualTransformation = CreditCardVisualTransformation()  

        Column(modifier = Modifier.padding(16.dp)) {  
            BasicTextField(  
                value = text,  
                onValueChange = { text = it.filter { it.isDigit() } },  
                visualTransformation = visualTransformation,  
                textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                    .padding(16.dp)  
            )  

            Spacer(modifier = Modifier.height(8.dp))  

            Text(text = "请输入您的信用卡号码", style = TextStyle(fontSize = 16.sp))  
        }  
    }  

    // 仅为示例目的的转换  
    class CreditCardVisualTransformation : VisualTransformation {  
        override fun filter(text: AnnotatedString): TransformedText {  
            val trimmed = if (text.text.length >= 16) text.text.substring(0..15) else text.text  
            val out = StringBuilder()  

            for (i in trimmed.indices) {  
                out.append(trimmed[i])  
                if (i % 4 == 3 && i != 15) out.append(" ")  
            }  

            val creditCardOffsetTranslator = object : OffsetMapping {  
                override fun originalToTransformed(offset: Int): Int {  
                    if (offset <= 3) return offset  
                    if (offset <= 7) return offset + 1  
                    if (offset <= 11) return offset + 2  
                    if (offset <= 16) return offset + 3  
                    return 19  
                }  

                override fun transformedToOriginal(offset: Int): Int {  
                    if (offset <= 4) return offset  
                    if (offset <= 9) return offset - 1  
                    if (offset <= 14) return offset - 2  
                    if (offset <= 19) return offset - 3  
                    return 16  
                }  
            }  

            return TransformedText(AnnotatedString(out.toString()), creditCardOffsetTranslator)  
        }  
    }
解释如下
  • 用于掩码的视觉变换 — 创建了一个自定义的 VisualTransformation(视觉变换类) CreditCardVisualTransformation(信用卡视觉变换类),用于通过每四个字符添加空格来格式化输入的文本。此类中的 OffsetMapping 保证了光标移动能够正确处理,与格式化的文本保持一致。
还能用在哪?
  • 输入电话号码 — 自动将输入格式化为标准电话号码格式(例如,(123) 456-7890)。
  • 输入 SSN — 将输入格式化为 SSN 格式(例如,123-45-6789)。
  • 输入日期 — 将输入格式化为 MM/DD/YYYY 格式。
6) 处理用户互动

交互式文本字段日志记录

    @Composable  
    fun InteractiveTextField() {  
        var text by remember { mutableStateOf("") }  
        val interactionSource = remember { MutableInteractionSource() }  
        val focusRequester = remember { FocusRequester() }  

        LaunchedEffect(interactionSource) {  
            interactionSource.interactions.collect { interaction ->  
                when (interaction) {  
                    is PressInteraction.Press -> println("测试文本字段被按下")  
                    is PressInteraction.Release -> println("测试文本字段被释放")  
                    is FocusInteraction.Focus -> println("测试文本字段获得焦点")  
                    is FocusInteraction.Unfocus -> println("测试文本字段失去焦点")  
                }  
            }  
        }  

        Column(modifier = Modifier.padding(16.dp)) {  
            BasicTextField(  
                value = text,  
                onValueChange = { text = it },  
                interactionSource = interactionSource,  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                    .padding(16.dp)  
                    .focusRequester(focusRequester)  
            )  

            Spacer(modifier = Modifier.height(8.dp))  

            Button(onClick = { focusRequester.requestFocus() }) {  
                Text(text = "让文本字段获得焦点")  
            }  
        }  
    }
对于解释:
  • 一个 MutableInteractionSource 用于跟踪和响应用户与文本输入框的互动。
  • 这允许检测按下、释放、获取焦点和失去焦点等事件。
  • 使用 LaunchedEffect 来收集并处理来自 MutableInteractionSource 的互动。
  • 当文本输入框被按下、释放、获取焦点或失去焦点时,此效果会在控制台打印消息。
  • 使用 FocusRequester 可以编程请求文本输入框的焦点。
  • 提供了一个按钮来演示此功能。
使用案例
  1. 表单验证 — 实时提供错误和验证的反馈。示例 — 用户离开字段时显示错误消息。
  2. 增强可访问性 — 帮助依赖辅助技术的用户更好地导航。示例 — 为视力受损的用户高亮显示聚焦字段。
  3. 用户互动 — 使应用程序更加响应迅速和充满活力。示例 — 用户键入时显示搜索建议。
  4. 基于上下文的操作 — 根据用户的互动触发相应的操作。例如,当用户离开文本字段时保存草稿。
7) 实时用户标记

实时用户标记功能

    @Composable  
    fun RealTimeUserTaggingTextField() {  
        var text by remember { mutableStateOf("") }  
        val context = LocalContext.current  

        val annotatedText = buildAnnotatedString {  
            val regex = Regex("@[\\w]+")  
            var lastIndex = 0  
            regex.findAll(text).forEach { result ->  
                append(text.substring(lastIndex, result.range.first))  
                pushStringAnnotation(tag = "USER_TAG", annotation = result.value)  
                withStyle(style = SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)) {  
                    append(result.value)  
                }  
                pop()  
                lastIndex = result.range.last + 1  
            }  
            append(text.substring(lastIndex))  
        }  

        val focusRequester = remember { FocusRequester() }  

        Column (modifier = Modifier.padding(horizontal = 16.dp)) {  
            Spacer(modifier = Modifier.height(300.dp))  

            BasicTextField(  
                value = text,  
                onValueChange = { text = it },  
                textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .clickable {  
                        focusRequester.requestFocus()  
                    }  
                    .focusRequester(focusRequester)  
                    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                    .padding(8.dp),  
                decorationBox = { innerTextField ->  
                    Box {  
                        ClickableText(  
                            text = annotatedText,  
                            onClick = { offset ->  
                                focusRequester.requestFocus()  
                                annotatedText.getStringAnnotations(tag = "USER_TAG", start = offset, end = offset).firstOrNull()?.let {  
                                    val username = it.item  
                                    Toast.makeText(context, "用户 $username 被点击了", Toast.LENGTH_SHORT).show()  
                                }  
                            },  
                            style = TextStyle(color = Color.Black, fontSize = 18.sp)  
                        )  
                        innerTextField()   
                    }  
                }  
            )  

            Spacer(modifier = Modifier.height(8.dp))  

            Text(text = "输入 @用户名 来标记用户。点击标记的用户名会弹出提示信息。", style = TextStyle(fontSize = 16.sp))  
        }  
    }
以下是解释
  • 一个被注释的字符串被构建来检测并高亮文本中的用户标签。Regex 识别 @[\\w]+(例如 @用户名)的模式匹配。每个检测到的标签都会被注解并用蓝色下划线样式标注。
  • 使用 FocusRequester 来程序化地管理文本框的焦点。点击文本字段内的任意位置或用户标签会请求文本框的焦点。
  • ClickableText 可组合项处理注释文本上的点击事件。当点击用户标签时,它会请求文本字段的焦点并显示一个带有用户名的提示信息
其他应用场景
  1. 社交媒体帖子中的标签(例如#话题)
  2. 在评论中提到用户
  3. 动态地址输入框
第8) 键盘动作

键盘动作

    @Composable  
    fun KeyboardActionsTextField() {  
        var text by remember { mutableStateOf("Lorem Ipsum Lorem Ipsum") }  
        val context = LocalContext.current  

        Column {  
            Spacer(modifier = Modifier.height(300.dp))  

            BasicTextField(  
                value = text,  
                onValueChange = { text = it },  
                textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                    .padding(8.dp),  
                keyboardOptions = KeyboardOptions.Default.copy(  
                    imeAction = ImeAction.Send  
                ),  
                keyboardActions = KeyboardActions(  
                    onDone = {  
                        Toast.makeText(context, "完成了,内容是:$text", Toast.LENGTH_SHORT).show()  
                    },  
                    onSearch = {  
                        Toast.makeText(context, "搜了一下,内容是:$text", Toast.LENGTH_SHORT).show()  
                    },  
                    onGo = {  
                        Toast.makeText(context, "准备前往,内容是:$text", Toast.LENGTH_SHORT).show()  
                    },  
                    onSend = {  
                        Toast.makeText(context, "准备发送,内容是:$text", Toast.LENGTH_SHORT).show()  
                    }  
                )  
            )  
        }  
    }
来解释一下吧
  • keyboardOptions 指定了键盘选项,例如输入法操作。
  • keyboardActions 指定了特定键被按下时的操作。
应用场景
  1. 表单
  2. 搜索框
  3. 通讯软件
  4. 屏幕间切换
9) 提供触感反馈

振动反馈

    @Composable  
    fun 可访问表单() {  
        var email by remember { mutableStateOf("") }  
        var 提交状态 by remember { mutableStateOf("") }  
        var 字符振动 by remember { mutableStateOf("") }  
        val context = LocalContext.current  
        val 振动器 = ContextCompat.getSystemService(context, Vibrator::class.java)  

        val 盲文映射 = mapOf(  
            'a' to longArrayOf(0, 50), // 'a' 的示例盲文模式  
            'b' to longArrayOf(0, 50, 100, 50),  
            'c' to longArrayOf(0, 100),  
            '.' to longArrayOf(0, 100, 100, 100),  
            '@' to longArrayOf(0, 200),  
            'o' to longArrayOf(0, 200, 200, 200),  
            'm' to longArrayOf(0, 200, 200, 200, 200, 200),  
            // 添加其他字符的映射  
        )  

        val 振动 = { 模式: LongArray ->  
            if (振动器?.hasVibrator() == true) {  
                振动器.vibrate(VibrationEffect.createWaveform(模式, -1))  
            }  
        }  

        val 验证电子邮件 = { 输入: String ->  
            when {  
                输入.isEmpty() -> {  
                    振动(longArrayOf(0, 100, 100, 100)) // 警告振动  
                    "请不要留空邮箱"  
                }  
                !android.util.Patterns.EMAIL_ADDRESS.matcher(输入).matches() -> {  
                    振动(longArrayOf(0, 100, 100, 100, 100, 100, 100, 100)) // 错误振动  
                    "邮箱格式不对哦"  
                }  
                else -> {  
                    振动(longArrayOf(0, 50)) // 成功反馈  
                    null  
                }  
            }  
        }  

        Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 100.dp)) {  
            Text("登录表单", style = TextStyle(fontSize = 24.sp, color = Color.Black))  

            Spacer(modifier = Modifier.height(16.dp))  

            BasicTextField(  
                value = email,  
                onValueChange = { 新文本 ->  
                    email = 新文本  
                    新文本.lastOrNull()?.let { 字符 ->  
                        盲文映射[字符]?.let { 模式 ->  
                            字符振动 = "字符 $字符 对应的振动模式 ➡ ${模式.asList()}"  
                            振动(模式)  
                        }  
                    }  
                },  
                textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))  
                    .padding(8.dp),  
                decorationBox = { innerTextField ->  
                    Box(  
                        modifier = Modifier.padding(8.dp)  
                    ) {  
                        if (email.isEmpty()) {  
                            Text("请输入您的电子邮件", style = TextStyle(color = Color.Gray, fontSize = 18.sp))  
                        }  
                        innerTextField()  
                    }  
                }  
            )  

            Spacer(modifier = Modifier.height(8.dp))  
            if(字符振动.isNotEmpty()) {  
                Text(字符振动, style = TextStyle(fontSize = 16.sp, color = Color.DarkGray))  
            }  

            Spacer(modifier = Modifier.height(16.dp))  

            Button(  
                onClick = {  
                    val 邮件错误 = 验证电子邮件(email)  
                    提交状态 = if (邮件错误 == null) {  
                        "提交成功"  
                    } else {  
                        "提交失败:$邮件错误"  
                    }  
                    if (邮件错误 == null) {  
                        振动(longArrayOf(0, 50, 50, 50, 50, 50, 50, 50)) // 成功反馈  
                    }  
                },  
                modifier = Modifier.fillMaxWidth()  
            ) {  
                Text("提交", style = TextStyle(fontSize = 18.sp, color = Color.White))  
            }  

            Spacer(modifier = Modifier.height(16.dp))  

            if(提交状态.isNotEmpty()) {  
                val 文本颜色 = if (提交状态.contains("失败")) Color.Red else Color.Green  
                Text("提交状态:$提交状态", style = TextStyle(fontSize = 16.sp, color = 文本颜色))  
            }  
        }  
    }
说明
  • 盲文映射 — 字符被映射到特定的振动模式。
  • 实时反馈 — 每输入一个字符都会有即时的触觉反馈。
  • 验证反馈 — 错误、警告和成功分别用不同的振动模式来表示。
其他的使用场景
  • 密码强度指示器
  • 多步骤表单完成反馈提示
  • 互动式的盲文学习工具
10) 支持丰富的多媒体内容

支持多样化的多媒体内容

文本字段中的富媒体支持允许用户通过键盘或剪贴板直接插入图片、GIF和其他多媒体内容。此功能在聊天应用、社交媒体帖子等用户希望将视觉内容与文本结合的场景中特别有用,如信息发送或发帖。

    @OptIn(ExperimentalFoundationApi::class)  
    @Composable  
    fun 支持丰富内容() {  
        var images by remember { mutableStateOf<List<Uri>>(emptyList()) }  
        val state = rememberTextFieldState("")  
        val scrollState = rememberScrollState()  
        val coroutineScope = rememberCoroutineScope()  

        Column(  
            modifier = Modifier  
                .padding(16.dp)  
                .fillMaxWidth()  
        ) {  
            Spacer(Modifier.height(125.dp))  
            Row(  
                modifier = Modifier  
                    .padding(bottom = 8.dp)  
                    .fillMaxWidth()  
                    .horizontalScroll(scrollState),  
                horizontalArrangement = Arrangement.spacedBy(8.dp)  
            ) {  
                images.forEach { uri ->  
                    AsyncImage(  
                        model = uri,  
                        contentDescription = null,  
                        modifier = Modifier  
                            .size(100.dp)  
                            .clip(RoundedCornerShape(8.dp))  
                            .border(1.dp, Color.Gray, RoundedCornerShape(8.dp)),  
                        contentScale = ContentScale.Crop  
                    )  
                }  
            }  
            BasicTextField(  
                state = state,  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .background(Color.LightGray, RoundedCornerShape(8.dp))  
                    .padding(16.dp)  
                    .contentReceiver(  
                        receiveContentListener = object : ReceiveContentListener {  
                            override fun onReceive(  
                                transferableContent: TransferableContent  
                            ): TransferableContent? {  

                                if (!transferableContent.hasMediaType(MediaType.Image)) {  
                                    return transferableContent  
                                }  

                                return transferableContent.consume { item ->  
                                    images += item.uri  
                                    coroutineScope.launch {  
                                        scrollState.animateScrollTo(scrollState.maxValue)  
                                    }  
                                    true  
                                }  
                            }  
                        }  
                    ),  
                textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),  
            )  
        }  
    }
解释。
  • 使用**contentReceiver**修饰符 — 允许直接从键盘/剪贴板插入丰富媒体。
  • 可转移内容 — 提供处理内容的元数据和方法。
  • **hasMediaType** — 检查内容是否匹配特定媒体类型(如图像)。

当前限制 — 这是基础库 1.7.0-beta05 中的新代码。在 Pixel 设备上无缝运行;三星及其他设备上的问题请参见此链接 [https://issuetracker.google.com/issues/353556433]。为了获得更广泛的兼容性,建议使用支持旧版 contentReceiver 的 1.7.0-alpha04 版本。

结尾.

当我们结束对 Jetpack Compose 中文本字段功能的探索时,可以看出文本字段不仅提供基本的输入功能。它们是创造丰富、互动性和视觉吸引人的用户体验的强大工具。

调整最微小的细节

我们从最基础的部分开始,逐步引入了带有渐变效果的文字和光标、自定义的装饰框以及有趣味性的文字样式等功能。这些改进不仅让界面更吸引人,还大大提高了用户的互动性和参与感。

呈现卓越的视觉和功能性

实现丰富媒体支持展示了如何将美学与实用性功能完美融合。让用户可以通过文本框插入图片、GIF和其他媒体,增强了用户体验。这对于聊天应用和社交媒体平台等应用非常重要,在这些平台上,视觉内容在用户互动中起着关键作用。

确保易访问和易使用性

提供触觉反馈和实现隐藏文本字段对于创建包容性应用程序来说非常重要。这些功能确保您的应用程序能够被更广泛的用户群体使用,包括有残疾的用户。

最后的remarks

时隔这么久重新开始写作,感觉真不错,我期待未来能恢复到原来的日程。

如果你喜欢你所读的内容,请留下你的宝贵的意见喜欢之处。我希望能从开发者们那里学习、合作并共同成长。

如果有任何问题,随时问我。

关注我在 Medium 上的更多文章,个人 Medium 专栏

可以在 LinkedIn 和 Twitter 上联系我,一起合作哦。

祝你创作顺利!(温馨提示:双关呀!)

0人推荐
随时随地看视频
慕课网APP