Animación básica en SkiaSharp

Download SampleDescargar el ejemplo

Descubra cómo animar los gráficos SkiaSharp

Puede animar gráficos SkiaSharp en Xamarin.Forms haciendo que se llame periódicamente al método PaintSurface, cada vez que dibuja los gráficos de forma un poco diferente. Esta es una animación que se muestra más adelante en este artículo con círculos concéntricos que aparentemente se expanden desde el centro:

Several concentric circles seemingly expanding from the center

La páginaElipse pulsante en el programa SkiaSharpFormsDemos anima los dos ejes de una elipse para que parezca que está pulsando, e incluso puede controlar la velocidad de esta pulsación. El archivo PulsatingEllipsePage.xaml crea una instancia de un Xamarin.FormsSlider y un Label para mostrar el valor actual del control deslizante. Se trata de una manera común de integrar SKCanvasViewcon otras Xamarin.Forms vistas:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.PulsatingEllipsePage"
             Title="Pulsating Ellipse">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Slider x:Name="slider"
                Grid.Row="0"
                Maximum="10"
                Minimum="0.1"
                Value="5"
                Margin="20, 0" />

        <Label Grid.Row="1"
               Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Cycle time = {0:F1} seconds'}"
               HorizontalTextAlignment="Center" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="2"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

El archivo de código subyacente crea una instancia de un objeto Stopwatch que sirve como reloj de alta precisión. La OnAppearing invalidación establece el pageIsActive campo en true y llama a un método denominado AnimationLoop. La OnDisappearing invalidación establece ese pageIsActivecampo en false:

Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float scale;            // ranges from 0 to 1 to 0

public PulsatingEllipsePage()
{
    InitializeComponent();
}

protected override void OnAppearing()
{
    base.OnAppearing();
    pageIsActive = true;
    AnimationLoop();
}

protected override void OnDisappearing()
{
    base.OnDisappearing();
    pageIsActive = false;
}

El AnimationLoop método inicia Stopwatch y, a continuación, recorre en bucle mientras pageIsActive es true. Esto es básicamente un "bucle infinito" mientras la página está activa, pero no hace que el programa se bloquee porque el bucle concluye con una llamada a Task.Delay con el await operador, lo que permite que otras partes de la función del programa. El argumento para Task.Delay que se complete después del 1/30 segundo. Esto define la velocidad de fotogramas de la animación.

async Task AnimationLoop()
{
    stopwatch.Start();

    while (pageIsActive)
    {
        double cycleTime = slider.Value;
        double t = stopwatch.Elapsed.TotalSeconds % cycleTime / cycleTime;
        scale = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2;
        canvasView.InvalidateSurface();
        await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
    }

    stopwatch.Stop();
}

El bucle while comienza obteniendo un tiempo de ciclo del Slider. Este es un tiempo en segundos, por ejemplo, 5. La segunda instrucción calcula un valor de t para tiempo. Para un cycleTime de 5, t aumenta de 0 a 1 cada 5 segundos. El argumento de la función en la Math.Sin segunda instrucción oscila entre 0 y 2π cada 5 segundos. La Math.Sin función devuelve un valor comprendido entre 0 y 1 de nuevo a 0 y, a continuación, a –1 y 0 cada 5 segundos, pero con valores que cambian más lentamente cuando el valor está cerca de 1 o –1. El valor 1 se agrega para que los valores sean siempre positivos y, a continuación, se divide en 2, por lo que los valores van de ½ a 1 a ½ 0 a ½, pero más lento cuando el valor es alrededor de 1 y 0. Esto se almacena en el scale campo y el SKCanvasView se invalida.

El PaintSurface método usa este scale valor para calcular los dos ejes de la elipse:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float maxRadius = 0.75f * Math.Min(info.Width, info.Height) / 2;
    float minRadius = 0.25f * maxRadius;

    float xRadius = minRadius * scale + maxRadius * (1 - scale);
    float yRadius = maxRadius * scale + minRadius * (1 - scale);

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.Color = SKColors.Blue;
        paint.StrokeWidth = 50;
        canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);

        paint.Style = SKPaintStyle.Fill;
        paint.Color = SKColors.SkyBlue;
        canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
    }
}

El método calcula un radio máximo basado en el tamaño del área de visualización y un radio mínimo basado en el radio máximo. El scale valor se anima entre 0 y 1 y de nuevo a 0, por lo que el método lo usa para calcular un xRadius y yRadius que oscila entre minRadius y maxRadius. Estos valores se usan para dibujar y rellenar una elipse:

Triple screenshot of the Pulsating Ellipse page

Observe que el SKPaint objeto se crea en un using bloque. Al igual que muchas clases SkiaSharp SKPaint derivan de SKObject, que se deriva de SKNativeObject, que implementa la IDisposable interfaz. SKPaint invalida el Dispose método para liberar recursos no administrados.

Colocar SKPaint en un using bloque garantiza que Dispose se llame al final del bloque para liberar estos recursos no administrados. Esto sucede de todos modos cuando el recolector de elementos no utilizados de .NET libera la memoria usada por el objeto SKPaint, pero en el código de animación, es mejor liberar memoria de forma más ordenada.

Una mejor solución en este caso concreto sería crear dos objetos SKPaint una vez y guardarlos como campos.

Eso es lo que hace la animación de Expandir círculos. La clase ExpandingCirclesPage comienza definiendo varios campos, incluido un objeto SKPaint:

public class ExpandingCirclesPage : ContentPage
{
    const double cycleTime = 1000;       // in milliseconds

    SKCanvasView canvasView;
    Stopwatch stopwatch = new Stopwatch();
    bool pageIsActive;
    float t;
    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke
    };

    public ExpandingCirclesPage()
    {
        Title = "Expanding Circles";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ...
}

Este programa usa un enfoque diferente para la animación en función del Xamarin.FormsDevice.StartTimer método. El t campo se anima de 0 a 1 cada cycleTime milisegundos:

public class ExpandingCirclesPage : ContentPage
{
    ...
    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;
        stopwatch.Start();

        Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
        {
            t = (float)(stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime);
            canvasView.InvalidateSurface();

            if (!pageIsActive)
            {
                stopwatch.Stop();
            }
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

El PaintSurface controlador dibuja cinco círculos concéntricos con radios animados. Si la baseRadius variable se calcula como 100, como t se anima de 0 a 1, los radios de los cinco círculos aumentan de 0 a 100, de 100 a 200, de 200 a 300, de 300 a 400 y de 400 a 500. Para la mayoría de los círculos, el strokeWidth es 50, pero para el primer círculo, el strokeWidth anima de 0 a 50. Para la mayoría de los círculos, el color es azul, pero para el último círculo, el color se anima de azul a transparente. Observe el cuarto argumento para el constructor SKColor que especifica la opacidad:

public class ExpandingCirclesPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
        float baseRadius = Math.Min(info.Width, info.Height) / 12;

        for (int circle = 0; circle < 5; circle++)
        {
            float radius = baseRadius * (circle + t);

            paint.StrokeWidth = baseRadius / 2 * (circle == 0 ? t : 1);
            paint.Color = new SKColor(0, 0, 255,
                (byte)(255 * (circle == 4 ? (1 - t) : 1)));

            canvas.DrawCircle(center.X, center.Y, radius, paint);
        }
    }
}

El resultado es que la imagen tiene el mismo aspecto cuando t es igual a 0 cuando t es igual a 1 y los círculos parecen seguir expandiéndose para siempre:

Triple screenshot of the Expanding Circles page