持续创造,加速生长!这是我参加「日新计划 10 月更文挑战」的第12天,点击检查活动概况

前语

在 Compose 中如果咱们想要完成文本输入框的话,一般都是运用 Material 包中的TextField或许OutlinedTextField

可是由于这两个组件都是属于 Material 包中的,自然是需求符合 Material 设计标准的,这也就会导致运用他们会丧失很多灵活性。

如果咱们想自己完成一些不符合 Material 标准可是很帅炫的作用,亦或是其他设计风格,那持续运用TextField或许OutlinedTextField将会变得非常痛苦,乃至没法完成。

好在,Compose 提供了一个名为BasicTextField的组件,这个组件比上面两个级别更低(上面两个位于androidx.compose.material包,而它位于androidx.compose.foundation.text包),相比于他们有着极大的灵活性。其实上述两个组件都是对BasicTextField的封装。

下面,咱们就以仿写一个微信的查找框为例解说怎么完成运用BasicTextField

开端

剖析布局

在开端之前咱们先剖析一下微信的查找框是什么姿态的。

这是没有输入内容时:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

这是输入内容后:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

能够看到,在没有输入内容前,输入框有一个前导图标显示查找,中间输入框中有一个淡色的占位字符,最终有一个后置图标显示语音输入。

而输入内容后,占位字符铲除,后置图标更改为铲除图标。

这么一剖析,如同没啥难度啊,直接用OutlinedTextField完全能够完成嘛。

是吗?那咱们先尝试直接用OutlinedTextField仿写一下试试。

直接运用 OutlinedTextField

根据上面的剖析,无非便是一个OutlinedTextField加上前导图标还有后置图标,以及占位字符罢了嘛,所以咱们很容易就能编写出这样的代码:

var inputText by remember { mutableStateOf("") }
OutlinedTextField(
    value = inputText,
    onValueChange = {
        inputText = it
    },
    leadingIcon = {
        Icon(imageVector = Icons.Outlined.Search, contentDescription = null)
    },
    trailingIcon = {
        if (inputText.isNotEmpty()) Icon(imageVector = Icons.Outlined.Close, contentDescription = null)
        else Icon(imageVector = Icons.Outlined.Mic, contentDescription = null)
    },
    placeholder = {
        Text(text = "查找")
    },
)

其间后置图标经过inputText.isNotEmpty()判断输入内容是否为空,如果为空则显示麦克风图标,不为空则显示铲除图标,运转作用如下:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

咋一看如同没啥问题,细心一看发现如同不对劲。

对了,是输入框布景色彩不对劲,而且微信的输入框是有圆角的,那就改一下吧。

首要是加上圆角,添加参数:

shape = RoundedCornerShape(8.dp)

然后改一下布景色彩,这儿咱们经过从头指定一个 colors 色彩装备文件并修正其间的backgroundColor字段完成修正布景色彩:

    colors = TextFieldDefaults.outlinedTextFieldColors(
        backgroundColor = Color.White
    )

修正完成,再次运转:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

这下如同对味了?不对!还是不对劲,首要微信的输入框是没有边框的;其次在微信中即使输入框拿到焦点边框也不会变色;别的微信的后置语音图标是黑色的,不是灰色的。

那么咱们再改一改。

首要是语音输入图标色彩,这个没什么难度,运用tint参数从头着色即可:

Icon(imageVector = Icons.Outlined.Mic, contentDescription = null, tint = Color.Black)

接下来是去掉边框,这个就不好弄了。

我看了一圈文档,发现没有提供设置边框尺寸的当地,又看了一下源码,果然,边框尺寸被直接写死了:

调用OutlinedTextField后,会调用到TextFieldImpl函数,并在其间经过TextFieldTransitionScope.Transition获取到边框宽度。

TextFieldTransitionScope.Transition中对边框宽度的界说如下:

val indicatorWidth by transition.animateDp(
    label = "IndicatorWidth",
    transitionSpec = { tween(durationMillis = AnimationDuration) }
) {
    when (it) {
        InputPhase.Focused -> IndicatorFocusedWidth
        InputPhase.UnfocusedEmpty -> IndicatorUnfocusedWidth
        InputPhase.UnfocusedNotEmpty -> IndicatorUnfocusedWidth
    }
}

能够看到这儿是界说的一个动画,可是不要紧,咱们只需求关心动画完成后最终的宽度值是多少就行,检查上面两个个常量值:

private val IndicatorUnfocusedWidth = 1.dp
private val IndicatorFocusedWidth = 2.dp

能够看到,在持有焦点时的宽度是 2 dp,没有焦点时是 1 dp。

不过,既然无法自己界说边框宽度,那咱们改一下色彩总能够了吧?把边框色彩改成和布景色彩相同,约等于没有边框嘛。

改边框色彩依旧是修正colors色彩装备信息,这儿需求把聚焦和失焦时的色彩都改成白色:

colors = TextFieldDefaults.outlinedTextFieldColors(
            backgroundColor = Color.White,
            focusedBorderColor = Color.White,
            unfocusedBorderColor = Color.White
        )

最终完整代码:

var inputText by remember { mutableStateOf("") }
OutlinedTextField(
    value = inputText,
    onValueChange = {
        inputText = it
    },
    leadingIcon = {
        Icon(imageVector = Icons.Outlined.Search, contentDescription = null)
    },
    trailingIcon = {
        if (inputText.isNotEmpty()) Icon(imageVector = Icons.Outlined.Close, contentDescription = null)
        else Icon(imageVector = Icons.Outlined.Mic, contentDescription = null, tint = Color.Black)
    },
    placeholder = {
        Text(text = "查找")
    },
    shape = RoundedCornerShape(8.dp),
    colors = TextFieldDefaults.outlinedTextFieldColors(
        backgroundColor = Color.White,
        focusedBorderColor = Color.White,
        unfocusedBorderColor = Color.White
    )
)

现在再来看看作用:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

如同差不多了欸?哈哈,你再细心看看。

发现问题了吗?

没错,虽然大体上是像了,可是明显文本和图标相对于输入框的边距不对劲啊。

又是翻了一圈文档和源码,并没有发现设置边距的当地,算了,太麻烦了,咱们还是运用BasicTextField自界说一个吧。

运用 BasicTextField 自界说

BasicTextField的参数和OutlinedTextField大差不差:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

可是它多了一个要害参数decorationBox,得益于这个参数,咱们能够随心所欲了。

根据文档介绍:

decorationBox – Composable lambda that allows to add decorations around text field, such as icon, placeholder, helper messages or similar, and automatically increase the hit target area of the text field. To allow you to control the placement of the inner text field relative to your decorations, the text field implementation will pass in a framework-controlled composable parameter “innerTextField” to the decorationBox lambda you provide. You must call innerTextField exactly once.

简略来说便是这个参数是一个作用域为 Composable 且带有参数innerTextField匿名函数

innerTextField也是一个 Composable 的匿名函数,而且它便是输入框的完成函数。

也便是说,咱们能够在decorationBox中经过自界说innerTextField的调用方位等方法完成自界说自己需求的文本框的目的。

需求留意的是,正如上面说的,innerTextField是输入框的完成,所以咱们有必要而且也只能调用一次这个函数,不然咱们的组件里边就没有输入框了。

依旧是完成上述的微信查找框,咱们能够这样写:

var inputText by remember { mutableStateOf("") }
BasicTextField(
    value = inputText,
    onValueChange = {
        inputText = it
    },
    decorationBox = { innerTextField ->
        Box {
            Surface(
                // border = BorderStroke(1.dp, Color.Gray),
                shape = RoundedCornerShape(8.dp)
            ) {
                Row(
                    modifier = Modifier.padding(8.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(imageVector = Icons.Outlined.Search, contentDescription = null, tint = Color(0x88000000))
                    Box(modifier = Modifier.padding(start = 4.dp, end = 4.dp)) {
                        if (inputText.isEmpty()) Text(text = "查找", color = Color(0x88000000))
                        innerTextField()
                    }
                    if (inputText.isNotEmpty()) Icon(imageVector = Icons.Outlined.Close, contentDescription = null, tint = Color(0x88000000))
                    else Icon(imageVector = Icons.Outlined.Mic, contentDescription = null, tint = Color(0xFF000000))
                }
            }
        }
    }
)

其他当地没什么好说的,咱们来要点剖析decorationBox的内容。

首要,咱们的根组件挑选了Surface,这是 Material 中的组件之一,官方称之为 “平面”,简略来说便是能够把它包括的内容以一致的样式装备(例如边框、暗影、圆角等)放到同一个“平面”内。

由于咱们需求给输入框加上圆角,所以挑选它做根组件,并设置了 8dp 的圆角shape = RoundedCornerShape(8.dp)

由于输入框的三个主要组件:前置图标、输入框(占位字符)、后置图标是水平摆放的,所以接下来用了一个Row,并设置笔直对齐方法为居中verticalAlignment = Alignment.CenterVertically

然后根据需求设置前置图标,后置图标,以及装备色彩和边距等这儿就不过多赘述了,要点需求留意占位文本和输入框(innerTextField())的摆放。

由于占位文本和输入框实际上应该是属于同一个方位的,虽然在输入框有内容后就不会显示占位文本了,可是咱们依旧需求把他们放到Box中,即堆叠到同一个方位,不然将会变成这样:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

没看出差异?细心看光标,输入框已经被挤到占位文本之后了。

加上Box后作用如下:

自定义 Compose 的 TextField,实现各种酷炫的文本框效果

这样看起来是不是对味多了?

总结

咱们经过模仿微信查找框的方法解说了怎么运用BasicTextField自界说文本输入框作用。

当然,这儿仅仅抛砖引玉,仅仅简略的介绍了运用方法,并没有做什么酷炫的组件,可是知道了怎么运用BasicTextField想要完成什么酷炫的输入框作用那还不是手到擒来?