Jetpack Compose中的软键盘与焦点控制
Posted 川峰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的软键盘与焦点控制相关的知识,希望对你有一定的参考价值。
FocusRequester 与 FocusManager
在 Compose 中,可以通过 FocusRequester
与 FocusManager
这两个对象可以主动在代码中控制焦点获取和取消焦点,其中FocusRequester
可以用来获取焦点,通过调用它的requestFocus()
方法来实现,而 FocusManager
可以用来取消焦点(以及移动焦点),通过调用它的clearFocus()
方法来实现。(不知道 Compose 官方的设计初衷是什么,这里为什么要搞两个对象。。)
@Composable
fun FocusControlExample()
var text by remember mutableStateOf("")
var hasFocus by remember mutableStateOf(false)
val focusRequester = remember FocusRequester()
val focusManager = LocalFocusManager.current
Column(horizontalAlignment = Alignment.CenterHorizontally)
TextField(
value = text,
onValueChange = text = it ,
Modifier
.focusRequester(focusRequester) // 如果要代码中requestFocus()这句必须加,否则会报错
.onFocusChanged hasFocus = it.isFocused ,
placeholder = Text("请输入...") ,
textStyle = TextStyle(fontSize = 20.sp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent // 修改输入框背景色
),
)
Button(
onClick =
if (!hasFocus)
// 必须先调用Modifier.focusRequester()绑定FocusRequester,否则报错
focusRequester.requestFocus() // 主动获取焦点
else
focusManager.clearFocus() // 主动清空焦点
)
Text(if (!hasFocus) "Focus" else "UnFocus")
这里有一个非常重要的点需要注意:在调用 focusRequester.requestFocus()
之前必须保证已经调用Modifier.focusRequester()
为组件绑定过了FocusRequester
,否则将会导致应用崩溃。
另外这里使用了 Modifier.onFocusChanged
修饰符来获取焦点状态,它的lambda中可以获取到一个FocusState
参数,FocusState.isFocused
可以判断当前组件是否获取焦点,而FocusState.hasFocus
则可以判断是否包含获取焦点的child组件。
进入页面后输入框自动弹出软键盘
既然 focusRequester
可以主动获取焦点,对于TextField
组件,那么我们理所当然地可以在进入其Composable重组作用域时,就通过一个副作用Api来启动协程进行焦点获取,那么软键盘自然也就自动弹出了。
@Composable
fun AutoFocusExample()
var text by remember mutableStateOf("")
var hasFocus by remember mutableStateOf(false)
val focusRequester = remember FocusRequester()
LaunchedEffect(Unit)
if (!hasFocus) focusRequester.requestFocus()
Column(horizontalAlignment = Alignment.CenterHorizontally)
TextField(
value = text,
onValueChange = text = it ,
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged hasFocus = it.isFocused ,
textStyle = TextStyle(fontSize = 20.sp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent
),
)
捕获焦点
研究了一下,在Compose中捕获焦点跟获取到焦点是两个概念,使用还是通过 focusRequester
的两个方法 captureFocus()
和 freeFocus()
,具体区别可以看下面代码与运行效果:
@Composable
fun CaptureFocusExample()
Column(horizontalAlignment = Alignment.CenterHorizontally)
val focusRequester = remember FocusRequester()
var text by remember mutableStateOf("")
TextField(
value = text,
onValueChange = text = it ,
placeholder = Text(text = "TextField1"),
modifier = Modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 20.sp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent
),
)
val focusRequester2 = remember FocusRequester()
var value by remember mutableStateOf("")
var borderColor by remember mutableStateOf(Color.Transparent)
TextField(
value = value,
onValueChange =
value = it.apply
if (length > 5)
focusRequester2.captureFocus() //捕获焦点
else
focusRequester2.freeFocus() // 释放焦点
,
placeholder = Text(text = "TextField2"),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent
),
modifier = Modifier
.border(2.dp, borderColor)
.focusRequester(focusRequester2)
.onFocusChanged
borderColor = if (it.isCaptured) Color.Red else Color.Transparent
)
val focusManager = LocalFocusManager.current
Button(onClick = focusManager.clearFocus() ) Text("clearFocus")
Button(onClick = focusRequester.requestFocus() ) Text("TextField1 requestFocus")
可以看到在捕获焦点以后,任何其他组件均不能获取焦点(包括clearFocus()
也不会起作用),直到当前组件调用freeFocus()
释放焦点。 相当于当前组件锁定了焦点模式。
这里为每个TextField
组件都绑定了一个 FocusRequester
对象,每个 FocusRequester
对象控制各自绑定的组件的焦点获取。而 FocusManager
使用一个就可以了,因此 FocusManager
的获取是通过 LocalFocusManager.current
它获取到的是一个范围内的单例。
通过 SoftwareKeyboardController 控制软键盘显示与隐藏
前面通过 FocusManager.clearFocus()
方法可以取消组件的焦点,对于TextField
组件,如果失去焦点,那么软键盘也会自动隐藏。这里 Compose 中还有另一种方式来专门控制键盘的显示与隐藏,就是通过SoftwareKeyboardController
来实现。
@OptIn(ExperimentalComposeUiApi::class, ExperimentalLayoutApi::class)
@Composable
fun KeyboardControllerExample()
var text by remember mutableStateOf("")
var hasFocus by remember mutableStateOf(false)
val focusRequester = remember FocusRequester()
val keyboardController = LocalSoftwareKeyboardController.current
val isImeVisible = WindowInsets.isImeVisible
Column(horizontalAlignment = Alignment.CenterHorizontally)
TextField(
value = text,
onValueChange = text = it ,
Modifier
.focusRequester(focusRequester)
.onFocusChanged hasFocus = it.isFocused ,
placeholder = Text("请输入...") ,
textStyle = TextStyle(fontSize = 20.sp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent
),
)
Button(
onClick =
if (!hasFocus)
focusRequester.requestFocus() // 主动获取焦点
else
if (isImeVisible)
keyboardController?.hide() // 隐藏软键盘
else
keyboardController?.show() // 显示软键盘
)
Text(
if (!hasFocus) "request Focus" else
if (isImeVisible) "hide keyboard" else "show keyboard"
)
val focusManager = LocalFocusManager.current
Button(onClick = focusManager.clearFocus() )
Text(text = "clear Focus")
需要注意的是,软键盘的显示与隐藏的前提是:输入框组件已经获取到了焦点。如果输入框都没有获取到焦点,主动调用显示软键盘不会有任何效果,因为它不知道要往哪里输入。另外这里使用了 Compose 提供的一个很方便的api WindowInsets.isImeVisible
它可以判断软键盘的显示状态。
通过 InputMethodManager 控制软键盘显示与隐藏
在面向传统View体系的开发中,一般我们在项目中应该都会有相应的工具类来专门处理软键盘的显示与隐藏,那么能不能直接复用以前的工具类代码呢?答案是可以。例如我们以前传统的开发中是通过context.getSystemService(Context.INPUT_METHOD_SERVICE)
拿到 InputMethodManager
对象,再通过该对象的方法来操作软键盘,如果你的项目中以前有这样的代码,那么依然可以拿来直接使用。
例如之前有如下封装:
// KeyboardUtils.kt
/**
* 显示软键盘
* @param context
*/
fun showKeyboard(context: Context)
if (context is Activity)
context.apply
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
runOnUiThread
currentFocus?.let
inputManager.showSoftInput(it, 0)
/**
* 隐藏软键盘
* @param context
*/
fun hideKeyboard(context: Context)
if (context is Activity)
context.apply
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
runOnUiThread
currentFocus?.let
inputManager.hideSoftInputFromWindow(it.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
那么就可以如下调用:
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun KeyboardControllerOldWayExample()
var text by remember mutableStateOf("")
var hasFocus by remember mutableStateOf(false)
val focusRequester = remember FocusRequester()
val context = LocalContext.current
val isImeVisible = WindowInsets.isImeVisible
Column(horizontalAlignment = Alignment.CenterHorizontally)
TextField(
value = text,
onValueChange = text = it ,
Modifier
.focusRequester(focusRequester)
.onFocusChanged hasFocus = it.isFocused ,
placeholder = Text("请输入...") ,
textStyle = TextStyle(fontSize = 20.sp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent
),
)
Button(
onClick =
if (!hasFocus)
focusRequester.requestFocus() // 主动获取焦点
else
if (isImeVisible)
hideKeyboard(context) // 隐藏软键盘
else
showKeyboard(context) // 显示软键盘
)
Text(
if (!hasFocus) "request Focus" else
if (isImeVisible) "hide keyboard" else "show keyboard"
)
val focusManager = LocalFocusManager.current
Button(onClick = focusManager.clearFocus() )
Text(text = "clear Focus")
运行效果跟前面通过SoftwareKeyboardController
实现的效果一模一样。
在其他组件上应用 FocusRequester
注意到 FocusRequester
是通过Modifier
修饰符来提供绑定的,因此它并不是仅仅针对 TextField
输入框组件的,任何组件上都可以使用它来控制获取焦点。
下面是使用Box
组件获取焦点的示例:
@Composable
fun FocusBoxSample()
val boxFocusRequester = remember FocusRequester()
val boxFocusRequester2 = remember FocusRequester()
var color by remember mutableStateOf(Color.Black)
var color2 by remember mutableStateOf(Color.Black)
val focusManager = LocalFocusManager.current
Column(horizontalAlignment = Alignment.CenterHorizontally)
Box(
Modifier
.size(100.dp)
.clickable boxFocusRequester.requestFocus()
.border(2.dp, color)
// The focusRequester should be added BEFORE the focusable.
.focusRequester(boxFocusRequester)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged color = if (it.isFocused) Color.Red else Color.Black
.focusable()
)
Spacer(modifier = Modifier.height(20.dp))
Box(
Modifier
.size(100.dp)
.clickable boxFocusRequester2.requestFocus()
.border(2.dp, color2)
// The focusRequester should be added BEFORE the focusable.
.focusRequester(boxFocusRequester2)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged color2 = if (it.isFocused) Color.Red else Color.Black
.focusable()
)
Button(onClick = focusManager.clearFocus() ) Text(text = "ClearFocus")
需要注意的是,为了保证生效,在Modifier
链中的focusRequester
和 onFocusChanged
必须在 focusable()
之前调用。
通过FocusRequester.createRefs()创建多个FocusRequester
由于每个需要进行焦点控制的组件都需要绑定一个FocusRequester
对象,如果一个一个创建太麻烦了,因此 Compose 提供了 FocusRequester.createRefs()
可以通过解构语法来一次性创建多个FocusRequester
对象。
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CreateFocusRequesterRefsExample()
val (item1, item2, item3, item4) = remember FocusRequester.createRefs()
val requesterList = listOf(item1, item2, item3, item4)
Column(horizontalAlignment = Alignment.CenterHorizontally)
repeat(4) index ->
var color by remember mutableStateOf(Color.Black)
Box(
Modifier
.size(100.dp)
.clickable requesterList[index].requestFocus()
.border(2.dp, color)
// The focusRequester should be added BEFORE the focusable.
.focusRequester(requesterList[index])
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged color = if (it.isFocused) Color.Red else Color.Black
.focusable()
)
Spacer(modifier = Modifier.height(20.dp))
既然是解构语法,那么一定是通过提供 component1....componentN
语法来实现的,那么 FocusRequester.createRefs()
最多能一次性创建多少个呢? 可以看一下源码:
class FocusRequester
.....
companion object
.....
object FocusRequesterFactory
operator fun component1() = FocusRequester()
operator fun component2() = FocusRequester()
operator fun component3() = FocusRequester()
operator fun component4() = FocusRequester()
operator fun component5() = FocusRequester()
operator fun component6()以上是关于Jetpack Compose中的软键盘与焦点控制的主要内容,如果未能解决你的问题,请参考以下文章
Jetpack Compose 中 BasicTextField 的焦点有多清晰?
Wear OS 上 Jetpack Compose 中的 BasicTextField 问题
如何从 Jetpack Compose TextField 关闭虚拟键盘?