How to request focus on TextField in Jetpack Compose
์ปดํฌ์ฆ์์๋ FocusRequester๋ฅผ ํตํด TextField์ ํฌ์ปค์ค๋ฅผ ์ค ์ ์์ต๋๋ค.
Modifier.focusRequest()์๋ค๊ฐ FocusRequest ๋ณ์๋ฅผ ์์ฑํด ๋ฃ์ด์ค์ผ๋ก์จ
ํด๋น ์์ ฏ์ ํฌ์ปค์ค๋ฅผ ์ด๋๋ผ ์ ๋๋ผ ํ๋ ์์ฒญ์ ๋ณด๋ผ ์ ์์ต๋๋ค.
val focusRequester = remember { FocusRequester() }
var value by remember { mutableStateOf("hello") }
TextField(
value = value,
onValueChange = {
value = it
},
modifier = Modifier.focusRequester(focusRequester)
)
focusRequester ๋ณ์๋ฅผ remember๋ก ์ ์ธํด์ผ Recomposition์ ํ๋ฉด์ ์ ๊ธฐ๋ฅ์ ํ ์ ์์ด์.
FocusRequester๋ฅผ ํตํด ํ ์ ์๋ ๊ธฐ๋ฅ์ 3๊ฐ์ง ์ ๋๋ค.
Capture Focus
fun captureFocus(): Boolean
ํฌ์ปค์ค๋ฅผ ๊ฝ ์ก๊ณ ์๊ฒ ํ๋ ํจ์์ ๋๋ค.
ํฌ์ปค์ค๊ฐ capture๋๊ณ ์๋ค๋ฉด, ํฌ์ปค์ค ํด์ ๋ฐ ์ด๋์ ํ ์ ์์ต๋๋ค.
Free Focus
fun freeFocus(): Boolean
ํฌ์ปค์ค๊ฐ ๊ฐ ์์ผ๋ฉด, ํฌ์ปค์ค ํด์ ๊ฐ ๊ฐ๋ฅ์ผํ๋ ํจ์์ ๋๋ค.
๋ ํจ์์ ์์๋ฅผ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
์ ๋ ฅ๊ฐ์ ๊ธธ์ด๊ฐ 5 ์ด์์ผ ๋๋ ํฌ์ปค์ค ์ด๋ ๋ถ๊ฐ, ๋ฏธ๋ง์ผ ๋๋ ํฌ์ปค์ค ์ด๋ ๊ฐ๋ฅ์ ๊ฒฝ์ฐ์ ๋๋ค.
val focusRequester1 = remember { FocusRequester() }
val focusRequester2 = remember { FocusRequester() }
var value1 by remember { mutableStateOf("apple") }
var value2 by remember { mutableStateOf("banana") }
TextField(
value = value1,
onValueChange = {
value1 = it.apply {
if (length > 5) focusRequester1.captureFocus() else focusRequester1.freeFocus()
}
},
modifier = Modifier.focusRequester(focusRequester1)
)
TextField(
value = value2,
onValueChange = {
value2 = it
},
modifier = Modifier.focusRequester(focusRequester2)
)
์ฒซ๋ฒ์งธ TextField์ ์ ๋ ฅ๊ฐ์ ๊ธธ์ด๊ฐ 5 ์ด์์ด ๋ ๋,
๋๋ฒ์งธ TextField๋ก ํฌ์ปค์ค๋ฅผ ์ด๋์ํฌ ์ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
Request Focus
fun requestFocus(): Unit
ํฌ์ปค์ค๋ฅผ ์ฃผ๋ ํจ์์ ๋๋ค.
ํด๋น ํจ์๋ฅผ ํธ์ถํ๋ฉด Modifier.focusRequester๋ก ์ง์ ํ ๊ณณ์ ํฌ์ปค์ค๊ฐ ์กํ๊ฒ ๋ฉ๋๋ค.
FocusRequester๋ฅผ ํ๋๋ง ์์ฑํด์ ๋ ๊ณณ์ ๋๋ฉด ์๋๋์?
์ ๋๋ก ๋์ํ์ง ์์ต๋๋ค.
val focusRequester = remember { FocusRequester() }
var input1 by remember { mutableStateOf("1") }
var input2 by remember { mutableStateOf("2") }
LaunchedEffect(true) {
focusRequester.requestFocus()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
TextField(
value = input1,
onValueChange = { input1 = it },
modifier = Modifier.focusRequester(focusRequester)
)
TextField(
value = input2,
onValueChange = { input2 = it },
modifier = Modifier.focusRequester(focusRequester)
)
}
}
์์ ์ฝ๋๋ฅผ ๋น๋ํด๋ณด๋ฉด ๋ฐํ์ ์๋ฌ๋ ๋ฐ์ํ์ง ์์ง๋ง,
focusRequester์๊ฒ ์ฒซ๋ฒ์งธ ํน์ ๋๋ฒ์งธ ํ ์คํธํ๋์ ํฌ์ปค์ค๋ฅผ ์ฃผ๋ผ๊ณ ์์ฒญํ ์๊ฐ ์๊ฒ ๋ฉ๋๋ค.
๊ตฌ๋ถํ ์๊ฐ ์์ผ๋๊น์..
FocusRequester๋ฅผ 2๊ฐ ๋ฃ์ด๋๊ณ , ๋ฒํผ ๋๋ ์ ๋ ๋ค์ focusRequester.requestFocus๋ฅผ ์์ฒญํ๋ฉด ์๋๋์?
val focusRequester1 = remember { FocusRequester() }
val focusRequester2 = remember { FocusRequester() }
var input1 by remember { mutableStateOf("1") }
var input2 by remember { mutableStateOf("2") }
LaunchedEffect(true) {
focusRequester1.requestFocus()
}
Box(
modifier = Modifier.fillMaxSize().padding(top = 20.dp),
contentAlignment = Alignment.TopCenter
) {
Column {
TextField(
value = input1,
onValueChange = { input1 = it },
modifier = Modifier.focusRequester(focusRequester1)
)
TextField(
value = input2,
onValueChange = { input2 = it },
modifier = Modifier.focusRequester(focusRequester2)
)
Button(
onClick = {
focusRequester2.requestFocus()
},
modifier = Modifier.padding(top = 10.dp)
){
Text("focus ์ด๋!")
}
}
}
์์ ๊ฐ์ ์ฝ๋๋ฉด ๋์์ ํ๊ธด ํฉ๋๋ค.
์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ์ธ์ง๋ ๋ชจ๋ฅด๊ฒ ๋ค์,, ๐
๋ณธ๋ฌธ ์๋์ ์ฐ์ฌ์๋ focusManager๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์ต๋๋ค.
์ฌ๋ฌ๊ฐ์ง ์ผ์ด์ค๋ณ๋ก ํฌ์ปค์ค๋ฅผ ์ฃผ๋ ๋ฐฉ๋ฒ์ ์ ์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
1. ํ๋ฉด ์ง์ ์ ํฌ์ปค์ค ์ธ -> ํค๋ณด๋ ์ฌ๋ผ์ค๊ฒ ํ๊ธฐ
val focusRequester = remember { FocusRequester() }
var input by remember { mutableStateOf("") }
LaunchedEffect(focusRequester) {
focusRequester.requestFocus()
}
BasicTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.focusRequester(focusRequester)
)
1๏ธโฃ ํฌ์ปค์ค๋ฅผ ์ฃผ๋ ๋ณ์ (FocusRequester)๋ฅผ ์์ฑํด์ฃผ๊ณ
2๏ธโฃ ํฌ์ปค์ค๊ฐ ํ์ํ TextField ์ Modifier ์์ ๋ณ์๋ฅผ ๋ฃ์ด์ฃผ๊ณ
3๏ธโฃ ํ๋ฉด ์ง์ ์ ํจ์๊ฐ ํธ์ถ๋๋๋ก LaunchedEffect๋ฅผ ํตํด
requestFocus ํจ์๋ฅผ ๋ฃ์ด์ฃผ๋ฉด๋ฉ๋๋ค
LauchedEffect ํ๋ผ๋ฏธํฐ์์ true ๊ฐ ๋ค์ด๊ฐ๋ ๋ฌด๋ฐฉํฉ๋๋ค.
ํ๋ฉด ์์ฑ์ ํ ๋ฒ๋ง ํธ์ถ๋๋ฉด ๋๋๊น์.
ํ์ง๋ง LaunchedEffect ๋ฐ์์ requestFocus๋ฅผ ํ๋ฉด ์๋์ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
Caused by: 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() }
ํด๋น ์์๋ 3๋ฒ์ ์ด์ ์์ ๋ฐ์ํ๋๋ฐ์,
์์ง TextField๊ฐ ๊ทธ๋ ค์ง๊ธฐ ์ (during compostion)์ด๋ผ
Modifier.focusRequester()๊ฐ ๋ค์ด์ค์ง ์์๊ธฐ ๋๋ฌธ์ ๋๋ค.
2. ๊ฐ์ด ์๋ ํ ์คํธ ํ๋์ ํฌ์ปค์ค ์ธ -> ์ปค์ ๋์ผ๋ก ๋ณด๋ด๊ธฐ
TextField์ ๊ฐ์ ๋ฃ์ด์ฃผ๋ฉด์ ํฌ์ปค์ค๋ฅผ ์ฃผ๋ฉด,
Cursor๊ฐ ๋งจ ๋ง์ง๋ง์ผ๋ก ๊ฐ์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ปดํฌ์ฆ์๋ TextFieldValue๋ผ๋ ํจ์๊ฐ ์์ด TextField์์ ํ์ฉํ ์ ์์ต๋๋ค.
์ด TextFieldValue์๋ text์ selection, compostion ์ธ ๊ฐ์ง ํญ๋ชฉ์ด ์กด์ฌํฉ๋๋ค.
@Immutable
class TextFieldValue constructor(
text: String = "",
selection: TextRange = TextRange.Zero,
composition: TextRange? = null
)
๊ทธ๋ฆฌ๊ณ ๊ทธ selection์ ์ปค์๋ฅผ ๋ค๋ฃฐ ์ ์์ด์.
var text by remember { mutableStateOf("hello") }
var input by remember {
mutableStateOf(TextFieldValue(
text = text,
selection = TextRange(text.length))
)
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(true) {
focusRequester.requestFocus()
}
TextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.focusRequester(focusRequester)
)
์ด selection์ TextRange๋ฅผ value์ ๊ธธ์ด ๊ฐ์ผ๋ก ์ค์ ํ๋ฉด ์ปค์๋ฅผ ๋์ผ๋ก ๋ณด๋ผ ์ ์์ต๋๋ค.
+
๋ณ๋ text ๋ณ์ ์์ด selection์ TextRange(Int.MAX_VALUE)๋ก ์ง์ ํด์ค์
์์ธ๋ฆฌ ๋งจ ๋์ผ๋ก ๋ณด๋ด์ฃผ๋ ๋ฐฉ๋ฒ๋ ์๋ค๊ณ ์ฐธ๊ณ ํ์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
3. TextField๊ฐ 2๊ฐ ์ด์์์ ๋,
ํ๋ฉด ์ง์ ์ ์ฒซ๋ฒ์ฌ TextField์ ํฌ์ปค์ค ์ธ -> ์ด๋ฒคํธ์ ๋ค์ TextField๋ก ํฌ์ปค์ค ์ฎ๊ธฐ๊ธฐ
ํ์ฌ ๋ณด์ด๋ ํ๋ฉด์ ํฌ์ปค์ค๋ฅผ ์ ์ดํ๋ ์ธํฐํ์ด์ค๊ฐ ์์ต๋๋ค.
FocusManager์ธ๋ฐ์, Focus manager๊ฐ ํ ์ ์๋ ๊ธฐ๋ฅ์ ํฌ๊ฒ 2๊ฐ์ง ์ ๋๋ค.
clear focus
fun clearFocus(force: Boolean! = false): Unit
ํ๋ฉด์ ์กํ์๋ ํฌ์ปค์ค๋ฅผ ํด์ ์ํต๋๋ค.
ํธ๋ํฐ ๋ฒํธ ์ ๋ ฅํ๋ฉด ๋ฐ๋ก ํค๋ณด๋ ๋ด๋ ค๊ฐ๊ฒ ํ ๋ ํ์ฉํ ์ ์์ต๋๋ค.
val focusManager : FocusManager = LocalFocusManager.current
var input by remember { mutableStateOf("") }
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
TextField(
value = input,
onValueChange = {
input = it.apply {
if (length >= 11) focusManager.clearFocus()
}
}
)
}
move focus
fun moveFocus(focusDirection: FocusDirection!): Boolean
ํฌ์ปค์ค๋ฅผ ๋ค๋ฅธ ๊ณณ(Down, Up, Left, Right ๋ฐฉํฅ)์ผ๋ก ์ด๋์์ผ ์ค๋๋ค.
ํ์๊ฐ์ ์ ์์ด๋๋ฅผ ์ ๋ ฅํ๋ฉด ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ๋๋ก ์ด๋์ํฌ ๋ ํ์ฉํ ์ ์์ต๋๋ค.
val focusManager : FocusManager = LocalFocusManager.current
var input1 by remember { mutableStateOf("1") }
var input2 by remember { mutableStateOf("2") }
Box(
Modifier
.fillMaxSize()
.padding(top = 20.dp),
contentAlignment = Alignment.TopCenter
) {
Column {
TextField(
value = input1,
onValueChange = {
input1 = it
}
)
TextField(
value = input2,
onValueChange = {
input2 = it
}
)
Button(
onClick = { focusManager.moveFocus(FocusDirection.Down) },
modifier = Modifier.padding(top = 10.dp)
) {
Text(
"Focus ์ด๋!"
)
}
}
์ ์ฝ๋๋ ํฌ์ปค์ค๋ฅผ ์๋๋ก ์ด๋์ํค๋ ์์์ ๋๋ค.
FocusDirection.Down์ ์ด์ฉํ์์ต๋๋ค.
๊ทธ๋ฌ๋ฉด move focus๋ฅผ ์ด์ฉํ์ฌ
ํ๋ฉด ์ง์ ์ ์ฒซ๋ฒ์ฌ TextField์ ํฌ์ปค์ค ์ธ -> ์ด๋ฒคํธ์ ๋ค์ TextField๋ก ํฌ์ปค์ค ์ฎ๊ธฐ๊ธฐ ์ ์ํฉ์ ๋ณด๊ฒ ์ต๋๋ค.
val firstFocus = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
var isClicked by remember { mutableStateOf(false) }
LaunchedEffect(firstFocus) {
firstFocus.requestFocus()
}
LaunchedEffect(isClicked) {
if (isClicked) {
focusManager.moveFocus(FocusDirection.Down)
isClicked = false
}
}
var inputId by remember { mutableStateOf("") }
var inputPw by remember { mutableStateOf("") }
Column {
TextField(
value = inputId,
onValueChange = { inputId = it },
modifier = Modifier.focusRequester(firstFocus)
)
Button(
onClick = {
isClicked = true
}
) {
Text(
text = "Focus ์ด๋!",
)
}
TextField(
value = inputPw,
onValueChange = { inputPw = it },
)
}
์ด๋ฒ์๋ ๋ฒํผ์ state๋ฅผ ์ค์ ํด๋น ๋ฒํผ์ด ํด๋ฆญ์ LaunchedEffect๊ฐ ๋์ํ๋๋ก ํ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ทธ์ ํจ๊ป focus manager๋ก ํฌ์ปค์ค๊ฐ ์๋๋ก ์ด๋ํ๋๋ก ํ๋ฉด
๋๋ฒ์งธ TextField๋ก ํฌ์ปค์ค๊ฐ ์ด๋๋๋ ๊ฑธ ํ์ธํ ์ ์์ต๋๋ค.
์ฐธ๊ณ ๋ก TextField์ธ์๋ ํฌ์ปค์ค ์ง์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
modifier = Modifier.focusable(true)
๋ฒํผ์ focusable(true)๋ฅผ ๋ฃ์ด์ฃผ๋ฉด
๋ฒํผ์ ํด๋ฆญํด๋ ์๋์ TextField๋ก ํฌ์ปค์ค๊ฐ ์ด๋ํ์ง ์์ต๋๋ค.
๋ฐ๋๋ก ํฌ์ปค์ค๊ฐ ํ์์๋ ์ปดํฌ๋ํธ์ false๋ฅผ ๋ฃ์ผ๋ฉด
move focusํ ๋ ์ ์ธํ๊ณ ๋์ด๊ฐ ์ ์์ต๋๋ค.
์ฐธ๊ณ
https://developer.android.com/reference/kotlin/androidx/compose/ui/focus/package-summary
'Android > Jetpack Compose' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] Jetpack Compose Navigation ์ ์ฉํด์ ํ๋ฉด ์ด๋ํ๊ธฐ (0) | 2023.07.23 |
---|---|
[Android Compose] TextField์ placeholder ๋ฃ๊ธฐ (0) | 2022.07.15 |