Animación básica en SkiaSharp
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:
La página deElipse pulsante en el programa de ejemplo anima los dos ejes de una elipse para que parezca ser pulsado, 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 SKCanvasView
con 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 pageIsActive
campo 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:
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: