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

El diseño de panel complementario mantiene el enfoque del usuario en el contenido principal de la app y, al mismo tiempo, muestra información complementaria relevante. Por ejemplo, el panel principal puede mostrar detalles sobre una película, mientras que el panel de apoyo enumera películas similares, películas del mismo director o trabajos con los mismos actores.

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

Implementa un panel complementario con un scaffold

NavigableSupportingPaneScaffold es una función de componibilidad que simplifica la implementación de un diseño de panel de apoyo en Jetpack Compose. Une SupportingPaneScaffold y agrega navegación integrada y manejo de atrás predictivo.

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 las ventanas grandes, los paneles principal y complementario aparecen uno al lado del otro.
  • En las ventanas pequeñas, solo se ve un panel a la vez, que cambia a medida que los usuarios navegan.

    Contenido principal que ocupa la mayor parte de la pantalla con contenido complementario al lado.
    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'
  • adaptables: Componentes básicos de bajo nivel, como HingeInfo y Posture

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

  • adaptive-navigation: Son funciones de componibilidad para navegar dentro de los paneles y entre ellos, 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 una 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 habilitar esta opción, agrega android:enableOnBackInvokedCallback="true" a la etiqueta <application> o a las etiquetas <activity> individuales dentro del 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 estará habilitado de forma predeterminada.

Crea un navegador

En las 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 de scaffold para verificar si el panel de apoyo está oculto. De ser así, muestra un botón que llame a navigateTo(SupportingPaneScaffoldRole.Supporting) para mostrar el panel de apoyo.

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

A continuación, se muestra 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")
            }

        }
    }
)

Cómo extraer elementos componibles de panel

Extrae los paneles individuales de un SupportingPaneScaffold en sus propios elementos componibles para que se puedan reutilizar y probar. Usa ThreePaneScaffoldScope para acceder a AnimatedPane si quieres 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 ThreePaneScaffoldValue o ThreePaneScaffoldState por separado. Esta flexibilidad te permite implementar una lógica personalizada para el espaciado de los 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 agregando ThreePaneScaffoldPredictiveBackHandler.

Agregar ThreePaneScaffoldPredictiveBackHandler

Adjunta el controlador de atrás predictivo que toma una instancia de navegador de 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 scaffoldDirective y scaffoldState a SupportingPaneScaffold. Usa la sobrecarga que acepta un ThreePaneScaffoldState y pasa scaffoldNavigator.scaffoldState.

Define los paneles principal y secundario 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) },
)