[Xamarin.Forms Challenge] My Trip Countdown

Introducción

La evolución de Xamarin.Forms es meritoria. En los últimos tiempos se han recibido novedades interesantes como efectos, vistas nativas, Forms Embedding, FlexLayout, etc. Sin embargo, en muchos casos se sigue asociado a desarrollos muy simples o formularios básicos.

Realmente, 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 Dribble, que intentaremos replicar con Xamarin.Forms paso a paso.

Countdown Timer

Lo primero que debemos realizar, es un breve análisis de la pantalla:

  • Simplificando, estamos ante un contador hacia atrás, en concreto, hacia una fecha. En Xamarin.Forms contamos con la clase Device, una clase importante que nos facilita información importante como si estamos en una plataforma u otra, o si ejecutamos la App en teléfono o tableta. Además, Device cuenta con un Timer que podemos utilizar para la cuenta atrás, entre otras opciones.
  • De lo que más llama la atención de la simple interfaz de usuario, son los degradados. La línea de progreso circular es candidata a realizarse con SkiaSharp. A pesar de poder abordarla con un Custom Renderer, SkiaSharp nos facilitará dibujarla una única vez para todas las plataformas. El botón, a pesar de poder abordarlo también con SkiaSharp, vamos a realizarlo en nativo para poder contar con los diferentes estados, etc. En este caso, vamos a necesitar Custom Renderer o plugin de comunidad.
  • La carátula circular no es compleja. Entre en conjunto de posibilidades que tenemos, FFImageLoading gana enteros por su facilidad de uso, opciones de cache además de transformaciones.
  • La rotación de la carátula es una sencilla animación de rotación. Xamarin.Forms cuenta con una API de animaciones bastante completa.

¿Manos a la obra?

Imagen circular

Comenzamos por una de las partes principales de la vista, la imagen circular. Vamos a utilizar FFImageLoading junto a las opciones de transformación disponibles:

xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:fftransformations="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations"

Vamos a utilizar CircleTransformation para conseguir la imagen circular:

<ffimageloading:CachedImage 
     Aspect="AspectFit"
     Source="{Binding MyTrip.Picture}">
     <ffimageloading:CachedImage.Transformations>
          <fftransformations:CircleTransformation />
     </ffimageloading:CachedImage.Transformations>
</ffimageloading:CachedImage>

Sencillo, ¿verdad?.

Progreso circular con degradado

Sin duda alguna, la clave de la vista es la barra de progreso circular… y con degradado!. Por un lado, podemos irnos a por Custom Renderer y aprovechar código nativo en cada plataforma. Si queremos conseguir todo sólo con código compartido podríamos conseguir una barra circular de forma sencilla con imágenes y rotaciones, por ejemplo. Sin embargo, necesitamos diferentes recursos gráficos para adaptarnos a diferentes resoluciones. Por otro lado, el degradado añade complejidad.

¿La solución sencilla?

SkiaSharp. Engine gráfico 2D disponible para iOS, Android, UWP, macOS, Windows, etc.

Nos permite trabajar de forma sencilla desde co n figuras básicas a efectos y shaders más complejos.

En este caso, vamos a crear un control personalizado que herede de la clase SKCanvasView.

SKCanvasView es una vista Xamarin.Forms donde podremos dibujar utilizando SkiaSharp.

public class CircleCountdown : SKCanvasView
{

}

Vamos a crear varias BindableProperties para gestionar el comportamiento y apariencia de la barra de progreso:

public static readonly BindableProperty StrokeWidthProperty =
     BindableProperty.Create(nameof(StrokeWidth), typeof(float), typeof(CircleCountdown), 10f, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty ProgressProperty =
     BindableProperty.Create(nameof(Progress), typeof(float), typeof(CircleCountdown), 0f, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty ProgressStartColorProperty =
     BindableProperty.Create(nameof(ProgressStartColor), typeof(Color), typeof(CircleCountdown), Color.Blue, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty ProgressEndColorProperty =
     BindableProperty.Create(nameof(ProgressEndColor), typeof(Color), typeof(CircleCountdown), Color.Red, propertyChanged: OnPropertyChanged);

public float StrokeWidth
{
     get { return (float)GetValue(StrokeWidthProperty); }
     set { SetValue(StrokeWidthProperty, value); }
}

public float Progress
{
     get { return (float)GetValue(ProgressProperty); }
     set { SetValue(ProgressProperty, value); }
}

public Color ProgressStartColor
{
     get { return (Color)GetValue(ProgressStartColorProperty); }
     set { SetValue(ProgressStartColorProperty, value); }
}

public Color ProgressEndColor
{
     get { return (Color)GetValue(ProgressEndColorProperty); }
     set { SetValue(ProgressEndColorProperty, value); }
}

Contamos con:

  • StrokeWidth: Permite controlar el grosor de la barra de progreso.
  • Progress: Determina el progreso. Acepta un valor numérico entre 0  y 1.
  • ProgressStartColor: ¿Recuerdas la necesidad de crear degradado?. Por ese motivo, vamos a contar con esta propiedad para gestionar el color de inicio del degradado.
  • ProgressEndColor: Color final del degradado.

Para dibujar el progreso circular, utilizaremos el método DrawPath para dibujar un Path al que le daremos la forma deseada utilizando el método AddArc.

using (SKPath path = new SKPath())
{
     path.AddArc(rect, StartAngle, angle);

     canvas.DrawPath(path, paint);
}

¿Y el degradado?.

En lugar de utilizar un SKColor (color en SkiaSharp) sólido, vamos a crear un shader. Utilizaremos el método CreateSweepGradient para crear el degradado:

var shader = SKShader.CreateSweepGradient(
     new SKPoint(size / 2, size / 2),
     new[]
     {
          ProgressStartColor.ToSKColor(),
          ProgressEndColor.ToSKColor(),
          ProgressStartColor.ToSKColor()
     },
     new[]
     {
          StartAngle / 360,
          (StartAngle + progressAngle + 1) / 360,
          (StartAngle + progressAngle + 2) / 360
});

Para utilizar el control, definimos el namespace:

xmlns:controls="clr-namespace:MyTripCountdown.Controls"

Y utilizamos el control:

<controls:CircleCountdown 
     VerticalOptions="FillAndExpand"
     HorizontalOptions="FillAndExpand"
     Progress="{Binding Progress}"
     ProgressStartColor="{StaticResource ProgressPinkColor}"
     ProgressEndColor="{StaticResource ProgressBlueColor}"
     StrokeWidth="10"/>

El progreso lo tenemos enlazado a una propiedad en la ViewModel donde se irá realizando el cálculo del progreso.

Botón con degradado

Llegamos al botón con degradado. Podríamos al igual que la barra de progreso circular, crearlo con SkiaSharp facilmente. Sin embargo, un botón cuenta con estados que tienen importancia. Habilitado, deshabilitado, pulsado, etc. Si queremos contar con un botón, debemos crear un Custom Renderer. No es complejo haciendo un Custom Renderer del botón y utilizando RippleDrawable con GradientDrawable en Android, y CAGradientLayer en iOS.

Sin embargo, la comunidad de Xamarin es fantástica. Contamos con un botón con degradado preparado para utilizar con Skor.UI. Tras añadir el paquete NuGet, añadimos el namespace:

xmlns:skor="clr-namespace:Skor.Controls;assembly=Skor.Controls"

Y utilizamos el control:

<skor:GradientButton 
     HeightRequest="60" 
     CornerRadius="96"
     StartColor="{StaticResource ProgressPinkColor}"
     EndColor="{StaticResource ProgressBlueColor}"
     Command="{Binding RestartCommand}"/>

De nuevo, muy sencillo esta parte de la interfaz, ¿verdad?.

Otros

Y no hay que olvidar que estamos haciendo un contador hacia atrás. ¿Formas de conseguirlo?. Tenemos diferentes opciones, directamente en Xamarin.Forms, tenemos el método StartTimer en la clase Device:

Device.StartTimer(TimeSpan.FromSeconds(seconds), () =>
{
     RemainTime = (EndDate - DateTime.Now);

     var ticked = RemainTime.TotalSeconds > 1;

     if (ticked)
     {
          Ticked?.Invoke();
     }
     else
     {
          Completed?.Invoke();
     }

     return ticked; 
});

¿Qué plugins o componentes se han utilizado?

Se ha utilizado:

  • FFImageLoading – Con el objetivo principal de gestionar la imágen circular. Esta librería también nos facilita importante funcionalidad relacionada con el cacheo de imágenes, etc. Aunque recuerda, en este ejemplo todas las imágenes son locales.
  • SKOR.UI – Nos facilita la creación de botones con degradados evitandonos crear un Custom Renderer.

Y llegamos a la parte final del artículo. Es un concepto de artículo que ya hemos realizado previamente con la creación de la interfaz de Netflix, por ejemplo. Es sumamente divertido preparar un diseño con cierto nivel de «reto» e intetar “desgranar” cada paso a realizar. Pero, ¿qué te parece este tipo de artículos?.

Cualquier duda o comentario es bienvenido en los comentarios!

El resultado

My Trip Countdown

Puedes encontrar el ejemplo en GitHub:

Ver GitHub

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *