Nikita Kalugin
Nikita Kalugin

Reputation: 742

ComposeView steals focus from content in AndroidTV

I'm trying to use Jetpack Compose in my existing AndroidTV App. I need to make a button with microphone icon which will change its color if it's focused. Like this^

Unfocused

enter image description here

Focused

enter image description here

Here's my ComposeView

<androidx.compose.ui.platform.ComposeView
    android:id="@+id/micBtn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

And here the code in my fragment

binding.micBtn.setContent {
    var buttonResId by remember { mutableStateOf(R.drawable.speech_recognition_button_unfocused) }
    IconButton(
        modifier = Modifier
            .size(60.dp)
            .onFocusChanged {
                buttonResId = if (it.isFocused) {
                    R.drawable.speech_recognition_button_focused
                } else {
                    R.drawable.speech_recognition_button_unfocused
                }
            },
        onClick = onClick,
    ) {
        Icon(
            painter = painterResource(id = buttonResId),
            contentDescription = null,
            tint = Color.Unspecified,
        )
    }
}

Looks good, right? The problem is when I try to focus on this button focus first goes to AndroidComposeView item (according to my GlobalFocusListener). And only my second action (click, D-Pad navigation) makes my content focused.

So, for some reason internal AndroidComposeView steals focus from my Content

Is there any way to prevent this behaviour? I need to focus only on my content, not AndroidComposeView wrapper.

Upvotes: 4

Views: 1739

Answers (1)

vighnesh153
vighnesh153

Reputation: 5416

Update (March 6, 2024):

This issue has been addressed in aosp/2813125 and the following hack shouldn't be required anymore. Just update to the latest version of Compose UI (1.7.x).


First draft

This is a known issue where moving focus from outside of a ComposeView to inside the ComposeView needs 2 inputs from the user instead of just 1: b/292432034

You can create an extension function which can transfer the focus to the child using just 1 input from the user.

fun ComposeView.setFocusableContent(content: @Composable () -> Unit) {
    isFocusable = true
    isFocusableInTouchMode = true
    val focusRequester = FocusRequester()

    onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
        if (hasFocus) focusRequester.requestFocus()
    }

    setContent {
        Box(modifier = Modifier.focusRequester(focusRequester).focusGroup()) {
            content.invoke()
        }
    }
}

Usage is pretty straight forward. Instead of using setContent, you can now use setFocusableContent.

binding.micBtn.setFocusableContent {
  // ...
}

Unrelated to the question

To react to focus changes in your component, you can make use of IconButton from androidx.tv.material3 library. By default, it changes color when the button is focused and it is easy to change the colors for different states (focused, pressed, etc.) using the colors parameter.

Usage:

androidx.tv.material3.IconButton(onClick = { }) {
    Icon(
        Icons.Filled.Mic, 
        contentDescription = "Mic"
    )
}

Upvotes: 6

Related Questions