Inserciones de ventana en Compose

La plataforma de Android se encarga de diseñar la IU del sistema, como la barra de estado y la de navegación. Esta IU del sistema se muestra sin importar qué app use el usuario. WindowInsets proporciona información sobre la IU del sistema para garantizar que tu app se dibuje en el área correcta y que la IU del sistema no oculte la IU.

De borde a borde para dibujar detrás de las barras del sistema
Figura 1: De borde a borde para dibujar detrás de las barras del sistema

De forma predeterminada, la IU de tu app solo puede diseñarse dentro de la IU del sistema, como la barra de estado y la barra de navegación. De esta manera, se garantiza que los elementos de la IU del sistema no oculten el contenido de tu app.

Sin embargo, recomendamos que las apps acepten mostrarse en estas áreas donde también se muestra la IU del sistema, lo que genera una experiencia del usuario más fluida y permite que tu app aproveche al máximo el espacio de ventana disponible. Esto también permite que las apps se animen junto con la IU del sistema, en especial cuando se muestra y se oculta el teclado en pantalla.

La habilitación de la visualización en estas regiones y el contenido detrás de la IU del sistema se denomina desarrollo de borde a borde. En esta página, aprenderás sobre los diferentes tipos de inserciones, cómo habilitar el uso de borde a borde y cómo usar las APIs de inserción para animar tu IU y evitar oscurecer partes de tu app.

Fundamentos de la inserción

Cuando una app pasa de borde a borde, debes asegurarte de que la IU del sistema no oculte el contenido ni las interacciones importantes. Por ejemplo, si se coloca un botón detrás de la barra de navegación, es posible que el usuario no pueda hacer clic en él.

El tamaño de la IU del sistema y la información sobre el lugar donde se coloca se especifican mediante las inserciones.

Cada parte de la IU del sistema tiene un tipo correspondiente de inserción que describe su tamaño y dónde se coloca. Por ejemplo, las inserciones de la barra de estado proporcionan el tamaño y la posición de la barra de estado, mientras que las inserciones de la barra de navegación proporcionan el tamaño y la posición. Cada tipo de inserción consta de cuatro dimensiones de píxeles: superior, izquierda, derecha e inferior. Estas dimensiones especifican qué tan lejos se extiende la IU del sistema desde los lados correspondientes de la ventana de la app. Por lo tanto, para evitar la superposición con ese tipo de IU del sistema, la IU de la app debe insertarse en esa cantidad.

Estos tipos de inserciones de Android integrados están disponibles a través de WindowInsets:

WindowInsets.statusBars

Las inserciones que describen las barras de estado Estas son las barras superiores de la IU del sistema que contienen íconos de notificaciones y otros indicadores.

WindowInsets.statusBarsIgnoringVisibility

La barra de estado se inserta cuando están visibles. Si las barras de estado están ocultas (debido al ingreso al modo de pantalla completa envolvente), las inserciones de la barra de estado principal estarán vacías, pero no estarán vacías.

WindowInsets.navigationBars

Las inserciones que describen las barras de navegación Son las barras de la IU del sistema ubicadas en el lado izquierdo, derecho o inferior del dispositivo, que describen la barra de tareas o los íconos de navegación. Estas pueden cambiar en el tiempo de ejecución según el método de navegación preferido del usuario y la interacción con la barra de tareas.

WindowInsets.navigationBarsIgnoringVisibility

La barra de navegación se inserta cuando están visibles. Si las barras de navegación están ocultas (debido al ingreso al modo de pantalla completa envolvente), las inserciones de la barra de navegación principal estarán vacías, pero estas no estarán vacías.

WindowInsets.captionBar

Es la inserción que describe la decoración de la ventana de la IU del sistema si se encuentra en una ventana de formato libre, como la barra de título superior.

WindowInsets.captionBarIgnoringVisibility

La barra de subtítulos se inserta cuando están visibles. Si las barras de subtítulos están ocultas actualmente, las inserciones de la barra de subtítulos principal estarán vacías, pero estas no lo estarán.

WindowInsets.systemBars

La unión de las inserciones de la barra del sistema, que incluyen las barras de estado, las barras de navegación y la barra de subtítulos.

WindowInsets.systemBarsIgnoringVisibility

Las inserciones de la barra del sistema cuando están visibles Si las barras del sistema están ocultas (debido al ingreso al modo de pantalla completa envolvente), las inserciones de la barra del sistema principal estarán vacías, pero estas no estarán vacías.

WindowInsets.ime

Las inserciones que describen la cantidad de espacio en la parte inferior que ocupa el teclado en pantalla.

WindowInsets.imeAnimationSource

Las inserciones que describen la cantidad de espacio que ocupó el teclado en pantalla antes de la animación actual del teclado

WindowInsets.imeAnimationTarget

Las inserciones que describen la cantidad de espacio que ocupará el teclado en pantalla después de la animación actual del teclado.

WindowInsets.tappableElement

Es un tipo de inserciones que describen información más detallada sobre la IU de navegación, lo que proporciona la cantidad de espacio en el que el sistema controlará los "toques" y no la app. En el caso de las barras de navegación transparentes con navegación por gestos, algunos elementos de la app se pueden presionar a través de la IU de navegación del sistema.

WindowInsets.tappableElementIgnoringVisibility

Las inserciones del elemento que se puede presionar se muestran cuando son visibles. Si los elementos que se pueden presionar están ocultos (debido al ingreso al modo de pantalla completa envolvente), las inserciones del elemento táctil principal estarán vacías, pero estas no estarán vacías.

WindowInsets.systemGestures

Las inserciones que representan la cantidad de inserciones en las que el sistema interceptará los gestos para la navegación. Las apps pueden especificar manualmente el control de una cantidad limitada de estos gestos mediante Modifier.systemGestureExclusion.

WindowInsets.mandatorySystemGestures

Es un subconjunto de gestos del sistema que siempre controlará el sistema y que no se puede inhabilitar mediante Modifier.systemGestureExclusion.

WindowInsets.displayCutout

Las inserciones que representan la cantidad de espaciado necesario para evitar la superposición con un corte de pantalla (muesca o agujero).

WindowInsets.waterfall

Las inserciones que representan las áreas curvas de una cascada. La pantalla de cascada tiene áreas curvas a lo largo de los bordes de la pantalla donde la pantalla comienza a envolverse a los lados del dispositivo.

Estos tipos se resumen en tres tipos de inserción "seguros" que garantizan que el contenido no esté oscurecido:

Estos tipos de inserciones "seguras" protegen el contenido de diferentes maneras, según las inserciones subyacentes de la plataforma:

  • Usa WindowInsets.safeDrawing para proteger el contenido que no se deba dibujar debajo de una IU del sistema. Este es el uso más común de las inserciones: para evitar dibujar contenido que está oculto por la IU del sistema (ya sea de forma parcial o total).
  • Usa WindowInsets.safeGestures para proteger el contenido con gestos. De esta manera, se evita que los gestos del sistema no coincidan con los de las apps (como los de hojas inferiores, carruseles o juegos).
  • Usa WindowInsets.safeContent como una combinación de WindowInsets.safeDrawing y WindowInsets.safeGestures para asegurarte de que el contenido no tenga superposición visual ni de gestos.

Configuración de inserciones

Para permitir que tu app controle por completo dónde dibuja contenido, sigue estos pasos de configuración. Sin estos pasos, tu app podría dibujar colores negros o sólidos detrás de la IU del sistema, o no animarse de forma síncrona con el teclado en pantalla.

  1. Llama a enableEdgeToEdge() en Activity.onCreate. Esta llamada solicita que tu app muestre detrás de la IU del sistema. Luego, la app controlará cómo se usan esas inserciones para ajustar la IU.
  2. Establece android:windowSoftInputMode="adjustResize" en la entrada AndroidManifest.xml de tu actividad. Esta configuración permite que tu app reciba el tamaño del IME del software como inserciones, que puedes usar para rellenar y diseñar el contenido de manera correcta cuando el IME aparezca y desaparezca en tu app.

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

APIs de Compose

Una vez que tu actividad haya tomado el control de todas las inserciones, puedes usar las APIs de Compose para asegurarte de que el contenido no se oculte y los elementos interactivos no se superpongan con la IU del sistema. Estas APIs también sincronizan el diseño de tu app con los cambios de inserción.

Por ejemplo, este es el método más básico para aplicar las inserciones al contenido de toda la app:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

Este fragmento aplica las inserciones de ventana safeDrawing como relleno alrededor de todo el contenido de la app. Si bien esto garantiza que los elementos interactivos no se superpongan con la IU del sistema, también significa que ninguna de las apps se dibujará detrás de la IU del sistema para lograr un efecto de borde a borde. Para aprovechar al máximo toda la ventana, debes ajustar dónde se aplican las inserciones en pantalla por pantalla o componente por componente.

Todos estos tipos de inserciones se animan automáticamente con animaciones de IME con portabilidad a versiones anteriores al nivel de API 21. Por extensión, todos los diseños que usan estas inserciones también se animan automáticamente a medida que cambian los valores de las inserciones.

Existen dos formas principales de usar estos tipos de inserciones para ajustar tus diseños componibles: modificadores de padding y modificadores de tamaño de inserción.

Modificadores de padding

Modifier.windowInsetsPadding(windowInsets: WindowInsets) aplica las inserciones de ventana determinadas como relleno y actúa de la misma manera que lo haría Modifier.padding. Por ejemplo, Modifier.windowInsetsPadding(WindowInsets.safeDrawing) aplica las inserciones de dibujo seguras como relleno en los 4 lados.

También hay varios métodos de utilidades incorporados para los tipos de inserción más comunes. Modifier.safeDrawingPadding() es uno de esos métodos, que equivale a Modifier.windowInsetsPadding(WindowInsets.safeDrawing). Existen modificadores análogos para los otros tipos de inserción.

Modificadores de tamaño de inserción

Los siguientes modificadores aplican una cantidad de inserciones de ventana configurando el tamaño del componente como el de las inserciones:

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

Aplica el lado inicial de windowInsets como el ancho (como Modifier.width).

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

Aplica el lado final de windowInsets como el ancho (como Modifier.width).

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

Aplica el lado superior de windowInsets como altura (como Modifier.height).

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

Aplica el lado inferior de windowInsets como la altura (como Modifier.height).

Estos modificadores son especialmente útiles para ajustar el tamaño de una Spacer que ocupa el espacio de las inserciones:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

Consumo de inserción

Los modificadores de padding de las inserciones (windowInsetsPadding y asistentes como safeDrawingPadding) consumen automáticamente la parte de las inserciones que se aplican como padding. Mientras profundizas en el árbol de composición, los modificadores de padding de inserción anidados y los modificadores de tamaño de inserción saben que los modificadores de padding externos ya consumieron alguna parte de las inserciones y evitan usar la misma parte de las inserciones más de una vez, lo que generaría demasiado espacio adicional.

Los modificadores de tamaño de inserción también evitan usar la misma parte de las inserciones más de una vez si las inserciones ya se consumieron. Sin embargo, como cambian su tamaño directamente, no consumen las inserciones.

Como resultado, los modificadores de padding anidados cambian automáticamente la cantidad de padding que se aplica a cada elemento componible.

En el mismo ejemplo de LazyColumn que antes, el modificador imePadding cambia el tamaño de LazyColumn. Dentro de LazyColumn, el último elemento tiene el tamaño de la altura de la parte inferior de las barras del sistema:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

Cuando se cierra el IME, el modificador imePadding() no aplica padding, ya que el IME no tiene altura. Dado que el modificador imePadding() no aplica padding, no se consumen inserciones, y la altura de Spacer será el tamaño de la parte inferior de las barras del sistema.

Cuando se abre el IME, las inserciones de IME se animan para que coincidan con el tamaño del IME, y el modificador imePadding() comienza a aplicar el padding inferior para cambiar el tamaño del LazyColumn a medida que se abre. Cuando el modificador imePadding() comienza a aplicar el padding inferior, también comienza a consumir esa cantidad de inserciones. Por lo tanto, la altura de Spacer comienza a disminuir, dado que el modificador imePadding() ya aplicó el espaciado para las barras del sistema. Una vez que el modificador imePadding() aplica una cantidad de padding inferior mayor que las barras del sistema, la altura de Spacer es cero.

Cuando se cierra el IME, los cambios ocurren a la inversa: Spacer comienza a expandirse desde una altura de cero una vez que imePadding() aplica menos que el lado inferior de las barras del sistema hasta que, finalmente, Spacer coincide con la altura de la parte inferior de las barras del sistema una vez que el IME se anima por completo.

Figura 2: Columna diferida de borde a borde con TextField

Este comportamiento se logra mediante la comunicación entre todos los modificadores windowInsetsPadding, y puede verse influenciado de otras maneras.

Modifier.consumeWindowInsets(insets: WindowInsets) también consume las inserciones de la misma manera que Modifier.windowInsetsPadding, pero no aplica las inserciones consumidas como padding. Esto es útil en combinación con los modificadores de tamaño de inserción para indicar a los elementos del mismo nivel que ya se consumió una cierta cantidad de inserciones:

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues) se comporta de forma muy similar a la versión con un argumento WindowInsets, pero toma un PaddingValues arbitrario para consumirlo. Esto es útil para informar a los elementos secundarios cuando algún otro mecanismo proporciona padding o espaciado en vez de los modificadores de padding de inserción, como un Modifier.padding común o espaciadores de altura fijos:

@OptIn(ExperimentalLayoutApi::class)
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

En los casos en que las inserciones de ventana sin procesar se necesiten sin consumo, usa los valores WindowInsets directamente o WindowInsets.asPaddingValues() para mostrar una PaddingValues de las inserciones que no se ven afectadas por el consumo. Sin embargo, debido a las siguientes advertencias, es preferible usar los modificadores de padding de las inserciones de ventana y los modificadores de tamaño de las inserciones de ventana siempre que sea posible.

Inserciones y fases de Jetpack Compose

Compose usa las APIs principales de AndroidX subyacentes para actualizar y animar las inserciones, que usan las APIs de la plataforma subyacentes que administran las inserciones. Debido a ese comportamiento de la plataforma, las inserciones tienen una relación especial con las fases de Jetpack Compose.

El valor de las inserciones se actualiza después de la fase de composición, pero antes de la fase de diseño. Esto significa que, por lo general, cuando se lee el valor de las inserciones en la composición, se usa un valor de las inserciones que está un fotograma tarde. Los modificadores integrados que se describen en esta página están diseñados para retrasar el uso de los valores de las inserciones hasta la fase de diseño, lo que garantiza que los valores de las inserciones se usen en el mismo marco en el que se actualizan.

Animaciones IME del teclado con WindowInsets

Puedes aplicar Modifier.imeNestedScroll() a un contenedor de desplazamiento para abrir y cerrar el IME automáticamente cuando te desplazas a la parte inferior del contenedor.

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

Animación que muestra un elemento de IU que se desplaza desde arriba hacia abajo a fin de dejar lugar para un teclado.

Figura 1: Animaciones IME

Compatibilidad con la inserción para componentes de Material 3

Para facilitar su uso, muchos de los elementos integrados que admiten composición de Material 3 (androidx.compose.material3) controlan las inserciones por sí mismos, según cómo se ubican los elementos componibles en tu app, según las especificaciones de Material.

Control de inserciones de elementos componibles

A continuación, se muestra una lista de los componentes de Material que controlan las inserciones automáticamente.

Barras de la app

Contenedores de contenido

Scaffold

De forma predeterminada, Scaffold proporciona inserciones como el parámetro paddingValues para que las consumas y uses. Scaffold no aplica las inserciones al contenido. Esta responsabilidad es tuya. Por ejemplo, para consumir estas inserciones con un LazyColumn dentro de un Scaffold, haz lo siguiente:

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

Anular inserciones predeterminadas

Puedes cambiar el parámetro windowInsets que se pasa al elemento componible para configurar su comportamiento. Este parámetro puede ser un tipo diferente de inserción de ventana para aplicar, o bien puede inhabilitarse si pasas una instancia vacía: WindowInsets(0, 0, 0, 0).

Por ejemplo, para inhabilitar el control de inserciones en LargeTopAppBar, configura el parámetro windowInsets en una instancia vacía:

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

Interoperabilidad con las inserciones del sistema de View

Es posible que debas anular las inserciones predeterminadas si tu pantalla tiene tanto elementos View como código de Compose en la misma jerarquía. En este caso, debes especificar de forma explícita en cuál se deben consumir las inserciones y cuál se deben ignorar.

Por ejemplo, si tu diseño más externo es un diseño de Android View, debes consumir las inserciones en el sistema de View e ignorarlas para Compose. Como alternativa, si tu diseño más externo es un elemento componible, debes consumir las inserciones en Compose y rellenar los elementos AndroidView componibles según corresponda.

De forma predeterminada, cada ComposeView consume todas las inserciones en el nivel de consumo WindowInsetsCompat. Para cambiar este comportamiento predeterminado, establece ComposeView.consumeWindowInsets en false.

Recursos

  • Now in Android: Es una app para Android completamente funcional que se compiló por completo con Kotlin y Jetpack Compose.