Posturas del foldable
# Un foldable puede estar en varias posturas al mismo tiempo:
# FLAT (extendido completamente)
# → La pantalla está completamente abierta y plana
# → Comportarse como tablet — usar layout expandido
# HALF_OPENED (doblado a ~90°)
# → El dispositivo está parcialmente doblado
# → Dos sub-modos según la orientación del pliegue:
# → LANDSCAPE + HALF_OPENED = modo mesa (tabletop mode)
# → PORTRAIT + HALF_OPENED = modo libro (book mode)
# Dispositivos con pantalla exterior + interior (ej: Galaxy Z Fold):
# → Pantalla exterior cerrada: layout de teléfono compact
# → Pantalla interior extendida: layout de tablet expanded
# → La app recibe un onChange de WindowSizeClass al abrir/cerrar
WindowInfoTracker — observar el estado del foldable
// implementation("androidx.window:window:1.3.0")
// Observar los cambios de layout del dispositivo:
@Composable
fun FoldableAwareLayout() {
val activity = LocalContext.current as ComponentActivity
// Observar el estado de las pantallas divididas y bisagra
val layoutInfo by activity.windowInfoTracker
.windowLayoutInfo(activity)
.collectAsStateWithLifecycle(initialValue = null)
// DisplayFeatures son los elementos físicos que interrumpen la pantalla
// (bisagras, cámaras bajo pantalla, etc.)
val foldingFeature = layoutInfo?.displayFeatures
?.filterIsInstance<FoldingFeature>()
?.firstOrNull()
when {
foldingFeature == null -> {
// Tablet plana o teléfono normal — layout estándar
LayoutEstandar()
}
foldingFeature.state == FoldingFeature.State.HALF_OPENED
&& foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL -> {
// Modo mesa — bisagra horizontal, dispositivo en landscape
LayoutModoMesa(foldingFeature)
}
foldingFeature.state == FoldingFeature.State.HALF_OPENED
&& foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL -> {
// Modo libro — bisagra vertical, dispositivo en portrait
LayoutModoLibro(foldingFeature)
}
foldingFeature.state == FoldingFeature.State.FLAT -> {
// Extendido completamente — layout de tablet
LayoutTablet()
}
}
}
La bisagra (hinge) — zona a evitar
La bisagra interrumpe físicamente la pantalla. Poner contenido importante sobre ella hace que quede oculto o distorsionado. La API da la posición exacta para evitarla:
@Composable
fun LayoutConBisagra(foldingFeature: FoldingFeature) {
// foldingFeature.bounds: el rectángulo exacto que ocupa la bisagra en pixels
val hingeOffset = with(LocalDensity.current) {
foldingFeature.bounds.top.toDp()
}
// Dividir el layout exactamente en la bisagra:
Column {
// Panel superior — hasta la bisagra
Box(
modifier = Modifier
.fillMaxWidth()
.height(hingeOffset)
) {
ContenidoPanelSuperior()
}
// La bisagra — espacio vacío, no poner nada aquí
Spacer(modifier = Modifier.height(
with(LocalDensity.current) {
(foldingFeature.bounds.bottom - foldingFeature.bounds.top).toDp()
}
))
// Panel inferior
Box(modifier = Modifier.fillMaxSize()) {
ContenidoPanelInferior()
}
}
}
// Para bisagra vertical (modo libro) — dividir horizontalmente:
// El mismo concepto pero con Row en lugar de Column
Modo mesa — el caso de uso más rico
En modo mesa el dispositivo está apoyado como una laptop. La parte superior de la pantalla es el "monitor" y la inferior es el "teclado". Es el modo con más potencial para apps de productividad:
@Composable
fun LayoutModoMesa(foldingFeature: FoldingFeature) {
// La bisagra es horizontal — dividir la pantalla en top/bottom
val hingeTopDp = with(LocalDensity.current) { foldingFeature.bounds.top.toDp() }
Column(modifier = Modifier.fillMaxSize()) {
// Panel superior: contenido principal (video, mapa, documento)
Box(
modifier = Modifier
.fillMaxWidth()
.height(hingeTopDp)
) {
// Ej: reproductor de video, preview de cámara, mapa
ContenidoPrincipal()
}
// Panel inferior: controles, teclado virtual, lista de opciones
Box(modifier = Modifier.fillMaxSize()) {
// Ej: controles de reproducción, lista de pistas, opciones
ControlesModoMesa()
}
}
}
// Casos de uso reales para modo mesa:
// → App de video: video arriba, controles abajo
// → App de recetas: ingredientes arriba, pasos arriba, timer abajo
// → Video llamada: cámara arriba, chat / botones abajo
// → Juego: campo de juego arriba, controles abajo
Modo libro — dos páginas en paralelo
@Composable
fun LayoutModoLibro(foldingFeature: FoldingFeature) {
// La bisagra es vertical — dividir la pantalla en left/right
val hingeLeftDp = with(LocalDensity.current) { foldingFeature.bounds.left.toDp() }
Row(modifier = Modifier.fillMaxSize()) {
// Panel izquierdo
Box(modifier = Modifier.width(hingeLeftDp).fillMaxHeight()) {
PanelIzquierdo()
}
// Panel derecho (ocupa el resto)
Box(modifier = Modifier.fillMaxSize()) {
PanelDerecho()
}
}
}
// Casos de uso para modo libro:
// → Lector: tabla de contenidos izquierda, contenido derecha
// → Email: lista de emails izquierda, detalle derecha (list-detail)
// → Mapa + lista de lugares
Simplificar con Compose Adaptive
// La librería adaptive de Material3 provee FoldableLayout
// que maneja automáticamente el posicionamiento respecto a la bisagra:
// implementation("androidx.compose.material3.adaptive:adaptive:1.0.0")
@Composable
fun LayoutAdaptativoConBisagra() {
val windowInfo = currentWindowAdaptiveInfo()
// ThreePaneScaffold maneja automáticamente los tres paneles
// y la bisagra en foldables
ListDetailPaneScaffold(
directive = calculatePaneScaffoldDirective(windowInfo),
value = rememberListDetailPaneScaffoldState().scaffoldValue,
listPane = { ListaItems() },
detailPane = { DetalleItem() }
)
// En teléfono: muestra solo lista o solo detalle (con back navigation)
// En tablet/foldable extendido: muestra ambos en paralelo
// En modo libro: separa exactamente en la bisagra
}