I am working on an Google TV/Android TV app. The f...
# compose
t
I am working on an Google TV/Android TV app. The focus system is only working when i add the focusOrder modifier to the items like in this code: https://cs.android.com/androidx/platform/frameworks/support/+/0a6f6ab5001b4885cf6f47[…]/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt. Which is not really practical when working with Lazy lists.
Also it looks like hardware keyevent are still not support (beta02)
a
@Ralston Da Silva
t
For simple screen with only 2 buttons i do have a working solution now. But it is much boilerplate code here. Maybe some one has simpler solution?
Copy code
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TvStart(googleSignIn: KGoogleSignIn, onClick: () -> Unit) {
    val signedIn by googleSignIn.signedIn

    val (signInOutBtn, startBtn) = FocusRequester.createRefs()

    DisposableEffect(Unit) {
        signInOutBtn.requestFocus()
        onDispose { }
    }
    Column {
        val onSignInClick = {
            if (signedIn) {
                googleSignIn.signOut()
            } else {
                googleSignIn.signIn()
            }
        }
        ClickableBox(
            modifier = Modifier
                .focusOrder(signInOutBtn) {
                    next = startBtn
                    down = startBtn
                },
            onClick = onSignInClick
        ) { isFocused ->
            TextButton(onClick = onSignInClick) {
                val buttonText = if (signedIn) "Sign out" else "Sign in"
                Text(buttonText, color = if (isFocused) Color.White else Color.Blue)
            }
        }
        if (signedIn) {
            ClickableBox(
                modifier = Modifier.focusOrder(startBtn) {
                    previous = signInOutBtn
                    up = signInOutBtn
                },
                onClick = onClick
            ) { isFocused ->
                TextButton(onClick = onClick) {
                    Text("Check photos", color = if (isFocused) Color.White else Color.Blue)
                }
            }
        }
    }
}

@Composable
private fun ClickableBox(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    content: @Composable BoxScope.(isFocused: Boolean) -> Unit
) {
    var isFocused by remember { mutableStateOf(false) }
    Box(
        modifier = modifier
            .onKeyEvent {
                log("$it")
                if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.DirectionCenter)) {
                    onClick?.invoke()
                    true
                } else {
                    false
                }
            }
            .onFocusChanged { isFocused = it.isFocused }
            .focusModifier(),
        content = { content(isFocused) }
    )
}
It only works when you specify the focusOrder for every direction.
w
OK, I’m having the same behavior
I’ve tried listening to key events and requesting focus myself, but I’m not sure how to determine if something already is focused or not
t
Yes i do have exactly the same problems. I think this is all still in progress. So it should be fixed in future. Maybe i will than go back and capture the key events on activity level and forward the events to the UI.
w
Found an (ugly) workaround. I added a root element of full width and height with a focus modifier that always moves to the desired initial focus:
Copy code
modifier = Modifier
            .focusOrder(tapOutside) {
                up = initial
                right = initial
                down = initial
                left = initial
            }
By initializing focus to the root element, nothing is focused. When the user clicks an arrow, focus is moved to the initial element. I then added a
.pointerInput
listener to the root element to recapture focus to the root element whenever the user taps somewhere where focus isn’t captured. I received an
FocusRequester is not initialized
error if I ran requestFocus directly from
pointerInput
, so I used a mutable state variable to run it with the
DisposableEffect
composable:
Copy code
var isDpadNavigationHidden by remember { mutableStateOf(true) }

...

.pointerInput(Unit) {
  detectTapGestures {
    isDpadNavigationHidden = true
  }
}

...

DisposableEffect(isDpadNavigationHidden) {
  if (isDpadNavigationHidden) {
    tapOutside.requestFocus()
    isDpadNavigationHidden = false
  }
  onDispose { }
}
🤔 1
Here’s a demo: https://github.com/thesauri/jetpack-compose-dpad-navigation Alternative solutions are welcome 😄
442 Views