Compose之文本编辑及输入法相关

持续创作,加速成长!这是我参与「掘金日新计划 · 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 = {

    })
}

长这样:

default.png

别看光标闪闪,其实这个编辑框徒有其表 —— 因为它不能编辑!

这时候应该反应过来为什么参数里的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)
}

这样一来,这就是一个正常的编辑框了(可以注意到,这里用到了 remembermutableStateOf,暂不深入探究,记住这是由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的进行桥连:

tis.png

如上,它提供了输入法的显示与隐藏,类似于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,
    //...

使用过 GlidePicasso 的同学肯定能猜到这个东西的基本用意了。它的注释是这样的:

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”已经呼之欲出了:

  1. placeholder的参数类型,是一个Composable方法,也就是说,它是一个组件构造
  2. 此组件的显示时机是:编辑框有焦点,且输入文本为空时

这不就是我们的hint吗?

写一个简单的提示看看:

TextField(
    value = text,
    onValueChange = setText,
    placeholder = {
        Text("输入否?", color = Color.Gray)
    },
    modifier = Modifier.focusRequester(focusRequester)
)

没有placeholder的时候,编辑框,空荡荡:

noholder.png

有了placeholder后,再也不用那么愣了:

placeholder.png

输入法的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。这时候,“回车”可以正常输入换行,且“回车”显示的也是“换行”,如下:

enter.png

输入回车:

entered.png

现在,设置action为ImeAction.Done看看:

TextField(
        value = text,
        onValueChange = setText,
        placeholder = {
            Text("输入否?", color = Color.Gray)
        },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
        modifier = Modifier.focusRequester(focusRequester)
    )

done.png

可以看到,“回车”已经变成了“完成”,这时候已经不能输入换行了。

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定制,输入文本内容的控制等等,还需要进一步研究和学习。

猜你喜欢

转载自juejin.im/post/7111887037212393508