How to manage Focus state in Jetpack's Compose
Asked Answered
R

4

22

I have a custom composable View (Surface + Text essentially) and I want to change the color of the surface depending on the focus state. The FocusManager#FocusNode is marked internal and I am unaware of any way to achieve this. Is this simply just not available yet? Any one else have to tackle this?

Rowe answered 23/4, 2020 at 18:31 Comment(2)
Do you have just a Text or a TextField?Weekender
I want to change the surface color when focused. It is a custom button composable very similar to the one in the material package.Rowe
G
12

As of dev11, FocusManagerAmbient has been deprecated in favor of FocusModifier. For more examples check out the KeyInputDemo, ComposeInputFieldFocusTransition or FocusableDemo.

@Composable
private fun FocusableText(text: MutableState<String>) {
    val focusModifier = FocusModifier()
    Text(
        modifier = focusModifier
            .tapGestureFilter { focusModifier.requestFocus() }
            .keyInputFilter { it.value?.let { text.value += it; true } ?: false },
        text = text.value,
        color = when (focusModifier.focusState) {
            Focused -> Color.Green
            NotFocused -> Color.Black
            NotFocusable -> Color.Gray
        }
    )
}

Compose has since updated and made the FocusManager members public; although, I'm not exactly sure how final the api is as of dev10. But as of now you can create a FocusNode and listen for changes using FocusManager.registerObserver.

val focusNode = remember {
    FocusNode().apply {
        focusManager.registerObserver(node = this) { fromNode, toNode ->
            if (toNode == this) {
                // has focus
            } else {
                // lost focus
            }
        }
    }
}

If you'd like to gain focus, you can now call FocusManager.requestFocus:

val focusManager = FocusManagerAmbient.current
focusManager.requestFocus(focusNode)

You can also set a focusIdentifier on your FocusNode:

val focusNode = remember {
    FocusNode().apply {
        ...
        focusManager.registerFocusNode("your-focus-identifier", node = this)
    }
}

To gain focus for a specific identifier, you just call FocusManager.requestFocusById

Using that you can easily create a Composable that can provide and request focus for you, for instance:

@Composable
fun useFocus(focusIdentifier: String? = null): Pair<Boolean, () -> Unit> {
    val focusManager = FocusManagerAmbient.current
    val (hasFocus, setHasFocus) = state { false }
    val focusNode = remember {
        FocusNode().apply {
            focusManager.registerObserver(node = this) { fromNode, toNode ->
                setHasFocus(toNode == this)
            }
            focusIdentifier?.let { identifier ->
                focusManager.registerFocusNode(identifier, node = this)
            }
        }
    }
    onDispose {
        focusIdentifier?.let { identifier ->
            focusManager.unregisterFocusNode(identifier)
        }
    }
    return hasFocus to {
        focusManager.requestFocus(focusNode)
    }
}
val (hasFocus, requestFocus) = useFocus("your-focus-identifier")

You could also compose children along with it:

@Composable
fun FocusableTextButton(
    text: String,
    focusedColor: Color = Color.Unset,
    unFocusedColor: Color = Color.Unset,
    textColor: Color = Color.White,
    focusIdentifier: String? = null
) {
    val (hasFocus, requestFocus) = useFocus(focusIdentifier)
    Surface(color = if (hasFocus) focusedColor else unFocusedColor) {
        TextButton(onClick = requestFocus) {
            Text(text = text, color = textColor)
        }
    }
}

Alternatively, there's also FocusModifier, which as of now is:

/**
 * A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
 * different instance of [FocusModifier] for each focusable component.
 *
 * TODO(b/152528891): Write tests for [FocusModifier] after we finalize on the api (API
 * review tracked by b/152529882).
 */

But I don't think you can apply an identifier with it right now.

val focusModifier = FocusModifier()
val hasFocus = focusModifier.focusDetailedState == FocusDetailedState.Active
Surface(
    modifier = focusModifier,
    color = if (hasFocus) focusedColor else unFocusedColor
) {
    TextButton(onClick = { focusModifier.requestFocus() }) {
        Text(text = text, color = textColor)
    }
}

All that being said, I'm not 100% sure this is the intended way to handle focus right now. I referenced CoreTextField a lot to see how it was being handled there.

Example:

@Composable
fun FocusTest() {
    val focusManager = FocusManagerAmbient.current
    val selectRandomIdentifier: () -> Unit = {
        focusManager.requestFocusById(arrayOf("red,", "blue", "green", "yellow").random())
    }
    Column(verticalArrangement = Arrangement.SpaceBetween) {
        FocusableTextButton(
            text = "When I gain focus, I turn red",
            focusedColor = Color.Red,
            focusIdentifier = "red"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn blue",
            focusedColor = Color.Blue,
            focusIdentifier = "blue"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn green",
            focusedColor = Color.Green,
            focusIdentifier = "green"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn yellow",
            focusedColor = Color.Yellow,
            focusIdentifier = "yellow"
        )
        Button(onClick = selectRandomIdentifier) {
            Text(text = "Click me to randomly select a node to focus")
        }
    }
}

gif

Gorga answered 9/5, 2020 at 1:29 Comment(0)
S
29

With 1.0.x you can use the Modifier.onFocusChanged to observe focus state events.
Something like:

var color by remember { mutableStateOf(Black) }
val focusRequester = FocusRequester()

Text(
    modifier = Modifier
        .focusRequester(focusRequester)
        .onFocusChanged { color = if (it.isFocused) Green else Black }
        .focusModifier()
        .pointerInput(Unit) { detectTapGestures { focusRequester.requestFocus() } },
    text = "Text",
    color = color
)
Sealskin answered 2/9, 2020 at 15:4 Comment(2)
Didn't see focusObserver { }. .onFocusChanged {} worked for me. Maybe it was one of the changes before compose hit betaHaiphong
Using .pointerInput gives me an FocusRequester is not initialized exception but using .clickable { focusRequester.requestFocus() } works for meCamillecamilo
G
12

As of dev11, FocusManagerAmbient has been deprecated in favor of FocusModifier. For more examples check out the KeyInputDemo, ComposeInputFieldFocusTransition or FocusableDemo.

@Composable
private fun FocusableText(text: MutableState<String>) {
    val focusModifier = FocusModifier()
    Text(
        modifier = focusModifier
            .tapGestureFilter { focusModifier.requestFocus() }
            .keyInputFilter { it.value?.let { text.value += it; true } ?: false },
        text = text.value,
        color = when (focusModifier.focusState) {
            Focused -> Color.Green
            NotFocused -> Color.Black
            NotFocusable -> Color.Gray
        }
    )
}

Compose has since updated and made the FocusManager members public; although, I'm not exactly sure how final the api is as of dev10. But as of now you can create a FocusNode and listen for changes using FocusManager.registerObserver.

val focusNode = remember {
    FocusNode().apply {
        focusManager.registerObserver(node = this) { fromNode, toNode ->
            if (toNode == this) {
                // has focus
            } else {
                // lost focus
            }
        }
    }
}

If you'd like to gain focus, you can now call FocusManager.requestFocus:

val focusManager = FocusManagerAmbient.current
focusManager.requestFocus(focusNode)

You can also set a focusIdentifier on your FocusNode:

val focusNode = remember {
    FocusNode().apply {
        ...
        focusManager.registerFocusNode("your-focus-identifier", node = this)
    }
}

To gain focus for a specific identifier, you just call FocusManager.requestFocusById

Using that you can easily create a Composable that can provide and request focus for you, for instance:

@Composable
fun useFocus(focusIdentifier: String? = null): Pair<Boolean, () -> Unit> {
    val focusManager = FocusManagerAmbient.current
    val (hasFocus, setHasFocus) = state { false }
    val focusNode = remember {
        FocusNode().apply {
            focusManager.registerObserver(node = this) { fromNode, toNode ->
                setHasFocus(toNode == this)
            }
            focusIdentifier?.let { identifier ->
                focusManager.registerFocusNode(identifier, node = this)
            }
        }
    }
    onDispose {
        focusIdentifier?.let { identifier ->
            focusManager.unregisterFocusNode(identifier)
        }
    }
    return hasFocus to {
        focusManager.requestFocus(focusNode)
    }
}
val (hasFocus, requestFocus) = useFocus("your-focus-identifier")

You could also compose children along with it:

@Composable
fun FocusableTextButton(
    text: String,
    focusedColor: Color = Color.Unset,
    unFocusedColor: Color = Color.Unset,
    textColor: Color = Color.White,
    focusIdentifier: String? = null
) {
    val (hasFocus, requestFocus) = useFocus(focusIdentifier)
    Surface(color = if (hasFocus) focusedColor else unFocusedColor) {
        TextButton(onClick = requestFocus) {
            Text(text = text, color = textColor)
        }
    }
}

Alternatively, there's also FocusModifier, which as of now is:

/**
 * A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
 * different instance of [FocusModifier] for each focusable component.
 *
 * TODO(b/152528891): Write tests for [FocusModifier] after we finalize on the api (API
 * review tracked by b/152529882).
 */

But I don't think you can apply an identifier with it right now.

val focusModifier = FocusModifier()
val hasFocus = focusModifier.focusDetailedState == FocusDetailedState.Active
Surface(
    modifier = focusModifier,
    color = if (hasFocus) focusedColor else unFocusedColor
) {
    TextButton(onClick = { focusModifier.requestFocus() }) {
        Text(text = text, color = textColor)
    }
}

All that being said, I'm not 100% sure this is the intended way to handle focus right now. I referenced CoreTextField a lot to see how it was being handled there.

Example:

@Composable
fun FocusTest() {
    val focusManager = FocusManagerAmbient.current
    val selectRandomIdentifier: () -> Unit = {
        focusManager.requestFocusById(arrayOf("red,", "blue", "green", "yellow").random())
    }
    Column(verticalArrangement = Arrangement.SpaceBetween) {
        FocusableTextButton(
            text = "When I gain focus, I turn red",
            focusedColor = Color.Red,
            focusIdentifier = "red"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn blue",
            focusedColor = Color.Blue,
            focusIdentifier = "blue"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn green",
            focusedColor = Color.Green,
            focusIdentifier = "green"
        )
        FocusableTextButton(
            text = "When I gain focus, I turn yellow",
            focusedColor = Color.Yellow,
            focusIdentifier = "yellow"
        )
        Button(onClick = selectRandomIdentifier) {
            Text(text = "Click me to randomly select a node to focus")
        }
    }
}

gif

Gorga answered 9/5, 2020 at 1:29 Comment(0)
S
8

this code works for me

var haveFocus by remember { mutableStateOf(false) }
Surface(
    modifier = Modifier.onFocusEvent {
        haveFocus = it.isFocused
    },
    shape = RectangleShape
){...}
Skyscape answered 19/3, 2021 at 1:29 Comment(0)
E
1

This answer is base on Gabriele Mariotti answer but I have change

  • Use remember { FocusRequester() } to prevent crash FocusRequester is not initialized after do 2nd click on Text
  • Use focusTarget instead of focusModifier because deprecated

.

var color by remember { mutableStateOf(Color.Black) }
val focusRequester = remember { FocusRequester() }

Text(
    text = "Hello",
    modifier = Modifier
        .focusRequester(focusRequester)
        .onFocusChanged { color = if (it.isFocused) Color.Green else Color.Black }
        .focusTarget()
        .pointerInput(Unit) { detectTapGestures { focusRequester.requestFocus() } }
)
Epidote answered 12/1, 2022 at 3:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.