持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情。
用惯了 View 系统里的EditText
后,在 Compose 世界里,控制输入法似乎变得既陌生又问题多多 —— 这就是本人的真实写照!
比如说,界面显示后自动弹出输入法,应该如何做?又比如说,EditText
里的那些action,怎么设置?Compose是不是完全相通的呢?
带着这些问题,我们来一起学习研究吧。
Compose的文本编辑
添加一个普通的文本编辑框,可以使用TextField
:
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
) {
// ......
}
我们用它实现一个最基本的:
class ComposeEditActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BonjourTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
TextField(value = "Hello $name!", onValueChange = {
})
}
长这样:
别看光标闪闪,其实这个编辑框徒有其表 —— 因为它不能编辑!
这时候应该反应过来为什么参数里的onValueChange
是必传项了,它是用来将输入法的“改变内容”更新到文本框的方法。其注释也说明了问题:
the callback that is triggered when the input service updates the text. An updated text comes as a parameter of the callback
修改下:
@Composable
fun Greeting(name: String) {
val (text, setText) = remember { mutableStateOf("Hello $name!") }
TextField(value = text, onValueChange = setText)
}
这样一来,这就是一个正常的编辑框了(可以注意到,这里用到了 remember
及mutableStateOf
,暂不深入探究,记住这是由Compose的特性决定的就行,即:组件的更新基于数据更新,数据更新,Re-Compose才出现)。
所以,value
拿text更新显示,onValueChange
通过setText更新text的值,这样TextField
就能正常跟着输入变化了。
输入法的自动显示
在View的世界里,界面加载后,让EditText
自动聚焦并呼出输入法,是一个很常见的操作。再看看TextField
,并没有焦点或者输入法相关的参数,那应该怎么做呢?
用TextInputService
。
TextInputService
Handles communication with the IME. Informs about the IME changes via EditCommands and provides utilities for working with software keyboard.
TextInputService
,功能就是和IME的进行桥连:
如上,它提供了输入法的显示与隐藏,类似于InputMethodManager
的作用。
要获取TextInputService
也很简单,CompositionLocals自带:
val LocalTextInputService = staticCompositionLocalOf<TextInputService?> { null }
修改如下:
val input = LocalTextInputService.current
val (text, setText) = remember { mutableStateOf("Hello $name!") }
TextField(value = text, onValueChange = setText)
input?.showSoftwareKeyboard()
运行吧,等待着输入法自动调出的成功实现……
然后……打脸了……
为什么呢?
回想一下在 View 系统里调出输入法的操作,其实有两步。
- 第1步:调用输入法显示,其中view是取得焦点的view,用于接收输入:
InputMethodManager.showSoftInput(view: View, flags: Int)
- 第2步:获取焦点,有了焦点才能和输入法产生连接
view.requestFocus()
所以,前面的做法,似乎是少了“焦点”这一步。那如何在Compose中获取焦点呢?得FocusRequester
登场了。
FocusRequester
FocusRequester
用于发起焦点请求,改变焦点:
/**
* The [FocusRequester] is used in conjunction with
* [Modifier.focusRequester][androidx.compose.ui.focus.focusRequester] to send requests to
* change focus.
*
* @sample androidx.compose.ui.samples.RequestFocusSample
*
* @see androidx.compose.ui.focus.focusRequester
*/
class FocusRequester {
// ....
}
使用时,调用Modifier.focusRequester
。更改代码如下:
val input = LocalTextInputService.current
val focusRequester = remember { FocusRequester() }
val (text, setText) = remember { mutableStateOf("Hello $name!") }
TextField(value = text, onValueChange = setText, modifier = Modifier.focusRequester(focusRequester))
input?.showSoftwareKeyboard()
focusRequester.requestFocus()
嗯,上面代码没带来奇迹,带来了崩溃:
java.lang.IllegalStateException:
FocusRequester is not initialized. Here are some possible fixes:
1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() }
2. Did you forget to add a Modifier.focusRequester() ?
3. Are you attempting to request focus during composition? Focus requests should be made in
response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }
at androidx.compose.ui.focus.FocusRequester.requestFocus(FocusRequester.kt:54)
......
这个错误日志真的良心了,连可能的使用错误点都列举告知了。
看起来,第3条就是崩溃原因所在:我们在界面compose过程中,调用了request —— 按View世界观点来说,相当于view都还没创建完成,就已经在给它加焦点了。
可把request包裹到LaunchedEffect
中来解决此问题,让request在其提供的协程scope中执行:
LaunchedEffect("ime") {
input?.showSoftwareKeyboard()
focusRequester.requestFocus()
}
【思考】LaunchedEffect做了什么?
Hint在哪里?
Hint对于EditText
来说,是个十分常见的显示方式,用于输入框的内容提示。但是在TextField
的参数列表中,却没有看到任何“hint”的身影。
当然不是Compose的编辑框不支持hint,只是,从View系统过来的你,真的有时候要转换一个思维:有些东西,Compose的实现真和传统上想像的不一样。
回看TextField
的参数列表,其中有一个参数是placeholder
:
@Composable
fun TextField(
//...
placeholder: @Composable (() -> Unit)? = null,
//...
使用过 Glide 或 Picasso 的同学肯定能猜到这个东西的基本用意了。它的注释是这样的:
placeholder - the optional placeholder to be displayed when the text field is in focus and the input text is empty. The default text style for internal Text is Typography.subtitle1
嗯,“hint”已经呼之欲出了:
- placeholder的参数类型,是一个
Composable
方法,也就是说,它是一个组件构造 - 此组件的显示时机是:编辑框有焦点,且输入文本为空时
这不就是我们的hint吗?
写一个简单的提示看看:
TextField(
value = text,
onValueChange = setText,
placeholder = {
Text("输入否?", color = Color.Gray)
},
modifier = Modifier.focusRequester(focusRequester)
)
没有placeholder的时候,编辑框,空荡荡:
有了placeholder后,再也不用那么愣了:
输入法的options
对比着来看,View世界的EditText
提供option的设置,表明了不同的输入法状态,比如actionDone
是“完成”,功能是隐藏输入法,actionNext
是“下一行”,功能是切换编辑框(有多个的情况下),等等……
而TextField
则使用KeyboardOptions
来控制:
KeyboardOptions
@Immutable
class KeyboardOptions constructor(
val capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
val autoCorrect: Boolean = true,
val keyboardType: KeyboardType = KeyboardType.Text,
val imeAction: ImeAction = ImeAction.Default
) {
// ....
}
相应的,各个 action 是由ImeAction
类实现:
/**
Signals the keyboard what type of action should be displayed. It is not guaranteed if the keyboard will show the requested action.
*/
inline class ImeAction internal constructor(@Suppress("unused") private val value: Int) {
// ...
companion object {
/**
* Use the platform and keyboard defaults and let the keyboard to decide the action. The
* keyboards will mostly show one of [Done] or [None] actions based on the single/multi
* line configuration.
*/
val Default: ImeAction = ImeAction(1)
/**
* Represents that no action is expected from the keyboard. Keyboard might choose to show an
* action which mostly will be newline, however this action is not carried into the app via
* any [Keyboard Action][androidx.compose.foundation.text.KeyboardAction].
*/
val None: ImeAction = ImeAction(0)
/**
* Represents that the user would like to go to the target of the text in the input i.e.
* visiting a URL.
*/
val Go: ImeAction = ImeAction(2)
/**
* Represents that the user wants to execute a search, i.e. web search query.
*/
val Search: ImeAction = ImeAction(3)
/**
* Represents that the user wants to send the text in the input, i.e. an SMS.
*/
val Send: ImeAction = ImeAction(4)
/**
* Represents that the user wants to return to the previous input i.e. going back to the
* previous field in a form.
*/
val Previous: ImeAction = ImeAction(5)
/**
* Represents that the user is done with the current input, and wants to move to the next
* one i.e. moving to the next field in a form.
*/
val Next: ImeAction = ImeAction(6)
/**
* Represents that the user is done providing input to a group of inputs. Some
* kind of finalization behavior should now take place i.e. the field was the last element in
* a group and the data input is finalized.
*/
val Done: ImeAction = ImeAction(7)
}
}
拿前文的案例来说,默认情况下,action是ImeAction.Default
。这时候,“回车”可以正常输入换行,且“回车”显示的也是“换行”,如下:
输入回车:
现在,设置action为ImeAction.Done
看看:
TextField(
value = text,
onValueChange = setText,
placeholder = {
Text("输入否?", color = Color.Gray)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
modifier = Modifier.focusRequester(focusRequester)
)
可以看到,“回车”已经变成了“完成”,这时候已经不能输入换行了。
KeyboardActions
既然已经是“完成”了,点击“完成”,我们期待的是完成输入,输入法隐藏。然而事与愿违,这时点击“完成”毫无反应。
因为我们还需要KeyboardActions
来指定操作:
/**
* The [KeyboardActions] class allows developers to specify actions that will be triggered in
* response to users triggering IME action on the software keyboard.
*/
class KeyboardActions(
// ...
val onDone: (KeyboardActionScope.() -> Unit)? = null,
// ...
val onGo: (KeyboardActionScope.() -> Unit)? = null,
// ...
}
它提供了各种action下的回调,这里设置下onDone
的:
val focusManager = LocalFocusManager.current
TextField(
value = text,
onValueChange = setText,
placeholder = {
Text("输入否?", color = Color.Gray)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
// 点击完成回调到此
focusManager.clearFocus()
}),
modifier = Modifier.focusRequester(focusRequester)
)
要实现隐藏输入法体现“完成”的状态,还需要搭配FocusManager
,用它清掉焦点,即可隐藏输入法。否则,单是回调一下,输入法会岿然不动。
小结
今天把Compose的编辑框简单讨论学习了下,涵盖使用EditText时常用到的一些功能。更多的东西,比如说编辑框的UI定制,输入文本内容的控制等等,还需要进一步研究和学习。