¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Luis Barroso 22/04/2024 Cargando comentarios…
Actualmente, disponemos en el mercado de una amplia variedad de dispositivos en cuanto a su pantalla. Además de las pulgadas, también tenemos que tener en cuenta la relación de aspecto, por ejemplo para móviles, 16:9, 18:9, 19:9, etc. así como algunas particularidades como pueden ser los dispositivos plegables. Por tanto, al adaptar nuestra aplicación a todos estos escenarios, el usuario tendrá una experiencia más enriquecedora.
En este artículo, veremos las posibles estrategias a seguir para que nuestra aplicación pueda adaptarse a los distintos tipos de pantalla.
Gracias a la reactividad de Compose, vamos a poder “escuchar” continuamente los cambios de la pantalla y mostrar una disposición de elementos distinta en base al espacio del que dispongamos. Estos cambios pueden ser: rotaciones, pantallas divididas, plegar/desplegar el dispositivo, etc.
Antes de entrar en materia, debemos conocer la diferencia entre las unidades a medir en una pantalla:
Las unidades dp y sp son una solución para que nuestra app no quede desproporcionada en determinados tamaños. Por ejemplo, si tenemos un componente que debe ocupar 8x3 píxeles, usando la unidad de “píxel”, el componente se verá a distintos tamaños en función de las pulgadas del dispositivo:
En cambio, si usamos la unidad de densidad de pantalla (dp), el componente de tamaño 8x3 queda de la siguiente manera:
Sobre esta idea de densidad de pantalla construiremos las bases para que nuestra app pueda adaptarse a cualquier tipo de dispositivo.
Antes de llegar Compose, adaptábamos las pantallas de nuestra aplicación en XML haciendo uso de varios elementos:
Según el tamaño del dispositivo, se usaban los siguientes sufijos en las carpetas de recursos:
Sufijo recurso | Píxeles por pulgada | Tamaño | Resolución |
---|---|---|---|
ldpi | 120 | Pequeño | 240x320 |
mdpi | 160 | Medio | 320x480 |
hdpi | 240 | Alto | 480x800 |
xhdpi | 320 | Extra Alto | 720x1280 |
xxhdpi | 480 | Extra Extra Alto | 1080X1920 |
xxxhdpi | 640 | Extra Extra Extra Alto | 1440X2560 |
De esta forma, si nuestra app soportaba, por ejemplo, pantallas en resolución media y alta, y además contemplaba la orientación vertical y horizontal, debíamos crear un layout para cada variante, quedando el árbol de directorios así:
Hasta ahora, hemos tenido en cuenta el tamaño de la pantalla y su orientación (vertical y horizontal). Sin embargo, han surgido otros formatos, debido a la inmensa variedad de relaciones de aspectos que los fabricantes de móviles y tablets han sacado.
De esta forma, se establecen 3 rangos a tener en cuenta, tanto en altura como en anchura:
De esta forma, los dispositivos encajan de la siguiente forma según el rango:
Rango en dp | Dispositivo | Window Size Class |
---|---|---|
0-599 | Móvil en vertical | Compact |
600-839 | Móvil plegable en vertical Tablet en vertical |
Medium |
840+ | Móvil en horizontal Móvil plegable en horizontal Tablet en horizontal |
Expanded |
Una vez comprendida la variedad de tamaños que hay, podemos implementarlo en nuestra aplicación de 2 formas:
Independientemente de qué forma elijamos, la idea es simple: tenemos que propagar esta configuración desde la raíz de nuestra interfaz, para que cualquier composable se alimente de ella y se “pinte” de la forma correcta.
Si usamos la librería de Material 3, “material3-window-size-class”, tendremos gran parte del trabajo hecho. Simplemente, tenemos que incluir la librería en nuestro proyecto y colocar la escucha del estado de las dimensiones del dispositivo, en la raíz de nuestros compostables.
Incluir la librería:
implementation("androidx.compose.material3:material3-window-size-class")
Obtenemos la configuración, mediante la función calculateWindowSizeClass, y la propagamos a toda nuestra aplicación para que cualquier pantalla o componente sepa cómo debe pintarse según las dimensiones del dispositivo:
Material3WindowSizeTheme {
val windowSize = calculateWindowSizeClass(this)
val width = windowSize.widthSizeClass
val height = windowSize.heightSizeClass
when (width) {
WindowWidthSizeClass.Compact -> "Screen width is Compact"
WindowWidthSizeClass.Medium -> "Screen width is Medium"
WindowWidthSizeClass.Expanded -> "Screen width is Expanded"
}
when (height) {
WindowHeightSizeClass.Compact -> "Screen height is Compact"
WindowHeightSizeClass.Medium -> "Screen height is Medium"
WindowHeightSizeClass.Expanded -> "Screen height is Expanded"
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Our app will use this configuration to change his size
MyApp(windowSize)
}
}
Nota: esta librería está todavía en fase alpha, por lo que tendremos que anotar todas las clases involucradas con la siguiente anotación:
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
Si por alguna razón la librería de Material no cubre nuestras necesidades, o no nos interesa tener un código en fase alpha, la flexibilidad de Compose nos permite construir nuestras propias clases para gestionar los cambios de pantalla.
En primer lugar, creamos los distintos tipos de tamaños que vamos a cubrir. Siguiendo las recomendaciones de Google, tendremos 3, aunque podemos reducirlo a 2 o incluso crear más si es necesario:
enum class WindowType {
Compact,
Medium,
Expanded
}
Ahora creamos la clase que va a tener la configuración del dispositivo, contemplando la altura y anchura:
data class WindowSize(
val widthType: WindowType,
val heightType: WindowType
)
Por último, necesitamos crear una función composable, rememberWindowSize, que sea reactivo a los cambios del dispositivo:
@Composable
fun rememberWindowSize(): WindowSize {
val configuration = LocalConfiguration.current
val screenWidth by remember(key1 = configuration) {
mutableStateOf(configuration.screenWidthDp)
}
val screenHeight by remember(key1 = configuration) {
mutableStateOf(configuration.screenHeightDp)
}
return WindowSize(
width = getScreenWidth(screenWidth),
height = getScreenHeight(screenHeight)
)
}
fun getScreenWidth(width: Int): WindowType = when {
width < 600 -> WindowType.Compact
width < 840 -> WindowType.Medium
else -> WindowType.Expanded
}
fun getScreenHeight(height: Int): WindowType = when {
height < 480 -> WindowType.Compact
height < 900 -> WindowType.Medium
else -> WindowType.Expanded
}
Esta función rememberWindowSize devuelve un objeto con la configuración actual del dispositivo. Como puede apreciarse, este objeto se compone de 2 parámetros, screenWidth y screenHeight, los cuales son reactivos ante cambios en las dimensiones del dispositivo.
De esta forma, si el dispositivo es rotado, la altura y anchura de la pantalla cambiará, forzando a propagar una nueva instancia de la clase WindowSize, que será usada en la recomposición total de la aplicación.
Las funciones auxiliares getScreenWidth y getScreenHeight son las que determinan dónde están los límites para diferenciar entre los 3 tamaños contemplados.
Finalmente, podemos usar todo esto en la raíz de nuestra aplicación:
Material3WindowSizeTheme {
val windowSize = rememberWindowSize()
when (windowSize.widthType) {
WindowType.Compact -> "Screen width is Compact"
WindowType.Medium -> "Screen width is Medium"
WindowType.Expanded -> "Screen width is Expanded"
}
when (windowSize.heightType) {
WindowType.Compact -> "Screen height is Compact"
WindowType.Medium -> "Screen height is Medium"
WindowType.Expanded -> "Screen height is Expanded"
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Our app will use this configuration to change his size
MyApp(windowSize)
}
}
Como se puede apreciar, es bastante similar a usar la librería de Material 3, pero somos nosotros quienes controlamos los límites para determinar las dimensiones usadas.
La estrategia es simple, cada composable tiene que ser responsable de la disposición y tamaño de sus elementos, así como propagar a los mismos la información de las dimensiones del dispositivo, para que estos puedan tomar ciertas decisiones internamente.
Imaginemos que tenemos un escenario en el que tenemos que mostrar un listado de elementos.
Nuestra aplicación va a contemplar el ancho de la pantalla, para poder mostrar un listado, si la pantalla es compacta, o un grid (con columnas variables) cuando es más ancha, además de incrementar los tamaños de las fuentes y los elementos.
Para este ejemplo, vamos a mostrar una lista de 20 elementos, que tiene una imagen, un título y una descripción:
// List to display items
val loremList = (0..19).map { index ->
Item(
icon = R.drawable.lorem,
name = "Lorem Ipsum $index",
description = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged."
)
}
La pantalla que va a mostrar ese listado, Screen1, recibe por parámetro la configuración del dispositivo detectada previamente. Con esta podemos tomar ciertas decisiones para mostrar un listado o un grid.
@Composable
fun Screen1(windowSize: WindowSize) {
when (windowSize.widthType) {
WindowType.Compact -> {
LazyColumn {
items(count = loremList.size) { index ->
MyItem(
item = loremList[index],
windowSize = windowSize
)
}
}
}
WindowType.Expanded, WindowType.Medium -> {
val maxColumns = if (windowSize.widthType == WindowType.Medium) {
2
} else {
3
}
LazyVerticalGrid(
columns = GridCells.Fixed(count = maxColumns)
) {
items(count = loremList.size) { index ->
MyItem(
item = loremList[index],
windowSize = windowSize
)
}
}
}
}
}
A su vez, el composable MyItem, recibe la configuración (windowSize), para que internamente pueda adaptarse a una pantalla compacta, mediana o expandida:
@Composable
private fun MyItem(item: Item, windowSize: WindowSize) {
val textStyleHeadline: TextStyle
val textStyleSupporting: TextStyle
val itemHeight: Dp
val iconSize: Dp
when (windowSize.widthType) {
WindowType.Compact -> {
textStyleHeadline = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
textStyleSupporting = TextStyle(
textAlign = TextAlign.Justify,
fontSize = 16.sp
)
itemHeight = 120.dp
iconSize = 80.dp
}
WindowType.Medium, WindowType.Expanded -> {
textStyleHeadline = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 26.sp
)
textStyleSupporting = TextStyle(
textAlign = TextAlign.Justify,
fontSize = 20.sp
)
itemHeight = 200.dp
iconSize = 120.dp
}
}
ListItem(leadingContent = {
Box(modifier = Modifier.fillMaxHeight()) {
Icon(
painter = painterResource(id = item.icon),
contentDescription = "",
modifier = Modifier.size(iconSize),
tint = Color.Unspecified
)
}
}, modifier = Modifier.height(itemHeight), headlineText = {
Text(
text = item.name, style = textStyleHeadline
)
}, supportingText = {
Text(
text = item.description,
style = textStyleSupporting,
overflow = TextOverflow.Ellipsis
)
})
}
Simplemente, para cada dimensión, damos unos estilos de texto y alguna propiedad más que pueda resultar necesaria cuando la pantalla es más ancha.
Por último, para poder hacernos una idea lo más ajustada posible de la previsualización de nuestra pantalla, podemos usar el parámetro device para indicar el tamaño del dispositivo:
@Preview(
showBackground = true,
device = NEXUS_5X,
showSystemUi = true
)
@Composable
fun Screen1CompactPreview() {
Material3WindowSizeTheme {
Screen1(WindowSize(WindowType.Compact, WindowType.Compact))
}
}
@Preview(
showBackground = true,
device = TABLET,
showSystemUi = true
)
@Composable
fun Screen1ExpandedPreview() {
Material3WindowSizeTheme {
Screen1(WindowSize(WindowType.Expanded, WindowType.Expanded))
}
}
Ya solo queda lanzar nuestra app en un móvil o en una tablet para ver el resultado final:
Con esto, hemos visto la principal estrategia a la hora de abordar los tamaños de varias pantallas en Compose. Simplemente, debemos tomar las decisiones oportunas en cada pantalla o componente a la hora de mostrarse, así como la propagación de la configuración del dispositivo a componentes internos.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Estamos comprometidos.
Tecnología, personas e impacto positivo.
Cuéntanos qué te parece.