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