Gradient ProgressBar para Xamarin.Forms usando Skiasharp

Gradient ProgressBar para Xamarin.Forms usando Skiasharp

Empece a ‌crear una aplicación para llevar cuenta de el progreso de los libros que estoy leyendo (algo así como goodreads pero con una interfaz mas bonita). En cuanto el diseño, en dribble encontre algunas ideas interesantes como esta https://dribbble.com/shots/5632432-Book-App-UI/attachments de Carlos Han.

Para implementarla necesito agregar algunos controles con gradientes de fondo por lo que empecé por crear esta barra de progreso con gradiente utilizando Skiasharp.

TLDR;

Si quieres utilizarlo en tu aplicación, solo copia este archivo de 187 lineas de código GradientProgressbar.cs en tu proyecto de Xamarin.Forms y asegurate de instalar el paquete de  SkiaSharp.Views.Forms.

Empecemos la explicación larga acerca de este control...

Este control es para mostrar un valor entre 0 y 1 como una barra de progreso. Dado su look minimalista, es algo sencillo. Son 3 partes principales superpuestas una encima de otra, el rectángulo de fondo que representa el ancho completo del control, el rectángulo que pinta el progreso y el texto indicando la cantidad.

Para crear nuestro control debemos crear una clase que derive de SKCanvasView y sobrescribir el método OnPaintSurface. Este método recibe un parámetro a través del cual tendremos acceso al Canvas para dibujar nuestro control así como  a la información del control que incluye datos como las dimensiones actuales.

var info = e.Info;
var canvas = e.Surface.Canvas;

float width = (float)Width;
var scale = CanvasSize.Width / width;

Es común incluir las lineas de arriba al inicio del método para tener a mano la información de nuestra superficie, el canvas para pintar y la escala.

La escala es importante porque nos permite transformar medidas en unidades independientes a pixeles. Xamarin.Forms maneja todo en unidades independientes de la densidad del dispositivo pero nuestro canvas de Skiasharp no.

Para dibujar los rectángulos con bordes redondeados podemos utilizar una curva o utilizar la clase SKRoundRect, en mi caso lo he tratado de hacer lo más simple posible así que estoy utilizando esta ultima.

var backgroundBar = new SKRoundRect(new SKRect(0, 0, info.Width, height), cornerRadius, cornerRadius);            

var backgroundPaint = new SKPaint { Color = BarBackgroundColor.ToSKColor(), IsAntialias = true};

canvas.DrawRoundRect(backgroundBar, backgroundPaint);

Dibujar el fondo es muy sencillo, la clase SKRoundRect necesita una instancia de SKRect para determinar las dimensiones del mismo y los valores X e Y para el radio del redondeado de las esquinas. Y nuestro canvas utiliza el método DrawRoundRect el que recibe nuestro rectángulo y una instancia de SKPaint para determinar como pintarlo, en nuestro caso el color de fondo.

Si te preguntas de donde sale BarBackgroundColor, pues es una propiedad de nuestro control que he definido para que se pueda personalizar, pero tranquilo ya llegaremos a eso.

var percentageWidth = (int) Math.Floor(info.Width * percentage);                     
var progressBar = new SKRoundRect(new SKRect(0, 0, percentageWidth, height), cornerRadius, cornerRadius);

Ahora, pueden ver la definición del rectángulo para pintar el progreso. Es exactamente igual que en el caso anterior, lo único especial es el calculo de la coordenada X que determina su ancho en base al porcentaje.

Yo espero recibir el porcentaje como un valor entre 0 y 1, por lo que si quieren pintar 50% utilizaríamos 0.5.

Entonces, si tenemos un ancho de 100 (obtenido con info.width) podemos calcular el ancho de nuestra barra de progreso como width * percentage.

100 * 0.5 = 50

Ahora, la única complicación que podríamos encontrar es el pintar el gradiente, dado que es mas complicado de lo que podrían imaginar, pero vamos a explicarlo parte por parte.

using (var paint = new SKPaint(){ IsAntialias = true })
{
  float x = percentageWidth;
  float y = info.Height;
  var rect = new SKRect(0, 0, x, y);
  
  paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Left, rect.Top),
     new SKPoint(rect.Right, rect.Top),
     new []
     {
         GradientStartColor.ToSKColor(), 
          GradientEndColor.ToSKColor()
     },
     new float[] { 0, 1 },
     
     SKShaderTileMode.Clamp);
            
  canvas.DrawRoundRect(progressBar, paint);
}

Ok, lo primero que hacemos es crear un SKPaint para nuestro rectángulo, dentro de un usingpara poder liberarlo luego de utilizarlo. Para que tengamos nuestro gradiente, que en mi caso sera un gradiente linear necesitamos fijar el Shader de nuestro objeto paint. El método SKShader.CreateLinearGradient nos permite crear nuestro SKShader para un gradiente linear, ahora prestemos atención a sus parámetros.

Los primeros dos parámetros de nuestro CreateLinearGradient, son 2 puntos que representan desde donde y hasta donde se va a crear nuestro gradiente, con esos valores se determina el tamaño del gradiente y la dirección. Este gradiente creado utilizara para pintar nuestro rectángulo. Es importante entender que estos valores determinan como se ve nuestro gradiente, pueden usar la siguiente imagen como referencia para entender el concepto.

Como el tamaño afecta el resultado final

En mi caso, he optado por la segunda opción en la cual ajusto el gradiente al tamaño de mi rectángulo.

     new []
     {
         GradientStartColor.ToSKColor(), 
          GradientEndColor.ToSKColor()
     },

El tercer, parámetro es un arreglo de colores ( SKColor) que conformaran nuestro gradiente, en nuestro caso solo estamos usando 2.

El cuarto parámetro, es un arreglo de float (gradient stops) que determinan la posición de los colores de nuestro gradiente. Con cero(0) para el inicio y uno(1) para el final. Dependiendo el efecto que deseen pueden cambiar estos valores.

El ultimo valor es una opción dentro de nuestro enumerado SKShaderTileMode. Con este valor determinamos que hacer cuando el gradiente no cubre toda la superficie que deseamos pintar, pueden hacer que se rellene en espejo, se continúe con el color de los bordes o se repita el gradiente (tiled).

La cereza del pastel, el texto que muestra el porcentaje

Ahora, finalmente necesitamos pintar el texto que muestra el porcentaje y veremos las consideraciones que tiene el pintar algo variable en longitud.

var textPaint = new SKPaint {Color = TextColor.ToSKColor(), TextSize = textSize};

var textBounds = new SKRect();

textPaint.MeasureText(text, ref textBounds);

var xText = percentageWidth / 2 - textBounds.MidX;
        
var yText = info.Height / 2 - textBounds.MidY;

canvas.DrawText(text, xText, yText, textPaint);

Lo primero que necesitamos es un SKPaint igual que antes, en nuestro caso nuestro objeto paint considera la propiedad TextSize.

Lo curioso con el texto es que necesitamos medir el espacio que ocupara nuestro texto para poder posicionarlo en nuestro control, para este propósito utilizaremos el método MeasureText del objeto SKPaint.  Una vez que conocemos el tamaño que tendrá podemos determinar las coordenadas de inicio de nuestro texto dependiendo de donde queremos que aparezca.

var yText = info.Height / 2 - textBounds.MidY;

El inicio en Y será el centro vertical, por lo que usamos la altura, la dividimos a la mitad y le restamos la mitad del tamaño del texto ( MidY ).

var xText = percentageWidth / 2 - textBounds.MidX;

En el caso de la posición en el eje X, voy tomar el tamaño de nuestro rectángulo que representa el progreso, calcular la mitad y luego restarle la mitad del tamaño de nuestro texto, eso lo posicionara al centro.

if (xText < 0)
{
    xText = info.Width / 2 - textBounds.MidX;
    textPaint.Color = AlternativeTextColor.ToSKColor();
}

Ademas, he colocado esta condición para validar si el texto es mas grande que la barra de progreso, cosas que podría suceder con porcentajes muy pequeños como 1% (dependiendo del ancho del control), en ese caso, utilizo el tamaño de la barra completa y le doy un color alternativo al texto.

Momento de volverlo personalizable

Para que el control se pueda personalizar debemos definir propiedades, y dado que nuestra intención es que se pueda aplicar binding a esas propiedades las definiremos como BindableProperties. Pueden revisar la documentación si no están familiarizados con el término. https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/bindable-properties

En el caso de nuestras propiedades, sera sencillo implementarlas solo debemos asegurarnos que cuando una propiedad cambie (OnPropertyChanged) se invalide la superficie de nuestro canvas y se le obligue al control a re-pintarlo.

public static BindableProperty GradientEndColorProperty = BindableProperty.Create(nameof(GradientEndColor), typeof(Color),
        typeof(GradientProgressBar), Color.Blue, BindingMode.OneWay,
        validateValue: (_, value) => value != null, propertyChanged: OnPropertyChangedInvalidate);

public Color GradientEndColor
{
    get => (Color)GetValue(GradientEndColorProperty);
    set => SetValue(GradientEndColorProperty, value);
}
    
private static void OnPropertyChangedInvalidate(BindableObject bindable, object oldvalue, object newvalue)
{
   var control = (GradientProgressBar)bindable;

   if (oldvalue != newvalue)
       control.InvalidateSurface();
}

Como pueden ver, el método OnPropertyChangedInvalidate se usara para todas las propiedades que definamos, dado que siempre deseamos hacer lo mismo, repintar el control frente a un cambio.

Pueden revisar todas las propiedades que he definido para el control en Github https://github.com/jesulink2514/XamBooksApp/blob/master/XamBooksApp/XamBooksApp/Controls/GradientProgressBar.cs .

¿Quieren más?

Les dejo el enlace del proyecto que estoy avanzando, seguiré creando mas elementos de UI con Skiasharp para completar mi diseño. Si tienen algunas ideas sobre mas controles que puedan crear con Skiasharp que serian muy útiles, no duden en dejar su idea en los comentarios.

https://github.com/jesulink2514/XamBooksApp

Enlaces de Interés