[Xamarin.Forms UI Challenge] Art News, transiciones entre páginas

Introducción

Volvemos a por un reto de interfaz de usuario con Xamarin.Forms. En este artículo, vamos a tomar como referencia un diseño de Dribbble (por Shirley Yao), que intentaremos replicar con paso a paso.

Art News

Vamos a intentar replicar la UI del diseño paso a paso en Xamarin.Forms.

Los retos del ejemplo

Vamos a replicar dos pantallas con algunos retos, pero la clave del ejemplo es la transición de elementos compartidos entre las dos páginas.

  • Listado horizontal: La llegada de CollectionView es no solo una mejora en el rendimiento a la hora de trabajar con colecciones, también con diferentes Layouts (listados horizontales, GridViews, etc.). En este ejemplo, el número de elementos en el listado horizontal es bajo, por lo que podemos también hacer uso de un sencillo StackLayout y Bindable Layout.
  • Grid de fotos en los detalles: En caso de mostrar un número limitado o concreto de fotos en la galería mostrada en la página de detalles, podría usar un Layout. Probablemente un Grid o FlexLayout. Sin embargo, en caso de no ser limitado, CollectionView nos permite mostrar un número indeterminado de fotos en dos columnas de forma sencilla.
  • Transición entre páginas: Por defecto, no tenemos soporte a transiciones de páginas en Xamarin.Forms. Contamos con dos tipos de transiciones diferentes. Por un lado, las transiciones tradiciones que implican una animación de toda la página al entrar o salir. Por otro lado, tenemos las conocidas como transcisiones de elementos compartidos. En muchas ocasiones, tenemos un elemento visual compartido entre dos páginas (por ejemplo, una imagen) para trasmitir una sensación de fluidez y continuidad. En este ejemplo, veremos ambas opciones. Es necesario crear un Custom Renderer de la NavigationPage para conseguir el objetivo. Por suerte, no partimos de cero, utilizaremos Xamarin.Plugin.SharedTransitions.
  • Animaciones: Necesitamos una sencilla animación de Fade In y translación desde la parte inferior hacia la superior al navegar a la página de detalles (además de la transición). Xamarin.Forms cuenta con una completa Api de animaciones. En el ejemplo usaremos Xamanimation que nos ofrece animaciones prepadas, Storyboard y la posibilidad de usarlo todo desde XAML.

Listado horizontal

Para crear el listado horizontal utilizaremos Bindable Layouts.

<ScrollView 
     Orientation="Horizontal"
     HorizontalScrollBarVisibility="Default"
     VerticalScrollBarVisibility="Never">
     <StackLayout 
          x:Name="Highlights"
          Padding="20, 0, 0, 36"
          Orientation="Horizontal"
          BindableLayout.ItemsSource="{Binding Author.Highlights}">
          <BindableLayout.ItemTemplate>
               <DataTemplate>
                     <Grid
                          x:Name="HighlightTemplate"
                          RowSpacing="0"
                          Style="{StaticResource HighlightStyle}">
                          ...
                    </Grid>
               </DataTemplate>
          </BindableLayout.ItemTemplate>
     </StackLayout>
</ScrollView>

Usamos un scroll horizontal (sin mostrar la barra de scroll vertical) con un StackLayout apilando los elementos horizontalmente. La clave es el uso de las propiedades BindableLayout.ItemsSource y BindableLayout.ItemTemplate.

El resultado:

Listado horizontal

NOTA: En caso de contar con un número de elementos elevado, recuerda que no tenemos virtualización, etc. al usar Bindable Layouts. En dicho caso, es más recomendable utilizar CollectionView.

Grid de fotos en los detalles

El Layout de fotos se puede conseguir de diferentes formas. Probablemente, y ante un número determinado de elementos (un número bajo de elementos) se podría usar un Grid o FlexLayout con Bindable Layouts. Sin embargo, en este caso se ha utilizado CollectionView.

<CollectionView
     Grid.Row="1"
     ItemsSource="{Binding ArtItem.Related}"
     SelectionMode="None"
     InputTransparent="True">
     <CollectionView.ItemsLayout>
           <GridItemsLayout 
                Orientation="Vertical" 
                Span="2"/>
     </CollectionView.ItemsLayout>
     <CollectionView.ItemTemplate>
          <DataTemplate>
               <templates:RelatedContentTemplate />
          </DataTemplate>
     </CollectionView.ItemTemplate>
</CollectionView>

Para mostrar las dos columnas, utilizamos la propiedad ItemsLayout con GridItemsLayout utilizando la propiedad Span para indicar el número de columnas deseadas.

El resultado:

Grid de contenido relacionado

Sencillo, ¿verdad?. La llegada de BindableLayouts y de CollectionView nos permite conseguir resultados que hasta ahora requerían la creación de Custom Controls o Custom Renderers.

Transiciones entre páginas

Llegamos al “plato fuerte” del ejemplo, las transiciones entre páginas. Vamos a utilizar una versión modificada (al momento de escribir este artículo quedan algunas Pull Request pendientea por mergear, aunque los cambios de este ejemplo acabarán estando probablemente en la librería) de Xamarin.Plugin.SharedTransitions.

La idea de la librería es:

  • Custom Renderer de NavigationPage donde permitir las transiciones tradicionales (animación de entrada y salida de una página).
  • Effects que poder aplicar a elementos de la UI para permitir aplicar transiciones de elementos compartidos.

En cualquier página (ContentPage), podemos indicar la transición a utilizar de forma sencilla utilizando el método SetBackgroundAnimation:

SharedTransitionNavigationPage.SetBackgroundAnimation(this, BackgroundAnimation.SlideFromLeft);
SharedTransitionNavigationPage.SetSharedTransitionDuration(this, 500);

Entre las opciones disponibles:

  • Fade
  • Flip
  • SlideFromLeft
  • SlideFromRight
  • SlideFromTop
  • SlideFromBottom

¿Y las transiciones de elementos conectados?

Vamos a ver como utilizarlas. Comenzando añadiendo el namespace XAML necesario:

xmlns:sharedTransitions="clr-namespace:Plugin.SharedTransitions;assembly=Plugin.SharedTransitions"

En casos básicos, por ejemplo, conectar dos elementos individuales, por ejemplo un botón en dos páginas diferentes, bastará con utilizar la propiedad Tag disponible en la clase Transition.

En caso de querer hacer transiciones entre elementos de una colección como es nuestro caso (imagen correspondiente a una plantilla usada en un Bindable Layout), necesitamos utilizar además de Tag, la propiedad TagGroup.

sharedTransitions:Transition.TagGroup="1"
sharedTransitions:Transition.Tag="{Binding Number}"

NOTA: Cada elemento debe tener un Tag único.

En la página de destino, volvemos a aplicar el mismo Tag utilizado en la página anterior.

sharedTransitions:Transition.Tag="{Binding ArtItem.Number}"

NOTA: No es necesario utilizar TagGroup en la página de destino.

¿Limitaciones?

  • De momento, entre las transiciones tradicionales soportadas se incluyen las opciones vistas previamente. Proximamente espero añadir alguna opción más (Scale, etc.).
  • Las transiciones de elementos compartidos funcionan con: Label, Image, Button. Próximamente se añadirá soporte a más elementos.
  • Funciona utilizando una NavigationPage. No tiene integración con Shell por ahora.

Animaciones

Llegamos al detalle final del ejemplo. Aunque al navegar a la página de detalles la transición realizada con la imagen que se situará como cabecera conseguirá ya un efecto de fluidez y continuidad elevado, también contamos con una animación del resto del contenido.

Xamarin.Forms cuenta con una API de animaciones completa y sencilla de utilizar. Sin embargo, vamos a conseguir el efecto buscado de forma aún más sencilla directamente desde XAML utilizando Xamanimation.

Para utilizar la librería, comenzamos añadiendo el namespace en la página de detalles:

xmlns:xamanimation="clr-namespace:Xamanimation;assembly=Xamanimation"

A continuación, vamos a crear un Storyboard:

<xamanimation:StoryBoard
     x:Key="ArtItemContentAnimation"
     Target="{x:Reference ArtItemContent}">
     <xamanimation:TranslateToAnimation TranslateY="0" Duration="300"/>
     <xamanimation:FadeInAnimation />
</xamanimation:StoryBoard>

El Storyboard nos permite realizar una animación más compleja, compuesta por otras animaciones. Vamos a hacer una animación de translación hacia arriba además de hacer animar la opacidad del contenido (de 0 a 1).

¿Y cómo se lanza la animación?

Utilizamos un Trigger, para lanzar la animación en el evento Appearing de la página:

<ContentPage.Triggers>
     <EventTrigger Event="Appearing">
          <xamanimation:BeginAnimation 
               Animation="{StaticResource ArtItemContentAnimation}" />
     </EventTrigger>
</ContentPage.Triggers>

El resultado final:

El resultado

¿Qué te parece?

En cuanto al ejemplo, esta disponible en GitHub:

Ver GitHub

Llegamos hasta aquí. Estamos ante un UI Challenge donde el mayor punto de interés recae en las transiciones. Espero que te haya resultado interesante. Pronto más y mejor. Recuerda, cualquier comentario es bienvenida en el artículo!.

Más información

Anuncios

[Xamarin.Forms Challenge] My Tasks

Introducción

Según evoluciona de Xamarin.Forms, llegan más y más opciones que simplifican la creación de diferentes elementos de la interfaz de usuario.

En el estado actual de Xamarin.Forms se pueden conseguir aplicaciones nativas de gran escala, con interfaces cuidadas y con alta integración con la plataforma. Hay que tener en cuenta el conjunto de Custom Renderers (código específico en cada plataforma) necesario para lograrlo.

NOTA: La elección entre Xamarin Classic o Xamarin.Forms es importante. Es necesario evaluar la aplicación a desarrollar, el conjunto de características específicas de cada plataforma (que pueden requerir un Custom Renderer), etc. 

En este artículo, vamos a tomar como referencia un diseño de Dribbble (por Anton Aheichanka), que intentaremos replicar con Xamarin.Forms paso a paso.

El diseño

My Tasks

Vamos a intentar replicar la UI del diseño paso a paso en Xamarin.Forms.

Los retos del ejemplo

Vamos a comenzar haciendo un análisis de la interfaz de usuario desglosando los elementos que la componen:

  • Cabecera: La cabecera cuenta con información del perfil de usuario. La imagen del perfil es circular algo que es sencillo de conseguir de diversas formas (FFImageLoading con transformaciones, ImageCirclePlugin o incluso con SkiaSharp). La otra característica destacada de la cabecera es la imagen de fondo ya que tiene un corte horizontal sencillo pero que hace destacar aun más la cabecera. Entre las opciones que tenemos, una de las más sencillas es utilizar SkiaSharp.
  • El listado: El listado cuenta con una cabecera sencilla pero elegante, fácil de conseguir con la propiedad Header del ListView; elementos que se pueden conseguir definiendo una celda personalizada. En la celda, en caso de reuniones se muestran los participantes. Ahora gracias a BindableLayout es algo sencillo.
  • El menu de filtro: La característica principal de la vista. Una de mis APIs favoritas en Xamarin.Forms es la de animaciones. Sencilla pero ofrece una libertas muy alta. En este caso, podemos replicarlo usando una ContentView con imágenes en el interior y el efecto de apertura y cierre del menu lo conseguimos a base de animaciones (de escala, opacidad, etc.).

Imagen con corte horizontal

¿Cómo conseguimos la imagen con corte horizontal?

SkiaSharp suele ser una opción ideal para opciones de dibujado. En este caso, nos vendría genial directamente dibujar la imagen pero recortando una pequeña parte (triangulo) de la parte inferior. SkiaSharp permite cargar imágenes además de tener una enorme variedad de posibilidades al tratar la misma. aplicar diferentes transformaciones, efectos, etc. Podemos hacer un recorte con trazado (un path) de una imagen.

Tras añadir los paquetes de SkiaSharp.Views.Forms a la solución, creamos un nuevo control que herede de SKCanvasView.

Comenzamos definiendo el trazado que usaremos para recortar la imagen:

SKPath Path = SKPath.ParseSvgPathData("m 0 0 l 400 0 l 0 300 l -400 -50");

Usamos el método ClipPath para hacer el recorte:

canvas.ClipPath(Path);

Para dibujar la imagen usamos SKBitmap que pintaremos con el método DrawBitmap:

canvas.DrawBitmap(_bitmap, info.Rect);

Sencillo, ¿verdad?

Listado

Llegamos al listado. No tiene nada especialmente complejo, pero vamos a ir desglosando cada bloque. Comenzamos por la cabecera. Nos apoyamos en la propiedad Header del ListView para definir la misma:

<ListView.Header>
     <templates:TaskHeaderTemplate />
</ListView.Header>

Donde TaskHeaderTemplate es un ContentView con la definición de la cabecera.

Cada elemento del listado es definido en el ItemTemplate del listado:

<ListView.ItemTemplate>
     <DataTemplate>
          <ViewCell>
               <templates:TaskItemTemplate />
          </ViewCell>
     </DataTemplate>
</ListView.ItemTemplate>

En la definición de cada elemento del listado tenemos una peculiaridad. En caso de reunión, mostramos las personas que asisten. Para ello, hacemos uso de BindableLayout introducido en Xamarin.Forms 3.5:

<StackLayout
     Orientation="Horizontal"
     BindableLayout.ItemsSource="{Binding People}">
     <BindableLayout.ItemTemplate>
          <DataTemplate>
               <Grid>
                    <imageCircle:CircleImage 
                    Source="{Binding Photo}"
                    Aspect="AspectFit"
                    Style="{StaticResource PhotoStyle}"/>
               </Grid>
          </DataTemplate>
     </BindableLayout.ItemTemplate>
</StackLayout>

Pero nos sigue quedando una peculiaridad del ejemplo en el listado…

Animando cada elemento del listado

Cada vez que se añaden o quitan elementos al listado, se introducen con una breve pero efectiva animación de Fade In y translación hacia arriba.

¿Cómo lo conseguimos?

Vamos a crear una ViewCell personalizada. Para ellos creamos una clase que here de ViewCell y al añadir un elemento hijo:

protected override void OnChildAdded(Element child)
{
     base.OnChildAdded(child);

     uint duration = 750;

     var animation = new Animation();

     animation.WithConcurrent((f) => TaskItemTemplate.Opacity = f, 0, 1, Easing.CubicOut);

     animation.WithConcurrent(
     (f) => TaskItemTemplate.TranslationY = f,
     TaskItemTemplate.TranslationY + 50, 0,
     Easing.CubicOut, 0, 1);

     TaskItemTemplate.Animate("FadeIn", animation, 16, Convert.ToUInt32(duration));
}

La definición del ItemTemplate del ListView debe cambiar a algo como:

<ListView.ItemTemplate>
     <DataTemplate>
          <cells:TaskItemViewCell />
     </DataTemplate>
</ListView.ItemTemplate>

Voila!

Filtro circular

Y llegamos a la parte más atractiva del ejemplo.

¿Cómo conseguirla de forma sencilla?

Vamos a crear un ContentView que contendrá primero la imagen del menu cerrado. Por otro lado, la imagen para cerrar el menu y el fondo expandido además de claro, una imagen por cada elemento del menu.

Haremos uso de GestureRecognizers para capturar las pulsaciones en el menu o cada elemento del menú. Al pulsar haremos dos acciones.

Por un lado, lanzaremos un comando definido en el control:

public static readonly BindableProperty SelectedCommandProperty =
     BindableProperty.Create("SelectedCommand", typeof(ICommand), typeof(FilterMenu), null);

public ICommand SelectedCommand
{
     get { return (ICommand)GetValue(SelectedCommandProperty); }
     set { SetValue(SelectedCommandProperty, value); }
}

Y por otro lado, haremos uso de animaciones!. Veamos por ejemplo, que hacemos para abrir el menu:

await InnerButtonMenu.RotateTo(360, _animationDelay);
await InnerButtonMenu.FadeTo(0, _animationDelay);
await InnerButtonClose.RotateTo(360, _animationDelay);
await InnerButtonClose.FadeTo(1, _animationDelay);
await OuterCircle.ScaleTo(3.5, 100, Easing.Linear);
await N.FadeTo(1, speed);
await NW.FadeTo(1, speed);
await SW.FadeTo(1, speed);
await S.FadeTo(1, speed);

Ocultamos con una rotación el botón (imagen) para abrir el menu. a continuación mostramos el botón de cerrar y escalamos (importante!) el area del menu. Por último, cambiamos la opacidad de cada elemento del menu.

Sencillo, pero realmente espectacular y efectivo.

Esta idea de menu esta basada en un gran ejemplo de Alan Beech que Ricardo Vasquez ha elaborado más para tener un control.

El resultado conseguido:

El resultado

Puedes encontrar el ejemplo en GitHub:

Ver GitHub

Hasta aquí el desglose del ejemplo. Lo terminamos sin Custom Renderers o efectos, haciendo uso de Xamarin.Forms y sus APIs para tareas complejas como el menú de filtro o el listado y apoyándonos en SkiaSharp para algunas tareas.

Cualquier duda o comentario es bienvenido en los comentarios!

Más información