Compartilhar via


Animação básica no SkiaSharp

Descubra como animar seus gráficos SkiaSharp

Você pode animar gráficos SkiaSharp fazendo Xamarin.Forms com que o PaintSurface método seja chamado periodicamente, cada vez desenhando os gráficos de forma um pouco diferente. Aqui está uma animação mostrada mais adiante neste artigo com círculos concêntricos que aparentemente se expandem a partir do centro:

Vários círculos concêntricos aparentemente se expandindo a partir do centro

A página Elipse Pulsante no programa de exemplo anima os dois eixos de uma elipse para que ela pareça estar pulsando, e você pode até controlar a taxa dessa pulsação. O arquivo PulsatingEllipsePage.xaml instancia a Xamarin.FormsSlider e a Label para exibir o valor atual do controle deslizante. Esta é uma maneira comum de integrar um SKCanvasView com outros Xamarin.Forms modos de exibição:

<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>

O arquivo code-behind instancia um Stopwatch objeto para servir como um relógio de alta precisão. A OnAppearing substituição define o pageIsActive campo como true e chama um método chamado AnimationLoop. A OnDisappearing substituição define esse pageIsActive campo como 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;
}

O AnimationLoop método inicia o Stopwatch e, em seguida, loops enquanto pageIsActive é true. Isso é essencialmente um "loop infinito" enquanto a página está ativa, mas não faz com que o programa trave porque o loop termina com uma chamada para Task.Delay com o await operador, o que permite que outras partes do programa funcionem. O argumento faz Task.Delay com que ele seja concluído após 1/30 segundo. Isso define a taxa de quadros da animação.

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();
}

O while loop começa obtendo um tempo de ciclo do Slider. Este é um tempo em segundos, por exemplo, 5. A segunda instrução calcula um valor de t para o tempo. Para um cycleTime de 5, t aumenta de 0 para 1 a cada 5 segundos. O argumento para a Math.Sin função na segunda instrução varia de 0 a 2π a cada 5 segundos. A Math.Sin função retorna um valor que varia de 0 a 1 de volta para 0 e, em seguida, para –1 e 0 a cada 5 segundos, mas com valores que mudam mais lentamente quando o valor está perto de 1 ou –1. O valor 1 é adicionado para que os valores sejam sempre positivos, e então é dividido por 2, então os valores variam de 1/2 a 1 a 1/2 a 0 a 1/2, mas mais lentamente quando o valor está em torno de 1 e 0. Isso é armazenado no scale campo e o SKCanvasView é invalidado.

O PaintSurface método usa esse scale valor para calcular os dois eixos da 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);
    }
}

O método calcula um raio máximo com base no tamanho da área de exibição e um raio mínimo com base no raio máximo. O scale valor é animado entre 0 e 1 e volta para 0, portanto, o método usa isso para calcular um xRadius e yRadius que varia entre minRadius e maxRadius. Esses valores são usados para desenhar e preencher uma elipse:

Captura de tela tripla da página Elipse Pulsante

Observe que o SKPaint objeto é criado em um using bloco. Como muitas classes SKPaint SkiaSharp deriva de SKObject, que deriva de SKNativeObject, que implementa a IDisposable interface. SKPaint Substitui o Dispose método para liberar recursos não gerenciados.

Colocar SKPaint um using bloco garante que Dispose seja chamado no final do bloco para liberar esses recursos não gerenciados. Isso acontece de qualquer maneira quando a SKPaint memória usada pelo objeto é liberada pelo coletor de lixo .NET, mas no código de animação, é melhor ser proativo na liberação de memória de uma maneira mais ordenada.

Uma solução melhor neste caso específico seria criar dois SKPaint objetos uma vez e salvá-los como campos.

É isso que a animação Expandindo Círculos faz. A ExpandingCirclesPage classe começa definindo vários campos, incluindo um SKPaint objeto:

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 uma abordagem diferente para animação com base no Xamarin.FormsDevice.StartTimer método. O t campo é animado de 0 a 1 a cada cycleTime milissegundos:

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;
    }
    ...
}

O PaintSurface manipulador desenha cinco círculos concêntricos com raios animados. Se a baseRadius variável é calculada como 100, então como t é animado de 0 a 1, os raios dos cinco círculos aumentam de 0 a 100, 100 a 200, 200 a 300, 300 a 400 e 400 a 500. Para a maioria dos círculos o strokeWidth é 50, mas para o primeiro círculo, os strokeWidth animados de 0 a 50. Para a maioria dos círculos, a cor é azul, mas para o último círculo, a cor é animada de azul para transparente. Observe o quarto argumento para o SKColor construtor que especifica a opacidade:

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);
        }
    }
}

O resultado é que a imagem parece a mesma quando t é igual a 0 como quando t é igual a 1, e os círculos parecem continuar se expandindo para sempre:

Captura de tela tripla da página Expandindo círculos