Condividi tramite


Animazione di bitmap SkiaSharp

Le applicazioni che animano la grafica SkiaSharp in genere chiamano InvalidateSurface su SKCanvasView a una velocità fissa, spesso ogni 16 millisecondi. Invalidando la superficie viene attivata una chiamata al PaintSurface gestore per ridisegnare la visualizzazione. Man mano che gli oggetti visivi vengono ridisegnati 60 volte al secondo, sembrano essere animati senza problemi.

Tuttavia, se il rendering della grafica è troppo complesso in 16 millisecondi, l'animazione può diventare instabilità. Il programmatore potrebbe scegliere di ridurre la frequenza di aggiornamento a 30 volte o 15 volte al secondo, ma talvolta anche non è sufficiente. A volte la grafica è così complessa che non è possibile eseguire il rendering in tempo reale.

Una soluzione consiste nel preparare l'animazione in anticipo eseguendo il rendering dei singoli fotogrammi dell'animazione in una serie di bitmap. Per visualizzare l'animazione, è necessario visualizzare queste bitmap in sequenza 60 volte al secondo.

Naturalmente, questo è potenzialmente un sacco di bitmap, ma questo è il modo in cui vengono realizzati film animati 3D di grandi dimensioni. La grafica 3D è troppo complessa per essere sottoposta a rendering in tempo reale. Per eseguire il rendering di ogni fotogramma è necessario molto tempo di elaborazione. Quello che vedi quando guardi il film è essenzialmente una serie di bitmap.

Puoi fare qualcosa di simile in SkiaSharp. Questo articolo illustra due tipi di animazione bitmap. Il primo esempio è un'animazione del set Di Mandelbrot:

Esempio di animazione

Il secondo esempio mostra come usare SkiaSharp per eseguire il rendering di un file GIF animato.

Animazione bitmap

Il Set Di Mandelbrot è visivamente affascinante ma a livello di calcolo. (Per una discussione sul Set Di Mandelbrot e la matematica usata qui, vedere Capitolo 20 della creazione di app per dispositivi mobili con Xamarin.Forms a partire dalla pagina 666. La descrizione seguente presuppone che la conoscenza in background.

L'esempio usa l'animazione bitmap per simulare uno zoom continuo di un punto fisso nel set Di Mandelbrot. Lo zoom avanti è seguito dallo zoom indietro e quindi il ciclo si ripete per sempre o fino a quando non si termina il programma.

Il programma si prepara per questa animazione creando fino a 50 bitmap archiviate nell'archiviazione locale dell'applicazione. Ogni bitmap comprende metà della larghezza e dell'altezza del piano complesso come bitmap precedente. Nel programma queste bitmap rappresentano i livelli di zoom integrale. Le bitmap vengono quindi visualizzate in sequenza. Il ridimensionamento di ogni bitmap viene animato per fornire una progressione uniforme da una bitmap a un'altra.

Come il programma finale descritto nel capitolo 20 della creazione di app per dispositivi mobili con Xamarin.Forms, il calcolo del set Di Mandelbrot in Mandelbrot Animation è un metodo asincrono con otto parametri. I parametri includono un punto centrale complesso e una larghezza e altezza del piano complesso che circonda tale punto centrale. I tre parametri successivi sono la larghezza e l'altezza in pixel della bitmap da creare e un numero massimo di iterazioni per il calcolo ricorsivo. Il progress parametro viene utilizzato per visualizzare lo stato di avanzamento di questo calcolo. Il cancelToken parametro non viene usato in questo programma:

static class Mandelbrot
{
    public static Task<BitmapInfo> CalculateAsync(Complex center,
                                                  double width, double height,
                                                  int pixelWidth, int pixelHeight,
                                                  int iterations,
                                                  IProgress<double> progress,
                                                  CancellationToken cancelToken)
    {
        return Task.Run(() =>
        {
            int[] iterationCounts = new int[pixelWidth * pixelHeight];
            int index = 0;

            for (int row = 0; row < pixelHeight; row++)
            {
                progress.Report((double)row / pixelHeight);
                cancelToken.ThrowIfCancellationRequested();

                double y = center.Imaginary + height / 2 - row * height / pixelHeight;

                for (int col = 0; col < pixelWidth; col++)
                {
                    double x = center.Real - width / 2 + col * width / pixelWidth;
                    Complex c = new Complex(x, y);

                    if ((c - new Complex(-1, 0)).Magnitude < 1.0 / 4)
                    {
                        iterationCounts[index++] = -1;
                    }
                    // http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
                    else if (c.Magnitude * c.Magnitude * (8 * c.Magnitude * c.Magnitude - 3) < 3.0 / 32 - c.Real)
                    {
                        iterationCounts[index++] = -1;
                    }
                    else
                    {
                        Complex z = 0;
                        int iteration = 0;

                        do
                        {
                            z = z * z + c;
                            iteration++;
                        }
                        while (iteration < iterations && z.Magnitude < 2);

                        if (iteration == iterations)
                        {
                            iterationCounts[index++] = -1;
                        }
                        else
                        {
                            iterationCounts[index++] = iteration;
                        }
                    }
                }
            }
            return new BitmapInfo(pixelWidth, pixelHeight, iterationCounts);
        }, cancelToken);
    }
}

Il metodo restituisce un oggetto di tipo BitmapInfo che fornisce informazioni per la creazione di una bitmap:

class BitmapInfo
{
    public BitmapInfo(int pixelWidth, int pixelHeight, int[] iterationCounts)
    {
        PixelWidth = pixelWidth;
        PixelHeight = pixelHeight;
        IterationCounts = iterationCounts;
    }

    public int PixelWidth { private set; get; }

    public int PixelHeight { private set; get; }

    public int[] IterationCounts { private set; get; }
}

Il file XAML dell'animazione Di Mandelbrot include due Label visualizzazioni, un ProgressBare un ButtonSKCanvasViewe :

<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="MandelAnima.MainPage"
             Title="Mandelbrot Animation">

    <StackLayout>
        <Label x:Name="statusLabel"
               HorizontalTextAlignment="Center" />
        <ProgressBar x:Name="progressBar" />

        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <StackLayout Orientation="Horizontal"
                     Padding="5">
            <Label x:Name="storageLabel"
                   VerticalOptions="Center" />

            <Button x:Name="deleteButton"
                    Text="Delete All"
                    HorizontalOptions="EndAndExpand"
                    Clicked="OnDeleteButtonClicked" />
        </StackLayout>
    </StackLayout>
</ContentPage>

Il file code-behind inizia definendo tre costanti cruciali e una matrice di bitmap:

public partial class MainPage : ContentPage
{
    const int COUNT = 10;           // The number of bitmaps in the animation.
                                    // This can go up to 50!

    const int BITMAP_SIZE = 1000;   // Program uses square bitmaps exclusively

    // Uncomment just one of these, or define your own
    static readonly Complex center = new Complex(-1.17651152924355, 0.298520986549558);
    //   static readonly Complex center = new Complex(-0.774693089457127, 0.124226621261617);
    //   static readonly Complex center = new Complex(-0.556624880053304, 0.634696788141351);

    SKBitmap[] bitmaps = new SKBitmap[COUNT];   // array of bitmaps
    ···
}

A un certo punto, probabilmente vuoi modificare il COUNT valore su 50 per visualizzare l'intera gamma dell'animazione. I valori superiori a 50 non sono utili. Intorno a un livello di zoom di 48 o così via, la risoluzione dei numeri a virgola mobile a precisione doppia diventa insufficiente per il calcolo del set di Mandelbrot. Questo problema viene descritto nella pagina 684 di Creazione di app per dispositivi mobili con Xamarin.Forms.

Il center valore è molto importante. Questo è lo stato attivo dello zoom dell'animazione. I tre valori nel file sono quelli usati nelle tre schermate finali del capitolo 20 di Creazione di app per dispositivi mobili con Xamarin.Forms nella pagina 684, ma è possibile sperimentare il programma in quel capitolo per ottenere uno dei propri valori.

L'esempio Di animazione Di Mandelbrot archivia queste COUNT bitmap nell'archiviazione dell'applicazione locale. Cinquanta bitmap richiedono più di 20 megabyte di spazio di archiviazione nel dispositivo, quindi potresti voler sapere quanto spazio di archiviazione occupano queste bitmap e a un certo punto potresti voler eliminarle tutte. Questo è lo scopo di questi due metodi nella parte inferiore della MainPage classe:

public partial class MainPage : ContentPage
{
    ···
    void TallyBitmapSizes()
    {
        long fileSize = 0;

        foreach (string filename in Directory.EnumerateFiles(FolderPath()))
        {
            fileSize += new FileInfo(filename).Length;
        }

        storageLabel.Text = $"Total storage: {fileSize:N0} bytes";
    }

    void OnDeleteButtonClicked(object sender, EventArgs args)
    {
        foreach (string filepath in Directory.EnumerateFiles(FolderPath()))
        {
            File.Delete(filepath);
        }

        TallyBitmapSizes();
    }
}

È possibile eliminare le bitmap nell'archiviazione locale mentre il programma sta animando le stesse bitmap perché il programma li mantiene in memoria. Ma la prossima volta che esegui il programma, sarà necessario ricreare le bitmap.

Le bitmap archiviate nell'archiviazione dell'applicazione locale incorporano il center valore nei relativi nomi file, quindi se si modifica l'impostazione center , le bitmap esistenti non verranno sostituite nell'archiviazione e continueranno a occupare spazio.

Ecco i metodi usati MainPage per costruire i nomi file, nonché un MakePixel metodo per definire un valore pixel in base ai componenti di colore:

public partial class MainPage : ContentPage
{
    ···
    // File path for storing each bitmap in local storage
    string FolderPath() =>
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);

    string FilePath(int zoomLevel) =>
        Path.Combine(FolderPath(),
                     String.Format("R{0}I{1}Z{2:D2}.png", center.Real, center.Imaginary, zoomLevel));

    // Form bitmap pixel for Rgba8888 format
    uint MakePixel(byte alpha, byte red, byte green, byte blue) =>
        (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
    ···
}

Parametro zoomLevel da impostare FilePath tra 0 e la COUNT costante meno 1.

Il MainPage costruttore chiama il LoadAndStartAnimation metodo :

public partial class MainPage : ContentPage
{
    ···
    public MainPage()
    {
        InitializeComponent();

        LoadAndStartAnimation();
    }
    ···
}

Il LoadAndStartAnimation metodo è responsabile dell'accesso all'archiviazione locale dell'applicazione per caricare eventuali bitmap che potrebbero essere state create quando il programma è stato eseguito in precedenza. Esegue un ciclo tra zoomLevel i valori compresi tra 0 e COUNT. Se il file esiste, lo carica nella bitmaps matrice. In caso contrario, deve creare una bitmap per i valori e zoomLevel specifici center chiamando Mandelbrot.CalculateAsync. Questo metodo ottiene il numero di iterazioni per ogni pixel, che questo metodo converte in colori:

public partial class MainPage : ContentPage
{
    ···
    async void LoadAndStartAnimation()
    {
        // Show total bitmap storage
        TallyBitmapSizes();

        // Create progressReporter for async operation
        Progress<double> progressReporter =
            new Progress<double>((double progress) => progressBar.Progress = progress);

        // Create (unused) CancellationTokenSource for async operation
        CancellationTokenSource cancelTokenSource = new CancellationTokenSource();

        // Loop through all the zoom levels
        for (int zoomLevel = 0; zoomLevel < COUNT; zoomLevel++)
        {
            // If the file exists, load it
            if (File.Exists(FilePath(zoomLevel)))
            {
                statusLabel.Text = $"Loading bitmap for zoom level {zoomLevel}";

                using (Stream stream = File.OpenRead(FilePath(zoomLevel)))
                {
                    bitmaps[zoomLevel] = SKBitmap.Decode(stream);
                }
            }
            // Otherwise, create a new bitmap
            else
            {
                statusLabel.Text = $"Creating bitmap for zoom level {zoomLevel}";

                CancellationToken cancelToken = cancelTokenSource.Token;

                // Do the (generally lengthy) Mandelbrot calculation
                BitmapInfo bitmapInfo =
                    await Mandelbrot.CalculateAsync(center,
                                                    4 / Math.Pow(2, zoomLevel),
                                                    4 / Math.Pow(2, zoomLevel),
                                                    BITMAP_SIZE, BITMAP_SIZE,
                                                    (int)Math.Pow(2, 10), progressReporter, cancelToken);

                // Create bitmap & get pointer to the pixel bits
                SKBitmap bitmap = new SKBitmap(BITMAP_SIZE, BITMAP_SIZE, SKColorType.Rgba8888, SKAlphaType.Opaque);
                IntPtr basePtr = bitmap.GetPixels();

                // Set pixel bits to color based on iteration count
                for (int row = 0; row < bitmap.Width; row++)
                    for (int col = 0; col < bitmap.Height; col++)
                    {
                        int iterationCount = bitmapInfo.IterationCounts[row * bitmap.Width + col];
                        uint pixel = 0xFF000000;            // black

                        if (iterationCount != -1)
                        {
                            double proportion = (iterationCount / 32.0) % 1;
                            byte red = 0, green = 0, blue = 0;

                            if (proportion < 0.5)
                            {
                                red = (byte)(255 * (1 - 2 * proportion));
                                blue = (byte)(255 * 2 * proportion);
                            }
                            else
                            {
                                proportion = 2 * (proportion - 0.5);
                                green = (byte)(255 * proportion);
                                blue = (byte)(255 * (1 - proportion));
                            }

                            pixel = MakePixel(0xFF, red, green, blue);
                        }

                        // Calculate pointer to pixel
                        IntPtr pixelPtr = basePtr + 4 * (row * bitmap.Width + col);

                        unsafe     // requires compiling with unsafe flag
                        {
                            *(uint*)pixelPtr.ToPointer() = pixel;
                        }
                    }

                // Save as PNG file
                SKData data = SKImage.FromBitmap(bitmap).Encode();

                try
                {
                    File.WriteAllBytes(FilePath(zoomLevel), data.ToArray());
                }
                catch
                {
                    // Probably out of space, but just ignore
                }

                // Store in array
                bitmaps[zoomLevel] = bitmap;

                // Show new bitmap sizes
                TallyBitmapSizes();
            }

            // Display the bitmap
            bitmapIndex = zoomLevel;
            canvasView.InvalidateSurface();
        }

        // Now start the animation
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }
    ···
}

Si noti che il programma archivia queste bitmap nell'archiviazione dell'applicazione locale anziché nella libreria di foto del dispositivo. La libreria .NET Standard 2.0 consente di usare i metodi e File.WriteAllBytes familiari File.OpenRead per questa attività.

Dopo che tutte le bitmap sono state create o caricate in memoria, il metodo avvia un Stopwatch oggetto e chiama Device.StartTimer. Il OnTimerTick metodo viene chiamato ogni 16 millisecondi.

OnTimerTick calcola un time valore in millisecondi compreso tra 0 e 6000 volte COUNT, che ripartisce sei secondi per la visualizzazione di ogni bitmap. Il progress valore usa il Math.Sin valore per creare un'animazione sinusoidale che sarà più lenta all'inizio del ciclo e più lenta alla fine mentre inverte la direzione.

Il progress valore è compreso tra 0 e COUNT. Ciò significa che la parte integer di progress è un indice nella bitmaps matrice, mentre la parte frazionaria di indica un livello di progress zoom per tale bitmap specifica. Questi valori vengono archiviati nei bitmapIndex campi e bitmapProgress e vengono visualizzati da Label e Slider nel file XAML. L'oggetto SKCanvasView viene invalidato per aggiornare la visualizzazione bitmap:

public partial class MainPage : ContentPage
{
    ···
    Stopwatch stopwatch = new Stopwatch();      // for the animation
    int bitmapIndex;
    double bitmapProgress = 0;
    ···
    bool OnTimerTick()
    {
        int cycle = 6000 * COUNT;       // total cycle length in milliseconds

        // Time in milliseconds from 0 to cycle
        int time = (int)(stopwatch.ElapsedMilliseconds % cycle);

        // Make it sinusoidal, including bitmap index and gradation between bitmaps
        double progress = COUNT * 0.5 * (1 + Math.Sin(2 * Math.PI * time / cycle - Math.PI / 2));

        // These are the field values that the PaintSurface handler uses
        bitmapIndex = (int)progress;
        bitmapProgress = progress - bitmapIndex;

        // It doesn't often happen that we get up to COUNT, but an exception would be raised
        if (bitmapIndex < COUNT)
        {
            // Show progress in UI
            statusLabel.Text = $"Displaying bitmap for zoom level {bitmapIndex}";
            progressBar.Progress = bitmapProgress;

            // Update the canvas
            canvasView.InvalidateSurface();
        }

        return true;
    }
    ···
}

Infine, il PaintSurface gestore dell'oggetto SKCanvasView calcola un rettangolo di destinazione per visualizzare la bitmap il più grande possibile mantenendo le proporzioni. Un rettangolo di origine si basa sul bitmapProgress valore . Il fraction valore calcolato qui varia da 0 quando bitmapProgress è 0 per visualizzare l'intera bitmap, fino a 0,25 quando bitmapProgress è 1 per visualizzare metà della larghezza e altezza della bitmap, in modo efficace lo zoom avanti:

public partial class MainPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        if (bitmaps[bitmapIndex] != null)
        {
            // Determine destination rect as square in canvas
            int dimension = Math.Min(info.Width, info.Height);
            float x = (info.Width - dimension) / 2;
            float y = (info.Height - dimension) / 2;
            SKRect destRect = new SKRect(x, y, x + dimension, y + dimension);

            // Calculate source rectangle based on fraction:
            //  bitmapProgress == 0: full bitmap
            //  bitmapProgress == 1: half of length and width of bitmap
            float fraction = 0.5f * (1 - (float)Math.Pow(2, -bitmapProgress));
            SKBitmap bitmap = bitmaps[bitmapIndex];
            int width = bitmap.Width;
            int height = bitmap.Height;
            SKRect sourceRect = new SKRect(fraction * width, fraction * height,
                                           (1 - fraction) * width, (1 - fraction) * height);

            // Display the bitmap
            canvas.DrawBitmap(bitmap, sourceRect, destRect);
        }
    }
    ···
}

Ecco il programma in esecuzione:

Animazione Di Mandelbrot

Animazione GIF

La specifica GIF (Graphics Interchange Format) include una funzionalità che consente a un singolo file GIF di contenere più fotogrammi sequenziali di una scena che possono essere visualizzati in successione, spesso in un ciclo. Questi file sono noti come GIF animate. I Web browser possono riprodurre GIF animate e SkiaSharp consente a un'applicazione di estrarre i fotogrammi da un file GIF animato e di visualizzarli in sequenza.

L'esempio include una risorsa GIF animata denominata Newtons_cradle_animation_book_2.gif creata da DemonDe Luxe e scaricata dalla pagina Della culla di Newton in Wikipedia. La pagina GIF animata include un file XAML che fornisce le informazioni e crea un'istanza SKCanvasViewdi :

<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.Bitmaps.AnimatedGifPage"
             Title="Animated GIF">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="0"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Label Text="GIF file by DemonDeLuxe from Wikipedia Newton's Cradle page"
               Grid.Row="1"
               Margin="0, 5"
               HorizontalTextAlignment="Center" />
    </Grid>
</ContentPage>

Il file code-behind non è generalizzato per riprodurre alcun file GIF animato. Ignora alcune delle informazioni disponibili, in particolare un conteggio delle ripetizioni, e riproduce semplicemente la GIF animata in un ciclo.

L'uso di SkisSharp per estrarre i fotogrammi di un file GIF animato non sembra essere documentato da nessuna parte, quindi la descrizione del codice che segue è più dettagliata del solito:

La decodifica del file GIF animato si verifica nel costruttore della pagina e richiede che l'oggetto Stream che fa riferimento alla bitmap venga usato per creare un SKManagedStream oggetto e quindi un SKCodec oggetto . La FrameCount proprietà indica il numero di fotogrammi che costituiscono l'animazione.

Questi fotogrammi vengono infine salvati come singole bitmap, quindi il costruttore usa FrameCount per allocare una matrice di tipo SKBitmap e due int matrici per la durata di ogni fotogramma e (per semplificare la logica di animazione) le durate accumulate.

La FrameInfo proprietà della SKCodec classe è una matrice di SKCodecFrameInfo valori, una per ogni fotogramma, ma l'unica cosa che questo programma prende da tale struttura è il Duration del frame in millisecondi.

SKCodec definisce una proprietà denominata Info di tipo SKImageInfo, ma tale SKImageInfo valore indica (almeno per questa immagine) che il tipo di colore è SKColorType.Index8, il che significa che ogni pixel è un indice in un tipo di colore. Per evitare di disturbare le tabelle dei colori, il programma usa le Width informazioni e Height da tale struttura per costruire il proprio valore full-color ImageInfo . Ogni SKBitmap oggetto viene creato da questo.

Il GetPixels metodo di SKBitmap restituisce un IntPtr riferimento ai bit pixel di tale bitmap. Questi bit di pixel non sono ancora stati impostati. Che IntPtr viene passato a uno dei GetPixels metodi di SKCodec. Questo metodo copia il frame dal file GIF nello spazio di memoria a cui fa riferimento l'oggetto IntPtr. Il SKCodecOptions costruttore indica il numero di frame:

public partial class AnimatedGifPage : ContentPage
{
    SKBitmap[] bitmaps;
    int[] durations;
    int[] accumulatedDurations;
    int totalDuration;
    ···

    public AnimatedGifPage ()
    {
        InitializeComponent ();

        string resourceID = "SkiaSharpFormsDemos.Media.Newtons_cradle_animation_book_2.gif";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        using (SKManagedStream skStream = new SKManagedStream(stream))
        using (SKCodec codec = SKCodec.Create(skStream))
        {
            // Get frame count and allocate bitmaps
            int frameCount = codec.FrameCount;
            bitmaps = new SKBitmap[frameCount];
            durations = new int[frameCount];
            accumulatedDurations = new int[frameCount];

            // Note: There's also a RepetitionCount property of SKCodec not used here

            // Loop through the frames
            for (int frame = 0; frame < frameCount; frame++)
            {
                // From the FrameInfo collection, get the duration of each frame
                durations[frame] = codec.FrameInfo[frame].Duration;

                // Create a full-color bitmap for each frame
                SKImageInfo imageInfo = code.new SKImageInfo(codec.Info.Width, codec.Info.Height);
                bitmaps[frame] = new SKBitmap(imageInfo);

                // Get the address of the pixels in that bitmap
                IntPtr pointer = bitmaps[frame].GetPixels();

                // Create an SKCodecOptions value to specify the frame
                SKCodecOptions codecOptions = new SKCodecOptions(frame, false);

                // Copy pixels from the frame into the bitmap
                codec.GetPixels(imageInfo, pointer, codecOptions);
            }

            // Sum up the total duration
            for (int frame = 0; frame < durations.Length; frame++)
            {
                totalDuration += durations[frame];
            }

            // Calculate the accumulated durations
            for (int frame = 0; frame < durations.Length; frame++)
            {
                accumulatedDurations[frame] = durations[frame] +
                    (frame == 0 ? 0 : accumulatedDurations[frame - 1]);
            }
        }
    }
    ···
}

Nonostante il IntPtr valore, non è necessario alcun unsafe codice perché non IntPtr viene mai convertito in un valore puntatore C#.

Dopo l'estrazione di ogni fotogramma, il costruttore totalizza le durate di tutti i fotogrammi e quindi inizializza un'altra matrice con le durate accumulate.

Il resto del file code-behind è dedicato all'animazione. Il Device.StartTimer metodo viene usato per avviare un timer e il OnTimerTick callback usa un Stopwatch oggetto per determinare il tempo trascorso in millisecondi. Il ciclo attraverso la matrice di durate accumulate è sufficiente per trovare il frame corrente:

public partial class AnimatedGifPage : ContentPage
{
    SKBitmap[] bitmaps;
    int[] durations;
    int[] accumulatedDurations;
    int totalDuration;

    Stopwatch stopwatch = new Stopwatch();
    bool isAnimating;

    int currentFrame;
    ···
    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        int msec = (int)(stopwatch.ElapsedMilliseconds % totalDuration);
        int frame = 0;

        // Find the frame based on the elapsed time
        for (frame = 0; frame < accumulatedDurations.Length; frame++)
        {
            if (msec < accumulatedDurations[frame])
            {
                break;
            }
        }

        // Save in a field and invalidate the SKCanvasView.
        if (currentFrame != frame)
        {
            currentFrame = frame;
            canvasView.InvalidateSurface();
        }

        return isAnimating;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Black);

        // Get the bitmap and center it
        SKBitmap bitmap = bitmaps[currentFrame];
        canvas.DrawBitmap(bitmap,info.Rect, BitmapStretch.Uniform);
    }
}

Ogni volta che la currentframe variabile cambia, viene SKCanvasView invalidata e viene visualizzato il nuovo frame:

GIF animata

Naturalmente, vuoi eseguire il programma manualmente per vedere l'animazione.