Condividi tramite


Animazione di base in SkiaSharp

Scopri come animare la grafica SkiaSharp

Puoi animare grafica SkiaSharp in Xamarin.Forms facendo in modo che il PaintSurface metodo venga chiamato periodicamente, ogni volta che disegna la grafica in modo leggermente diverso. Ecco un'animazione illustrata più avanti in questo articolo con cerchi concentrici che sembrano espandersi dal centro:

Diversi cerchi concentrici apparentemente espansi dal centro

La pagina Pulsating Ellipse nel programma di esempio anima i due assi di un'ellisse in modo che appaia pulsare, e si può anche controllare la frequenza di questa pulsazione. Il file PulsatingEllipsePage.xaml crea un'istanza Xamarin.FormsSlider di e un oggetto Label per visualizzare il valore corrente del dispositivo di scorrimento. Questo è un modo comune per integrare un oggetto SKCanvasView con altre Xamarin.Forms visualizzazioni:

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

Il file code-behind crea un'istanza di un Stopwatch oggetto da usare come orologio ad alta precisione. L'override OnAppearing imposta il pageIsActive campo su true e chiama un metodo denominato AnimationLoop. L'override OnDisappearing imposta tale pageIsActive campo su 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;
}

Il AnimationLoop metodo avvia Stopwatch e quindi esegue il ciclo mentre pageIsActive è true. Si tratta essenzialmente di un "ciclo infinito" mentre la pagina è attiva, ma non causa il blocco del programma perché il ciclo termina con una chiamata a Task.Delay con l'operatore await , che consente ad altre parti della funzione di programma. L'argomento per Task.Delay fare in modo che venga completato dopo 1/30° secondo. In questo modo viene definita la frequenza dei fotogrammi dell'animazione.

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

Il while ciclo inizia ottenendo un tempo di ciclo dall'oggetto Slider. Questo è un tempo in secondi, ad esempio 5. La seconda istruzione calcola un valore di t per il tempo. Per un cycleTime valore pari a 5, t aumenta da 0 a 1 ogni 5 secondi. L'argomento della Math.Sin funzione nella seconda istruzione varia da 0 a 2π ogni 5 secondi. La Math.Sin funzione restituisce un valore compreso tra 0 e 1 a 0 e quindi a –1 e 0 ogni 5 secondi, ma con valori che cambiano più lentamente quando il valore è vicino a 1 o -1. Il valore 1 viene aggiunto in modo che i valori siano sempre positivi e quindi divisi per 2, quindi i valori vanno da 1/2 a 1/2 a 1/2 a 0/2, ma più lenti quando il valore è intorno a 1 e 0. Viene archiviato nel scale campo e l'oggetto SKCanvasView viene invalidato.

Il PaintSurface metodo usa questo scale valore per calcolare i due assi dell'ellisse:

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

Il metodo calcola un raggio massimo in base alle dimensioni dell'area di visualizzazione e un raggio minimo in base al raggio massimo. Il scale valore viene animato tra 0 e 1 e torna a 0, quindi il metodo usa tale valore per calcolare un oggetto xRadius e yRadius che è compreso tra minRadius e maxRadius. Questi valori vengono usati per disegnare e riempire un'ellisse:

Screenshot triplo della pagina Pulsating Ellipse

Si noti che l'oggetto SKPaint viene creato in un using blocco. Come molte classi SKPaint SkiaSharp derivano da SKObject, che deriva da SKNativeObject, che implementa l'interfaccia IDisposable . SKPaint esegue l'override del Dispose metodo per rilasciare risorse non gestite.

L'inserimento SKPaint di un using blocco garantisce che Dispose venga chiamato alla fine del blocco per liberare queste risorse non gestite. Ciò accade comunque quando la memoria usata dall'oggetto SKPaint viene liberata dal Garbage Collector .NET, ma nel codice di animazione è preferibile essere proattiva nella liberare memoria in modo più ordinato.

Una soluzione migliore in questo caso specifico consiste nel creare due SKPaint oggetti una sola volta e salvarli come campi.

Questo è il funzionamento dell'animazione Cerchi espansi . La ExpandingCirclesPage classe inizia definendo diversi campi, tra cui un SKPaint oggetto :

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

Questo programma usa un approccio diverso all'animazione in base al Xamarin.FormsDevice.StartTimer metodo . Il t campo viene animato da 0 a 1 ogni cycleTime millisecondo:

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

Il PaintSurface gestore disegna cinque cerchi concentrici con raggi animati. Se la baseRadius variabile viene calcolata come 100, così come t viene animata da 0 a 1, i raggi dei cinque cerchi aumentano da 0 a 100, 100 a 200, 200 a 300, 300 a 400 e 400 a 500. Per la maggior parte dei cerchi è strokeWidth 50, ma per il primo cerchio, l'animazione strokeWidth va da 0 a 50. Per la maggior parte dei cerchi, il colore è blu, ma per l'ultimo cerchio il colore viene animato dal blu al trasparente. Si noti il quarto argomento del SKColor costruttore che specifica l'opacità:

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

Il risultato è che l'immagine ha lo stesso aspetto quando t è uguale a 0 come quando t è uguale a 1 e i cerchi sembrano continuare ad espandersi per sempre:

Screenshot triplo della pagina Cerchi di espansione