Cómo crear una interfaz adaptable para Windows 10 usando VisualStates

La aparición de Windows 10 significó una revolución para el mercado de desarrollo de aplicaciones: por primera vez en la historia es posible crear UNA SOLA aplicación, con UN SOLO binario y que funcione de manera espectacular en TODOS los dispositivos que corran la llamada Plataforma Universal de Windows o UWP por sus siglas en inglés. Para que entiendas un poco mejor este concepto, te recomiendo leer este artículo escrito por el gran Mauricio Contreras.

Este enorme paso significa también un gran reto para nosotros, los desarrolladores, ya que tendremos que preocuparnos por que nuestra aplicación tenga una interfaz de usuario que sea capaz de brillar en cada uno de los dispositivos con diferentes tamaños y resoluciones.

Hay dos maneras para lograr esto: crear una sola interfaz y que sea adaptable según los tamaños de pantalla (adaptive design), o desarrollar una interfaz específica o "a medida" para cada familia de dispositivos (tailored design). Esta vez pondremos la mirada en el primer punto.

Creando nuestro primer Adaptive Layout

Aquí tenemos una aplicación que simula ser una página de noticias sobre Mr. Tibbles que despliega contenido en 3 columnas. Sin embargo, hay un pequeño problema: La interfaz no es la más adecuada en pantallas pequeñas como la de un Mobile y dificulta un poco la lectura:

Adaptive1

Para solucionar esto, te invito a abrir tu Visual Studio y copiar el código de la aplicación manteniendo solo la etiqueta Page . Para esto creamos un Nuevo Proyecto > Visual C# > Windows > Blank App (Universal Windows).


<Grid Name="LayoutRoot" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <TextBlock Text="Mr. Tibbles"  FontSize="24" Margin="10,5,0,5"/>
    <ScrollViewer Grid.Row="1">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <StackPanel Name="First" Margin="20,20,0,0">
                <Image Source="Assets/StoreLogo.png" HorizontalAlignment="Center" MaxWidth="250" />
                <TextBlock TextWrapping="Wrap">This is some information about Mr. Tibbles.</TextBlock>
            </StackPanel>
            <StackPanel Name="Second" Grid.Column="1" Margin="20,20,0,0">
                <TextBlock TextWrapping="Wrap">
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
                        Cras id orci iaculis, aliquet nibh at, dictum lorem. Vivamus 
                        tempus tristique sollicitudin. Etiam interdum et lectus 
                        semper molestie. Phasellus lobortis felis quis risus posuere, 
                        id molestie mi sagittis. Cras odio leo, dictum vitae euismod et, 
                        lacinia non lectus. Integer quis massa velit. Ut at dui 
                        rutrum, venenatis dui a, pretium arcu. Nunc eu urna pulvinar, 
                        condimentum sapien non, consectetur turpis. Pellentesque dapibus, 
                        eros ac rutrum congue, quam dolor rhoncus nunc, ut sagittis nisl 
                        urna in ante. Sed nunc libero, aliquet at elit eget, vulputate 
                        ultrices leo. Aliquam vel sapien varius, blandit dui ac, 
                        fringilla metus. Lorem ipsum dolor sit amet, consectetur 
                        adipiscing elit. Fusce vehicula odio sit amet tortor lobortis 
                        sagittis. Nullam vestibulum tortor eget risus vulputate, at 
                        semper nunc pharetra. Nullam fringilla dapibus turpis non 
                        vehicula. Proin sollicitudin sapien enim, at interdum risus 
                        cursus quis.
                </TextBlock>
            </StackPanel>
            <StackPanel Name="Third" Grid.Column="2" Margin="20,20,0,0">
                <TextBlock TextWrapping="Wrap">
                        Nam sollicitudin justo quis consequat molestie. Etiam dictum 
                        sodales tellus, ut consectetur magna sodales in. Phasellus viverra 
                        volutpat porttitor. Pellentesque sed condimentum neque. In 
                        ultrices ex ac lacus tincidunt, eget euismod urna cursus. Donec tempor 
                        mauris leo, ac cursus nisl tempus a. Aliquam dignissim eleifend lorem a 
                        facilisis. Praesent tincidunt semper ante non ornare. Cras eleifend 
                        eros et tincidunt auctor. Duis lorem nunc, dictum dignissim est vitae, 
                        luctus dapibus lacus. Donec fringilla ipsum nec diam sagittis, 
                        nec suscipit metus maximus. Aliquam aliquam non ante tincidunt 
                        fringilla. Phasellus auctor, nisl non rutrum imperdiet, arcu
                        purus pretium libero, nec eleifend metus turpis vel ante. Phasellus 
                        sit amet rhoncus lectus.
                </TextBlock>
            </StackPanel>
        </Grid>
    </ScrollViewer>
</Grid>

Para hacer nuestra interfaz adaptable, tenemos que hacer uso de Visual States. Esta herramienta tan solo consta de dos componentes principales: los triggers y setters.

Los triggers (o disparadores) funcionan como condiciones, si es que se cumple lo que está dentro de ellos, entonces se ejecutarán las propiedades que indicamos dentro de los setters. Para indicar qué controles vamos a modificar, tenemos que ponerle un nombre a cada uno, tal como está en el código que acabas de copiar.

Bien, ahora hacer falta nuestro adaptador. Así que lo haremos de la siguiente manera:

Este código irá en la línea 11 aproximadamente, dentro de nuestro Grid principal llamado LayoutRoot.


<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="VisualStateGroup">
        <VisualState x:Name="Amplio">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="800" />
            </VisualState.StateTriggers>
            <VisualState.Setters>
                <Setter Target="First.(Grid.Row)" Value="0" />
                <Setter Target="First.(Grid.Column)" Value="0" />
                <Setter Target="Second.(Grid.Row)" Value="0" />
                <Setter Target="Second.(Grid.Column)" Value="1" />
                <Setter Target="Third.(Grid.Row)" Value="0" />
                <Setter Target="Third.(Grid.Column)" Value="2" />

                <Setter Target="First.(Grid.ColumnSpan)" Value="1" />
                <Setter Target="Second.(Grid.ColumnSpan)" Value="1" />
                <Setter Target="Third.(Grid.ColumnSpan)" Value="1" />
            </VisualState.Setters>
        </VisualState>
        <VisualState x:Name="Angosto">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="0"/>
            </VisualState.StateTriggers>
            <VisualState.Setters>
                <Setter Target="First.(Grid.Row)" Value="0" />
                <Setter Target="First.(Grid.Column)" Value="0" />
                <Setter Target="Second.(Grid.Row)" Value="1" />
                <Setter Target="Second.(Grid.Column)" Value="0" />
                <Setter Target="Third.(Grid.Row)" Value="2" />
                <Setter Target="Third.(Grid.Column)" Value="0" />

                <Setter Target="First.(Grid.ColumnSpan)" Value="3" />
                <Setter Target="Second.(Grid.ColumnSpan)" Value="3" />
                <Setter Target="Third.(Grid.ColumnSpan)" Value="3" />
            </VisualState.Setters>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Tenemos dos Visual States, el primero es para pantallas amplias, y el siguiente para pantallas angostas.

Cuando estemos en el primer caso, desplegaremos el contenido en 3 columnas de igual medida (tal como lo tenemos líneas arriba); mientras que cuando se cumpla lo segundo, moveremos el contenido para que se despliegue ahora en 3 filas y haremos que cada fila ocupe todo el ancho de nuestras antiguas 3 columnas (para eso el ColumnSpan = 3), transformándola en una sola columna.

Es más o menos lo que usamos en Excel cuando "combinamos celdas".

Nuestros triggers o condiciones se basan en el ancho de la pantalla. Si tiene por lo menos(MinWindowsWidth) 800px de ancho, será una pantalla amplia; de lo contrario, se considera una pantalla angosta.

Y listo! Podemos correr nuestra aplicación y ver los resultados:

AdaptiveLayout2_Angosto

AdaptiveLayout3_Amplio

Recuerda que puedes tener tantos VisualStates como creas conveniente y según lo que tu aplicación requiera. Ah! Y acerca de las medidas que utilicé (0px, 800px): puedes utilizar cualquier otras cantidades, no existen estándares que debas seguir, pero sí debes considerar todos los tamaños posibles.

Yyyy, bueno! Hemos acabado con este tutorial. Tal vez esta no sea la interfaz más atractiva, pero quise hacerlo de la manera más sencilla posible y que se llegue a entender el mensaje. Ahora el reto es tuyo! Aquí te dejo el link del código fuente en Github:

Si te quedaste con alguna duda, puedes dejar tus comentarios aquí abajo o contactarme a mis redes sociales al pié de este artículo, estaré encantado de responderte (de verdad). Si te sirvió de algo, cuéntamelo para saber si lo estoy haciendo bien o si puedo mejorar en algo, realmente te agradecería que me cuentes también!

Nos leemos,
Byte, byte :)

-Tú que sabes C++, C#, Java y Python, ¿me puedes reparar la impresora?
-...

Gracias especiales...
Artículo guía: http://www.wintellect.com/devcenter/jprosise/using-adaptivetrigger-to-build-adaptive-uis-in-windows-10