Saltar al contenido
zabloo

Layout

Flexbox, calculado por el SDK en el dispositivo: el subconjunto de v1, los pases de medida y colocación, el wrap, los tamaños intrínsecos y por qué nada pinta nunca fuera de su propio rect.

El layout es lo que decide dónde cae cada caja. Mira el panel de la tienda: dos filas de objetos apiladas con un hueco entre ellas, cada fila con un nombre a la izquierda y un botón de comprar a la derecha, y cada botón exactamente igual de ancho que su propia etiqueta más su padding. Nadie escribió esos rectángulos. El envelope —el sobre que el juego descarga— lleva las reglas: apila así, deja este hueco, deja que este hijo se quede lo que sobre. Y el SDK saca los números en el dispositivo del jugador.

Formalmente: el layout es Flexbox, calculado por el SDK en tiempo de ejecución. La IR lleva entradas de layout, no rects. No se cuece nada al exportar, porque una UI que pliega una sección, recibe datos nuevos o corre en otra pantalla tiene que volver a maquetarse allí donde se está ejecutando.

No interviene el sistema de layout de ningún motor. El SDK corre su propio pase, y eso es lo que hace que el resultado sea idéntico en todos los targets.

El subconjunto de v1

Nueve propiedades, todas en node.layout, y ese es el vocabulario entero. Son una porción deliberadamente pequeña de Yoga, el motor de Flexbox que sigue el propio pase del SDK — pequeña para que cada target pueda reproducir el mismo resultado exacto.

PropTipoPor defectoDescripción
direction"row" | "column""column"Eje principal del flujo de los hijos.
justify"start" | "center" | "end" | "space-between""start"Reparto del espacio sobrante a lo largo del eje principal, dentro de una línea.
align"start" | "center" | "end" | "stretch""start"Colocación de un hijo a lo ancho del eje cruzado.
gapDim0Espacio entre hijos consecutivos.
paddingDim0Espacio interior en los cuatro lados.
widthDimautoAncho explícito. Ausente = el tamaño medido.
heightDimautoAlto explícito. Ausente = el tamaño medido.
grownumber0Parte del espacio sobrante de la línea, en el eje principal, que se lleva este hijo.
wrapbooleanfalseParte el eje principal en varias líneas cuando los hijos no caben.

Lo que no está en el subconjunto: padding por lado, margin, shrink, basis, posicionamiento absoluto, porcentajes y unidades fraccionarias, align-self, align-content y transformaciones de cualquier tipo. Cada una de ellas es una extensión aditiva que una versión posterior puede introducir; ninguna se puede expresar hoy.

Los dos pases

El dimensionado corre en dos barridos, y saber cuál es cuál es lo que hace predecible el resto de esta página: primero cada nodo dice cómo de grande quiere ser, trabajando de las hojas hacia arriba, y después cada padre reparte el espacio que realmente tiene, trabajando de la raíz hacia abajo. El rect final de un nodo es la segunda respuesta, nunca la primera.

1. Medir, de abajo arriba

Cada nodo se dimensiona a partir de su contenido, contra el ancho que le ofrece su padre:

  1. La view ofrece su propio ancho a la raíz.
  2. El layout.width propio de un nodo, cuando está declarado, sustituye la oferta para su subárbol.
  3. Lo que queda tras el padding del nodo por ambos lados baja a cada hijo — en una fila exactamente igual que en una columna. En v1 la medida no contempla competencia entre hijos por la oferta: a cada hijo se le ofrece el ancho de contenido entero del padre, nunca una parte.
  4. Una oferta puede ser sin restringir: ningún ancho en toda la cadena hacia arriba, o un eje con scroll. Un ScrollView no ofrece nada en el eje por el que scrollea, y eso es lo que hace que su contenido desborde el viewport en vez de encogerse para caber.

La oferta solo les importa a las hojas: es el ancho contra el que un Text parte sus líneas. Un nodo sin hijos se dimensiona con measureLeaf, que es donde un Text consulta las métricas de la fuente y una Image su entrada del manifiesto.

El tamaño medido de un nodo es contenido + padding * 2 en cada eje, y después layout.width y layout.height sobrescriben el resultado allí donde están declarados.

2. Colocar, de arriba abajo

Cada nodo recibe un rect y coloca a sus hijos dentro de la caja de contenido —su rect menos el padding:

  • grow reparte el espacio que queda en la línea, en proporción al grow de cada hijo. Es por línea: una fila con wrap reparte los sobrantes de cada línea. Solo se reparte un sobrante positivo. En el subconjunto no hay shrink, así que un hijo cuyo tamaño medido es mayor que el sitio que tiene la línea conserva ese tamaño y desborda.
  • justify reparte después lo que siga sobrando, a lo largo del eje principal. space-between añade el sobrante entre los hijos, encima del gap, y no le hace nada a una línea con un solo hijo.
  • align coloca cada hijo a lo ancho del eje cruzado. stretch hace que el hijo llene el tamaño cruzado de la línea en vez de quedarse con el suyo medido.

Ejemplo resuelto — una fila de la tienda. El panel declara width: 420, así que ese número sustituye la oferta para todo lo que cuelga de él, y a cada fila se le ofrecen 420 menos el padding del panel. Al medir, los tres hijos de la fila toman sus propios tamaños: la columna del nombre tan ancha como su línea más larga, y el precio y el botón de comprar tan anchos como su texto más el padding. Al colocar, esos tres son más estrechos que la fila, y el sobrante entero se va al único hijo que declara grow: 1: la columna del nombre. Por eso los precios y los botones de comprar quedan alineados contra el borde derecho se llame como se llame el objeto.

Crecer desde una base cero

grow se suma al tamaño medido de un hijo, no lo sustituye. Un nodo que tenga que llevarse exactamente lo que sobra ha de declarar ese tamaño como cero: height: 0 con grow: 1 en una columna, width: 0 con grow: 1 en una fila. Es el equivalente en el subconjunto de flex-basis: 0: con la base a cero el hijo no añade nada al total de la línea, así que grow pasa a ser su única fuente de tamaño y el sobrante es su tamaño.

Así es como un scroller consigue su viewport

Un ScrollView mide a sus hijos sin restringir en el eje por el que scrollea, así que su propio tamaño medido es el del contenido entero. Con grow: 1 a secas no queda sobrante que repartir, y el scroller acaba siendo más grande que su padre y sin nada que scrollear. Poner la base a cero es lo que convierte el sobrante de la columna en el viewport.

El wrap

wrap: true parte a los hijos en líneas, con el primer hueco que sirva: un hijo se añade a la línea actual mientras quepa, y si no, empieza una nueva.

wrap se aplica a una fila. El pase de medida lleva una oferta de ancho y nada más, así que una columna no tiene ninguna longitud contra la que partir y coloca a sus hijos en una sola línea. Esto es también lo que hace con cualquier nodo un SDK anterior al flag: la fila desborda, y recorta si el nodo recorta, pero no se pierde contenido.

justify y align siguen significando lo que significan dentro de una línea. Cómo se reparten las líneas entre sí en el eje cruzado —el align-content de Yoga— queda fuera del subconjunto: las líneas se apilan desde el principio, separadas por el gap.

Una rejilla es esto y nada más: una fila con wrap cuyos elementos tienen un ancho, de forma que caben N por línea. Repeat es donde la capa de autoría te resuelve esa aritmética.

Los tamaños intrínsecos

La mayoría de los nodos son tan grandes como los hijos que llevan dentro. Estos cuatro son tan grandes como otra cosa —el texto que llevan, la imagen a la que apuntan— y conviene saberlo porque es el único tamaño que tienen hasta que les des uno:

NodoTamaño intrínseco
TextEl bloque de texto ya maquetado: anchos de línea, y lineHeight × número de líneas.
ImageEl tamaño en píxeles del origen, según el manifiesto de assets.
TextInputUna línea de texto, al fontSize del campo.
SliderSolo su propio layout — los slots nunca le suman.

Cualquier otro nodo sin hijos mide cero de contenido en los dos ejes: su tamaño es su padding, y nada más. Un Container vacío usado como bloque de color —una muestra, una raya, un separador— no tiene por tanto tamaño en un eje para el que no se le dio uno, y no pinta nada hasta que reciba un width o un height explícito, o hasta que su padre lo estire con align: "stretch".

Los nodos que colocan a sus propios hijos

Tres tipos de nodo sobrescriben el pase de flex para sus slots posicionales, porque su geometría es función de un valor y no del flujo:

  • Slider coloca su relleno y su pulgar a partir del valor actual.
  • ProgressBar dimensiona su relleno a contentMain × value.
  • Overlay sale por completo del flujo de su padre: no ocupa sitio entre sus hermanos, y su propio rect es el de la view.

Todo lo demás —incluidos los slots del indicador de un Toggle y la cabecera de un Collapse— se maqueta por el pase ordinario.

Ocultar

visible: false saca un nodo del layout: sus hermanos cierran el hueco y el nodo ni pinta ni recibe entrada. Este es el único mecanismo para ocultar que hay en el formato, y es el que usan el contenido de un Collapse, los paneles de pestaña no seleccionados y los slots del indicador de un Toggle.

El desbordamiento

Nada pinta fuera de su propio rect de layout. Es un invariante que el formato mantiene a propósito, y es lo que hace honesto el hit-testing sobre rects:

  • Los bordes van hacia dentro: un borderWidth crece hacia el interior, nunca hacia fuera.
  • Una Image con fit: "cover" recorta a través de sus UV en vez de desbordar.
  • El recorrido del pulgar de un Slider va metido hacia dentro medio pulgar, para que se quede dentro de la pista.
  • El texto que no cabe se recorta o se corta con puntos suspensivos, nunca se derrama.

Los hijos de un contenedor pueden desbordarlo: una columna de alto fijo con demasiadas filas dibuja más allá de su borde inferior. clip: true corta eso, tanto para el pintado como para la entrada; un ScrollView lo hace siempre.

Relacionado