Partager via


Animation de base dans SkiaSharp

Découvrez comment animer vos graphiques SkiaSharp

Vous pouvez animer des graphiques SkiaSharp en Xamarin.Forms provoquant l’appel périodique de la PaintSurface méthode, chaque fois que vous dessinez les graphiques différemment. Voici une animation présentée plus loin dans cet article avec des cercles concentriques qui semblent s’étendre à partir du centre :

Plusieurs cercles concentriques semblent s’étendre du centre

La page Pulsating Ellipse dans l’exemple de programme anime les deux axes d’un ellipse afin qu’il semble pulser, et vous pouvez même contrôler le taux de cette pulsation. Le fichier PulsatingEllipsePage.xaml instancie a Xamarin.FormsSlider et a Label pour afficher la valeur actuelle du curseur. Il s’agit d’un moyen courant d’intégrer une SKCanvasView vue à d’autres Xamarin.Forms vues :

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

Le fichier code-behind instancie un Stopwatch objet pour servir d’horloge haute précision. Le OnAppearing remplacement définit le pageIsActive champ sur true et appelle une méthode nommée AnimationLoop. Le OnDisappearing remplacement définit ce pageIsActive champ sur 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;
}

La AnimationLoop méthode démarre les Stopwatch boucles, puis est pageIsActivetrue. Il s’agit essentiellement d’une « boucle infinie » alors que la page est active, mais elle n’entraîne pas le blocage du programme, car la boucle se termine par un appel à l’opérateurawait, ce qui permet à Task.Delay d’autres parties de la fonction de programme. L’argument pour Task.Delay qu’il se termine après 1/30e seconde. Cela définit la fréquence d’images de l’animation.

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

La while boucle commence par obtenir une durée de cycle à partir du Slider. Il s’agit d’une durée en secondes, par exemple 5. La deuxième instruction calcule une valeur pour t le temps. Pour un cycleTime de 5, t passe de 0 à 1 toutes les 5 secondes. L’argument de la Math.Sin fonction dans la deuxième instruction est comprise entre 0 et 2π toutes les 5 secondes. La Math.Sin fonction retourne une valeur comprise entre 0 et 1 et 0, puis –1 et 0 toutes les 5 secondes, mais avec des valeurs qui changent plus lentement lorsque la valeur est proche de 1 ou –1. La valeur 1 est ajoutée afin que les valeurs soient toujours positives, puis divisées par 2, de sorte que les valeurs varient de 1/2 à 1/2 à 0 à 1/2, mais plus lentes lorsque la valeur est d’environ 1 et 0. Il est stocké dans le scale champ et il SKCanvasView est invalidé.

La PaintSurface méthode utilise cette scale valeur pour calculer les deux axes de l’ellipse :

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

La méthode calcule un rayon maximal en fonction de la taille de la zone d’affichage et d’un rayon minimal basé sur le rayon maximal. La scale valeur est animée entre 0 et 1 et 0, de sorte que la méthode utilise cette méthode pour calculer une xRadius plage comprise yRadius entre minRadius et maxRadius. Ces valeurs sont utilisées pour dessiner et remplir un ellipse :

Capture d’écran triple de la page Pulsating Ellipse

Notez que l’objet SKPaint est créé dans un using bloc. Comme de nombreuses classes SKPaint SkiaSharp dérivent de SKObject, qui dérive de SKNativeObject, qui implémente l’interface IDisposable . SKPaint remplace la Dispose méthode pour libérer des ressources non managées.

La mise SKPaint en place d’un using bloc garantit qu’elle Dispose est appelée à la fin du bloc pour libérer ces ressources non managées. Cela se produit quand même lorsque la mémoire utilisée par l’objet SKPaint est libérée par le garbage collector .NET, mais dans le code d’animation, il est préférable d’être proactif dans la libération de la mémoire de manière plus ordonnée.

Dans ce cas particulier, une meilleure solution consisterait à créer deux SKPaint objets une seule fois et à les enregistrer sous forme de champs.

C’est ce que fait l’animation Des cercles de développement. La ExpandingCirclesPage classe commence par définir plusieurs champs, y compris un SKPaint objet :

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

Ce programme utilise une approche différente de l’animation basée sur la Xamarin.FormsDevice.StartTimer méthode. Le t champ est animé de 0 à 1 toutes les cycleTime millisecondes :

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

Le PaintSurface gestionnaire dessine cinq cercles concentriques avec des rayons animés. Si la baseRadius variable est calculée comme étant 100, comme t elle est animée de 0 à 1, le rayon des cinq cercles passe de 0 à 100, 100 à 200, 200 à 300, 300 à 400 et 400 à 500. Pour la plupart des cercles, la valeur strokeWidth est de 50, mais pour le premier cercle, les strokeWidth anime de 0 à 50. Pour la plupart des cercles, la couleur est bleue, mais pour le dernier cercle, la couleur est animée de bleu à transparent. Notez le quatrième argument du SKColor constructeur qui spécifie 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);
        }
    }
}

Le résultat est que l’image ressemble au même quand t elle est égale à 0 que lorsqu’elle t est égale à 1, et les cercles semblent continuer à s’étendre pour toujours :

Capture d’écran triple de la page Développer des cercles