androidandroid-jetpack-composeandroid-tvandroid-jetpack-compose-tv

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.


Solution

  • 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"
        )
    }