Cómo compilar el diseño de un panel complementario

El diseño de panel complementario mantiene la atención del usuario en el contenido principal de la app mientras muestra información complementaria pertinente. Por ejemplo, el panel principal podría mostrar detalles sobre una película, mientras que el panel complementario muestra películas similares, películas del mismo director o trabajos con los mismos actores.

Para obtener más detalles, consulta los lineamientos del panel complementario de Material 3.

Implementa un panel complementario con un scaffold

NavigableSupportingPaneScaffold es un elemento componible que simplifica la implementación de un diseño de panel complementario en Jetpack Compose. Envuelve SupportingPaneScaffold y agrega navegación integrada y control de retroceso predictivo handling.

Un scaffold de panel complementario admite hasta tres paneles:

  • Panel principal: Muestra el contenido principal.
  • Panel complementario: Proporciona contexto o herramientas adicionales relacionados con el panel principal.
  • Panel adicional (opcional): Se usa para contenido complementario cuando es necesario.

El scaffold se adapta según el tamaño de la ventana:

  • En ventanas grandes, los paneles principal y complementario aparecen uno al lado del otro.
  • En ventanas pequeñas, solo se muestra un panel a la vez, que cambia a medida que los usuarios navegan.

    El contenido principal ocupa la mayor parte de la pantalla, y el contenido complementario se muestra junto a él.
    Figura 1. Diseño de panel complementario

Cómo agregar dependencias

NavigableSupportingPaneScaffold forma parte de la biblioteca de diseño adaptable de Material 3.

Agrega las siguientes tres dependencias relacionadas al archivo build.gradle de tu app o módulo:

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • adaptable: Componentes básicos de bajo nivel, como HingeInfo y Posture

  • adaptive-layout: Diseños adaptables, como ListDetailPaneScaffold y SupportingPaneScaffold

  • adaptive-navigation: Elementos componibles para navegar dentro y entre paneles, así como diseños adaptables que admiten la navegación de forma predeterminada, como NavigableListDetailPaneScaffold y NavigableSupportingPaneScaffold

Verifica que tu proyecto incluya compose-material3-adaptive versión 1.1.0-beta1 o posterior.

Acepta el gesto atrás predictivo

Para habilitar las animaciones de atrás predictivo en Android 15 o versiones anteriores, debes habilitar la compatibilidad con el gesto atrás predictivo. Para habilitarlo, agrega android:enableOnBackInvokedCallback="true" a la etiqueta <application> o a las etiquetas <activity> individuales dentro de tu archivo AndroidManifest.xml.

Una vez que tu app se oriente a Android 16 (nivel de API 36) o versiones posteriores, el gesto atrás predictivo se habilitará de forma predeterminada.

Crea un navegador

En ventanas pequeñas, solo se muestra un panel a la vez, por lo que debes usar un ThreePaneScaffoldNavigator para moverte entre los paneles. Crea una instancia del navegador con rememberSupportingPaneScaffoldNavigator.

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

Pasa el navegador al scaffold

El scaffold requiere un ThreePaneScaffoldNavigator que es una interfaz que representa el estado del scaffold, el ThreePaneScaffoldValue y un PaneScaffoldDirective.

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = { /*...*/ },
    supportingPane = { /*...*/ },
)

El panel principal y el panel complementario son elementos componibles que contienen tu contenido. Usa AnimatedPane para aplicar las animaciones de panel predeterminadas durante la navegación. Usa el valor del scaffold para verificar si el panel complementario está oculto. Si es así, muestra un botón que llame a navigateTo(SupportingPaneScaffoldRole.Supporting) para mostrar el panel complementario.

En el caso de pantallas grandes, usa el ThreePaneScaffoldNavigator.navigateBack() método para descartar el panel complementario y pasar la BackNavigationBehavior.PopUntilScaffoldValueChange constante. Llamar a este método fuerza una recomposición del NavigableSupportingPaneScaffold. Durante la recomposición, verifica la ThreePaneScaffoldNavigator.currentDestination propiedad para determinar si se debe mostrar el panel complementario.

A continuación, se incluye una implementación completa del scaffold:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        AnimatedPane(
            modifier = Modifier
                .safeContentPadding()
                .background(Color.Red)
        ) {
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden) {
                Button(
                    modifier = Modifier
                        .wrapContentSize(),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting)
                        }
                    }
                ) {
                    Text("Show supporting pane")
                }
            } else {
                Text("Supporting pane is shown")
            }
        }
    },
    supportingPane = {
        AnimatedPane(modifier = Modifier.safeContentPadding()) {
            Column {
                // Allow users to dismiss the supporting pane. Use back navigation to
                // hide an expanded supporting pane.
                if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                    // Material design principles promote the usage of a right-aligned
                    // close (X) button.
                    IconButton(
                        modifier =  Modifier.align(Alignment.End).padding(16.dp),
                        onClick = {
                            scope.launch {
                                scaffoldNavigator.navigateBack(backNavigationBehavior)
                            }
                        }
                    ) {
                        Icon(Icons.Default.Close, contentDescription = "Close")
                    }
                }
                Text("Supporting pane")
            }

        }
    }
)

Extrae elementos componibles del panel

Extrae los paneles individuales de un SupportingPaneScaffold en sus propios elementos componibles para que sean reutilizables y se puedan probar. Usa ThreePaneScaffoldScope para acceder a AnimatedPane si deseas las animaciones predeterminadas:

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.MainPane(
    shouldShowSupportingPaneButton: Boolean,
    onNavigateToSupportingPane: () -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedPane(
        modifier = modifier.safeContentPadding()
    ) {
        // Main pane content
        if (shouldShowSupportingPaneButton) {
            Button(onClick = onNavigateToSupportingPane) {
                Text("Show supporting pane")
            }
        } else {
            Text("Supporting pane is shown")
        }
    }
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.SupportingPane(
    scaffoldNavigator: ThreePaneScaffoldNavigator<Any>,
    modifier: Modifier = Modifier,
    backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange,
) {
    val scope = rememberCoroutineScope()
    AnimatedPane(modifier = Modifier.safeContentPadding()) {
        Column {
            // Allow users to dismiss the supporting pane. Use back navigation to
            // hide an expanded supporting pane.
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                // Material design principles promote the usage of a right-aligned
                // close (X) button.
                IconButton(
                    modifier =  modifier.align(Alignment.End).padding(16.dp),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateBack(backNavigationBehavior)
                        }
                    }
                ) {
                    Icon(Icons.Default.Close, contentDescription = "Close")
                }
            }
            Text("Supporting pane")
        }

    }
}

Extraer los paneles en elementos componibles simplifica el uso de SupportingPaneScaffold (compara lo siguiente con la implementación completa del scaffold en la sección anterior):

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)

Si necesitas más control sobre aspectos específicos del scaffold, considera usar SupportingPaneScaffold en lugar de NavigableSupportingPaneScaffold. Esto acepta un PaneScaffoldDirective y un ThreePaneScaffoldValue o ThreePaneScaffoldState por separado. Esta flexibilidad te permite implementar lógica personalizada para el espaciado de paneles y determinar cuántos paneles se deben mostrar de forma simultánea. También puedes habilitar la compatibilidad con el gesto atrás predictivo si agregas ThreePaneScaffoldPredictiveBackHandler.

Agrega ThreePaneScaffoldPredictiveBackHandler

Adjunta el controlador de retroceso predictivo que toma una instancia del navegador del scaffold y especifica el backBehavior. Esto determina cómo se quitan los destinos de la pila de actividades durante la navegación hacia atrás. Luego, pasa el scaffoldDirective y el scaffoldState a SupportingPaneScaffold. Usa la sobrecarga que acepta un ThreePaneScaffoldState y pasa scaffoldNavigator.scaffoldState.

Define los paneles principal y complementario dentro de SupportingPaneScaffold. Usa AnimatedPane para las animaciones de panel predeterminadas.

Después de implementar estos pasos, tu código debería ser similar al siguiente:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

ThreePaneScaffoldPredictiveBackHandler(
    navigator = scaffoldNavigator,
    backBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
)

SupportingPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    scaffoldState = scaffoldNavigator.scaffoldState,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)