You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中实现带IP验证与自动格式化的TextField?

在Jetpack Compose中实现带IP验证与自动格式化的TextField最简方案

你需要的IP地址+端口输入功能,完全可以通过单个TextField配合自定义视觉转换和输入逻辑实现,比XML拆分多个输入框的方案精简太多。下面是完整的可复用实现:

核心实现思路

  • 用VisualTransformation将纯数字输入转换为带.和:的IP+端口格式(仅视觉分段,实际输入值仍为纯数字,方便后续解析)
  • 在onValueChange中处理输入验证:过滤非数字、限制每段IP不超过255、端口不超过65535
  • 自动跳转逻辑:当前段输入满3位或数值超过25时自动切换到下一段;删除时自动回退到上一段

完整代码示例

import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation

@Composable
fun IPPortTextField(
    value: String,
    onValueChange: (String) -> Unit,
    label: String = "IP地址+端口"
) {
    TextField(
        value = value,
        onValueChange = { rawInput ->
            // 过滤所有非数字字符
            val filtered = rawInput.filter { it.isDigit() }
            val processed = buildString {
                var segmentIndex = 0
                var currentSegment = ""
                filtered.forEach { char ->
                    currentSegment += char
                    when (segmentIndex) {
                        // 前4段为IP,每段最多3位,数值超25时自动跳转
                        0,1,2,3 -> {
                            if (currentSegment.length == 3 || (currentSegment.length == 2 && currentSegment.toInt() > 25)) {
                                append(currentSegment)
                                if (segmentIndex < 3) append(".")
                                currentSegment = ""
                                segmentIndex++
                            }
                        }
                        // 第5段为端口,最多5位(符合0-65535范围)
                        4 -> {
                            if (currentSegment.length == 5) {
                                append(currentSegment)
                                currentSegment = ""
                                segmentIndex++
                            }
                        }
                        // 超出端口段的字符直接忽略
                        else -> return@forEach
                    }
                }
                // 追加未完成的当前段
                if (currentSegment.isNotEmpty()) append(currentSegment)
            }
            // 处理删除回退逻辑:删除到分隔符时自动回退上一段
            val finalValue = if (rawInput.length < value.length && value.isNotEmpty()) {
                val lastChar = value.last()
                if (lastChar == '.' || lastChar == ':') processed.dropLast(1) else processed
            } else {
                processed
            }
            onValueChange(finalValue)
        },
        label = { Text(label) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        visualTransformation = IPVisualTransformation()
    )
}

// 自定义视觉转换器:将纯数字转换为IP+端口的格式化显示
class IPVisualTransformation : VisualTransformation {
    override fun filter(text: androidx.compose.ui.text.AnnotatedString): androidx.compose.ui.text.AnnotatedString {
        val rawText = text.text
        if (rawText.isEmpty()) return text

        return androidx.compose.ui.text.AnnotatedString.Builder().run {
            var segmentIndex = 0
            var charCount = 0
            rawText.forEach { char ->
                append(char)
                charCount++
                when (segmentIndex) {
                    0,1,2 -> {
                        if (charCount == 3) {
                            append(".")
                            charCount = 0
                            segmentIndex++
                        }
                    }
                    3 -> {
                        if (charCount == 3) {
                            append(":")
                            charCount = 0
                            segmentIndex++
                        }
                    }
                    // 端口段不添加分隔符
                    4 -> {}
                }
            }
            toAnnotatedString()
        }
    }
}

// 调用示例
@Composable
fun IPPortInputDemo() {
    val ipPortValue = remember { mutableStateOf("") }
    IPPortTextField(
        value = ipPortValue.value,
        onValueChange = { ipPortValue.value = it }
    )
}

关键逻辑说明

  1. 输入过滤与分段:

    • 自动过滤非数字字符,确保输入合法性
    • IP段自动限制长度与数值范围,输入26时直接跳转下一段,避免输入256这类无效值
    • 端口段限制最多5位,符合TCP/UDP端口的标准范围
  2. 视觉转换:

    • 自动在IP段间添加.,第4段IP后添加:分隔端口,视觉上和多输入框一致
    • 实际绑定的value仍为纯数字,后续解析IP和端口时无需额外处理格式
  3. 删除体验优化:

    • 当删除操作触碰到分隔符时,自动回退到上一段的末尾,模拟多输入框的连贯删除体验

内容的提问来源于stack exchange,提问作者Sergio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 16:16:04