google / horologist

Horologist is a group of libraries that aim to supplement Wear OS developers with features that are commonly required by developers but not yet available.
https://google.github.io/horologist/
Apache License 2.0
565 stars 93 forks source link

rotaryWithScroll in Dialogs #2092

Closed Pezcraft closed 7 months ago

Pezcraft commented 7 months ago

I was able to make rotaryWithScroll work on my screen, but it doesn't scroll in my dialog. I experimented a lot with multiple Focusrequesters, but I either get a not initialized exception or it just doesn't scroll using the crown. Can someone provide me with information on how to do this?

@OptIn(ExperimentalHorologistApi::class,
    ExperimentalWearFoundationApi::class
)
@Composable
fun MainScreen(
    modifier: Modifier = Modifier,
) {
    val focusRequester = rememberActiveFocusRequester()
    val columnState = rememberColumnState()

    val showDialog = remember { mutableStateOf(false) }
    val dialogScrollState = rememberScalingLazyListState()

    Scaffold(
        modifier = modifier,
    ) {
        ScalingLazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .rotaryWithScroll(columnState, focusRequester),
            state = columnState.state
        ) {
            // ...
        }

        Dialog(
            // modifier = Modifier.rotaryWithScroll(dialogScrollState, focusRequester),
            showDialog = showDialog.value,
            onDismissRequest = { showDialog.value = false },
            scrollState = dialogScrollState,
        ) {
            Alert(
                modifier = Modifier.rotaryWithScroll(dialogScrollState, focusRequester),
                scrollState = dialogScrollState,
                icon = {},
                title = {},
                negativeButton = {},
                positiveButton = {},
            ) {
                // ...
            }
        }
    }
}
yschimke commented 7 months ago

You are defining it in the wrong scope. Or rather you need a focusRequester for each scrollable thing, and in the right scope. The active bit of the name, means that it follows the focus lifecycle of the Composable. But you have it at the screen level. It should be inside the Dialog.

@OptIn(ExperimentalHorologistApi::class,
    ExperimentalWearFoundationApi::class
)
@Composable
fun MainScreen(
    modifier: Modifier = Modifier,
) {
    val focusRequester = rememberActiveFocusRequester()
    val columnState = rememberColumnState()

    val showDialog = remember { mutableStateOf(false) }

    Scaffold(
        modifier = modifier,
    ) {
        ScalingLazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .rotaryWithScroll(columnState, focusRequester),
            state = columnState.state
        ) {
            // ...
        }

        Dialog(
            // modifier = Modifier.rotaryWithScroll(dialogScrollState, focusRequester),
            showDialog = showDialog.value,
            onDismissRequest = { showDialog.value = false },
            scrollState = dialogScrollState,
        ) {
            val dialogFocusRequester = rememberActiveFocusRequester()
            val dialogScrollState = rememberScalingLazyListState()
            Alert(
                modifier = Modifier.rotaryWithScroll(dialogScrollState, dialogFocusRequester),
                scrollState = dialogScrollState,
                icon = {},
                title = {},
                negativeButton = {},
                positiveButton = {},
            ) {
                // ...
            }
        }
    }
}
yschimke commented 7 months ago

Horologist has some nicer APIs that do all this for you, AlertDialog and Confirmation.

            AlertDialog(
                showDialog = showDialog.value,
                icon = {},
                title = "Title",
                onCancel = { showDialog.value = false },
                onOk = { showDialog.value = false },
            ) {
                item {
                    Text(text = "Abc Abc Abc Abc\ndef def def\nghi ghi ghi")
                }
            }
Digipom commented 7 months ago

@yschimke Thank you, I was able to fix it like this:

Dialog(showDialog = showDialog, onDismissRequest = onDialogDismissed) {
        val scrollState = rememberScalingLazyListState()
        Alert(
            modifier = Modifier
                .fillMaxSize()
                .rotaryWithScroll(
                    focusRequester = rememberActiveFocusRequester(),
                    scrollableState = scrollState
                ),
            scrollState = scrollState,
// ...

As a feature request, if we could have an "opinionated" version that does the same thing as Alert does currently (lets us provide our own buttons and our own composables for title and message, that it wraps itself using CompositionLocalProvider etc...) it would allow me to use AlertDialog instead of Dialog / Alert.

yschimke commented 7 months ago

cc @BowerSteve thoughts? The original issue is how to make Wear Compose dialogs work with RSB. The fix is Horologist combined APIs, or Dialog + Alert combos. There isn't a working slot based single API.

I'm not sure it's worth the limitations. You can combine these as you show.

The Horologist material dialogs take strings, in order to follow internal guidance on Material Dialogs, on different screen sizes. Even measuring text length.

https://github.com/google/horologist/blob/c25f4073428789d249f8d9653ea06e898c2c7503/compose-material/src/main/java/com/google/android/horologist/compose/material/AlertDialog.kt#L125

The other benefit is the middle part, Alert in your example or AlertContent for the Horologist ones are easily used in Preview or Screenshot tests.