androidandroid-jetpack-composeandroid-jetpack-compose-material3

How to apply paneExpansionDragHandle in ListDetailPaneScaffold?


It's unclear how the paneExpansionDragHandle can be applied.

The code documentation says only this:

paneExpansionDragHandle - provide a custom pane expansion drag handle to allow users to resize panes and change the pane expansion state by dragging. This is null by default, which renders no drag handle. Even there's no drag handle, you can still change pane size directly via modifying paneExpansionState.

The web documentation mentions nothing about that at all.

I tried to use of paneExpansionDragHandle and paneExpansionState to make a custom drag handle, but it was inconvenient. How can I achieve this?


Solution

  • The ListDetailPaneScaffold Composable is part of the material3/adaptive library. You can find useful documentation and samples there.

    You must use the latest material3 dependency to your build.gradle file to get the predefined VerticalDragHandle Composable:

    implementation("androidx.compose.material3.adaptive:adaptive")
    implementation("androidx.compose.material3.adaptive:adaptive-layout")
    implementation("androidx.compose.material3.adaptive:adaptive-navigation")
    //...
    implementation("androidx.compose.material3:material3:1.4.0-alpha04")  // VerticalDragHandle
    

    You then can use the paneExpansionDragHandle like this:

    paneExpansionDragHandle = { state ->
        val interactionSource = remember { MutableInteractionSource() }
        VerticalDragHandle(
            modifier = Modifier.paneExpansionDraggable(
                state,
                LocalMinimumInteractiveComponentSize.current,
                interactionSource
            ),
            interactionSource = interactionSource
        )
    }
    

    Output:

    Screen Recording no snapping

    If you want the drag position to snap to predefined anchors, you can also pass anchors in the paneExpansionState:

    paneExpansionState =
        rememberPaneExpansionState(
            keyProvider = scaffoldNavigator.scaffoldValue,
            anchors = listOf(
                PaneExpansionAnchor.Proportion(0.25f),
                PaneExpansionAnchor.Proportion(0.5f),
                PaneExpansionAnchor.Proportion(0.75f),
            )
        ),
    

    Output:

    Screen Recording with snapping


    Code Sample

    @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
    @Composable
    fun SampleListDetailPaneScaffoldFull() {
        val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
        val coroutineScope = rememberCoroutineScope()
        ListDetailPaneScaffold(
            directive = scaffoldNavigator.scaffoldDirective,
            value = scaffoldNavigator.scaffoldValue,
            listPane = {
                AnimatedPane(
                    modifier = Modifier.preferredWidth(200.dp),
                ) {
                    Surface(
                        color = MaterialTheme.colorScheme.secondary,
                        onClick = {
                            coroutineScope.launch {
                                scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                            }
                        }
                    ) {
                        Text("List")
                    }
                }
            },
            detailPane = {
                AnimatedPane(modifier = Modifier) {
                    Surface(
                        color = MaterialTheme.colorScheme.primary,
                    ) {
                        Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                            Text("Detail")
                            Row(
                                modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
                                horizontalArrangement = Arrangement.spacedBy(8.dp)
                            ) {
                                Surface(
                                    onClick = {
                                        coroutineScope.launch { scaffoldNavigator.navigateBack() }
                                    },
                                    modifier = Modifier.weight(0.5f).fillMaxHeight(),
                                    color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
                                ) {
                                    Box(
                                        modifier = Modifier.fillMaxSize(),
                                        contentAlignment = Alignment.Center
                                    ) {
                                        Text("Previous")
                                    }
                                }
                                VerticalDivider()
                                Surface(
                                    onClick = {
                                        coroutineScope.launch {
                                            scaffoldNavigator.navigateTo(
                                                ListDetailPaneScaffoldRole.Extra
                                            )
                                        }
                                    },
                                    modifier = Modifier.weight(0.5f).fillMaxHeight(),
                                    color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
                                ) {
                                    Box(
                                        modifier = Modifier.fillMaxSize(),
                                        contentAlignment = Alignment.Center
                                    ) {
                                        Text("Next")
                                    }
                                }
                            }
                        }
                    }
                }
            },
            extraPane = {
                AnimatedPane(modifier = Modifier.fillMaxSize()) {
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.tertiary,
                        onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
                    ) {
                        Text("Extra")
                    }
                }
            },
            paneExpansionState =
            rememberPaneExpansionState(
                keyProvider = scaffoldNavigator.scaffoldValue,
                anchors = listOf(
                    PaneExpansionAnchor.Proportion(0.25f),
                    PaneExpansionAnchor.Proportion(0.5f),
                    PaneExpansionAnchor.Proportion(0.75f),
                )
            ),
            paneExpansionDragHandle = { state ->
                val interactionSource = remember { MutableInteractionSource() }
                VerticalDragHandle(
                    modifier =
                    Modifier.paneExpansionDraggable(
                        state,
                        LocalMinimumInteractiveComponentSize.current,
                        interactionSource
                    ),
                    interactionSource = interactionSource
                )
            }
        )
    }