[Xamarin UI Challenge] Flights UI

El reto

Volvemos a por un reto de interfaz de usuario con Xamarin.Forms. En esta ocasión, crear un efecto Pull To Refresh personalizado.

La UI a replicar

Los retos del ejemplo

Los retos para conseguir un Pull to Refresh personalizado son los siguientes:

  • Pull to Refresh: Contamos con el control RefreshView diseñado justamente para hacer pull to refresh. Sin embargo, el contenido al hacer refresco no es personalizable. Así que, para conseguir el resultado deseado vamos a utilizar un SwipeView. El contenido del SwipeView será el listado de vuelos mientras que usaremos un SwipeItemView (soporta una View como contenido) para añadir la animación personalizada.
  • La animación de refresco: Para la animación de refresco vamos a usar Lottie.
  • El listado de vuelos: El listado de vuelos se realizará usando CollectionView.

Listado de vuelos

El listado es de todos los elementos de la interfaz, la parte más sencilla.

<CollectionView
     ItemsSource="{Binding Booking}">
</CollectionView>

Mostramos un listado de vuelos resultados de una búsqueda. En la cabecera mostramos información básica como el número total de resultados. Podemos conseguir añadir la información encima de cada resultado usando la cabecera (Header) del CollectionView.

<CollectionView.Header>
     <Grid
          Padding="0, 12, 0, 0">
          <Label 
               Text="2 Search Results"
               Style="{StaticResource HeaderStyle}"/>
     </Grid>
</CollectionView.Header>

El resultado:

La cabecera del listado de vuelos

Cada resultado de trayecto puede tener múltiples vuelos. Vamos a usar Bindable Layout dentro de cada elemento del CollectionView.

<StackLayout
     BindableLayout.ItemsSource="{Binding Flights}"> 
</StackLayout>

De esta forma adaptaremos cada celda de forma sencilla a mostrar el total de paradas y trayectos de un resultado:

Detalles de cada vuelos

Sencillo, ¿verdad?.

Pull to Refresh personalizado

La clave del ejemplo, el efecto Pull to Refresh personalizado lo conseguimos usando el control SwipeView. SwipeView permite cuatro direcciones para hacer swipe: arriba, abajo, izquierda y derecha. En nuestro caso, queremos hacer pull desde la parte superior y por ello vamos a utilizar TopItems. Podemos usar:

  • SwipeItem: Acción contextual sencilla. Es fácil de configurar (color de fondo, texto e icono), pudiendo ejecutar un comando o evento.
  • SwipeItemView: Permite como contenido una View, por lo que las opciones de personalización son más altas.

¿Sabes que vamos a utilizar en este caso?. Probablemente has acertado, vamos a usar un SwipeItemView donde vamos a añadir nuestra animación personalizada.

<SwipeView
     x:Name="SwipeView"
     SwipeEnded="OnSwipeEnded">
     <!-- PULL TO REFRESH CONTENT -->
     <SwipeView.TopItems>
          <SwipeItemView>
               <Grid
                    HeightRequest="60">
                    <!-- CUSTOM ANIMATION -->
               </Grid>
          </SwipeItemView>
     </SwipeView.TopItems>
     <!-- CONTENT -->
     <Grid>
          <CollectionView
               ItemsSource="{Binding Booking}">
          </CollectionView>
     </Grid>
</SwipeView>

Pasamos a ver como definir la animación personalizada.

Animación usando Lottie

La librería Lottie fue creada por Airbnb inicialmente para iOS, Android y React Native. Sin embargo, gracias a la contrinución de Martijn van Dijk y otros miembros de la comuidad, tenemos soporte en Xamarin y Xamarin.Forms.

Para comenzar a trabajar con Lottie en Xamarin.Forms utilizaremos el paquete NuGet Aribnb.Xamarin.Forms.Lottie:

Install-Package Com.Airbnb.Xamarin.Forms.Lottie

Para completar el proceso de preparación de Lottie en nuestro proyecto Xamarin.Forms, necesitamos añadir la siguiente línea tras la inicialización de Xamarin.Forms en cada proyecto nativo:

AnimationViewRenderer.Init();

Para poder mostrar una animación de Lottie, debemos añadir el archivo JSON a cada proyecto nativo:

  • Android: En la carpeta Assets. El archivo debe tener como acción de compilación AndroidAsset.
  • iOS: En la carpeta Resources. El archivo debe tener como acción de compilación BundleResource.
  • UWP: En la carpeta Assets (o en la raíz), el archivo debe tener como acción de compilación Content.

Para mostrar la animación necesitamos un elemento visual que nos permita visualizar la animación además realizar una gestión de la misma. Este elemento visual es AnimationView.

<SwipeView.TopItems>
     <SwipeItemView>
          <Grid
               BackgroundColor="{StaticResource AccentColor}"
               HeightRequest="60">
               <lottie:AnimationView
                    Animation="pulltorefresh.json"
                    Loop="False" 
                    AutoPlay="True"
                    VerticalOptions="FillAndExpand"
                    HorizontalOptions="FillAndExpand"/>
          </Grid>
     </SwipeItemView>
</SwipeView.TopItems>

En nuestro ejemplo usamos esta gran animación creada por Lenny Miranda jr.

Animación de refresco

Obtener recursos

Un gran fuente de animaciones es LottieFiles. Puedes encontrar una gran variedad de animaciones listas para utilizar con opciones de búsqueda, etc.

LottieFiles

NOTA: Si usas animaciones de LottieFiles, recuerda otorgar crédito a sus creadores.

Con la definición del CollectionView, SwipeView y animación con Lottie, tenemos todo lo necesario. Veamos el resultado final!.

El resultado

Puedes encontrar el código en GitHub:

Ver GitHub

Más información

[Xamarin October Challenge] Best Practices

Introducción

Los retos (Challenges) han sido una de las gratas sorpresas en la comunidad de Xamarin en este año. No solo hemos tenido varios retos, además han sido recibidos y apoyados de una forma muy positiva.

¿Por qué no tener otro reto en el mes de Octubre?.

Nuevo Challenge

De esta forma nos llega de la mano de Claudio Sanchez el reto de Octubre basado en buenas prácticas. Hablamos de buenas prácticas en todos los ámbitos del desarrollo Xamarin: estructura del proyecto, DevOps, Testing, creación de APIs, etc.

¿Cómo funciona?

Como otros retos anteriores, la idea es llegar a contar con un artículo por cada día del mes. En esta ocasión todo esta centralizado en un repositorio en GitHub. En el repo puedes ver dos aspectos fundamentales:

  • Calendario de fechas: Donde ver cuales están libres y poder elegir cual prefieres ocupar.
  • Tabla de contenido: Un listado con temas y posibles ideas que puedes abordar.

¿Cómo participo?

Debes reservar un hueco!. Para hacerlo bastará con crear una issue en el repositorio:

Reservar hueco

Tras reservar el hueco, podrás escribir el artículo en un archivo de markdown y enviar una Pull Request al repositorio del reto. Por supuesto, también podrás publicar el artículo en tu blog.

Al final del reto, habrá un evento donde se repasarán todas las contribuciones, etc.

Suena divertido, ¿te animas a participar?.

NOTA: El artículo debe ser en ingles.

Más información

[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

[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