Udostępnij za pośrednictwem


Animowanie map bitowych SkiaSharp

Aplikacje, które animują grafikę SkiaSharp, zwykle są wywoływane InvalidateSurface z SKCanvasView stałą szybkością, często co 16 milisekund. Unieważnienie powierzchni wyzwala wywołanie PaintSurface programu obsługi w celu ponownego wyrysowania wyświetlacza. Ponieważ wizualizacje są ponownie rysowane 60 razy na sekundę, wydają się być płynnie animowane.

Jeśli jednak grafika jest zbyt złożona do renderowania w ciągu 16 milisekund, animacja może stać się burzliwa. Programista może zdecydować się zmniejszyć częstotliwość odświeżania do 30 razy lub 15 razy na sekundę, ale czasami nawet to nie wystarczy. Czasami grafiki są tak złożone, że po prostu nie można ich renderować w czasie rzeczywistym.

Jednym z rozwiązań jest przygotowanie animacji wcześniej przez renderowanie pojedynczych ramek animacji na serii map bitowych. Aby wyświetlić animację, wystarczy wyświetlić te mapy bitowe sekwencyjnie 60 razy na sekundę.

Oczywiście jest to potencjalnie wiele map bitowych, ale tak tworzone są filmy animowane 3D w dużym budżecie. Grafika 3D jest zbyt złożona, aby można je było renderować w czasie rzeczywistym. Do renderowania każdej ramki jest wymagany dużo czasu przetwarzania. To, co widzisz podczas oglądania filmu, to zasadniczo seria map bitowych.

Możesz zrobić coś podobnego w skiaSharp. W tym artykule przedstawiono dwa typy animacji mapy bitowej. Pierwszy przykład to animacja zestawu Mandelbrot:

Przykład animowania

W drugim przykładzie pokazano, jak użyć narzędzia SkiaSharp do renderowania animowanego pliku GIF.

Animacja mapy bitowej

Zestaw Mandelbrot jest wizualnie fascynujący, ale computionally długi. (Aby zapoznać się z omówieniem zestawu Mandelbrot i matematyki używanej tutaj, zobacz Rozdział 20 Tworzenie aplikacji mobilnych , Xamarin.Forms począwszy od strony 666. W poniższym opisie przyjęto założenie, że wiedza w tle).

W przykładzie użyto animacji mapy bitowej do symulowania ciągłego powiększenia stałego punktu w zestawie Mandelbrot. Powiększenia następuje pomniejszenie, a następnie cykl powtarza się na zawsze lub do momentu zakończenia programu.

Program przygotowuje się do tej animacji, tworząc do 50 map bitowych przechowywanych w magazynie lokalnym aplikacji. Każda mapa bitowa obejmuje połowę szerokości i wysokości złożonej płaszczyzny jako poprzedniej mapy bitowej. (W programie mówi się, że mapy bitowe reprezentują całkowite poziomy powiększenia). Mapy bitowe są następnie wyświetlane w sekwencji. Skalowanie każdej mapy bitowej jest animowane w celu zapewnienia płynnego postępu z jednej mapy bitowej do innej.

Podobnie jak w ostatnim programie opisanym w rozdziale 20 tworzenie aplikacji mobilnych za pomocą Xamarin.Formsmetody , obliczenie zestawu Mandelbrot w animacji Mandelbrot jest metodą asynchroniczną z ośmioma parametrami. Parametry obejmują złożony punkt środkowy oraz szerokość i wysokość złożonej płaszczyzny otaczającej ten punkt środkowy. Następne trzy parametry to szerokość i wysokość mapy bitowej, która ma zostać utworzona, oraz maksymalna liczba iteracji dla obliczeń cyklicznych. Parametr progress służy do wyświetlania postępu tego obliczenia. Parametr cancelToken nie jest używany w tym programie:

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

Metoda zwraca obiekt typu BitmapInfo , który zawiera informacje dotyczące tworzenia mapy bitowej:

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

Plik XAML animacji Mandelbrot zawiera dwa Label widoki, a ProgressBartakże Button SKCanvasView:

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

Plik kodu rozpoczyna się od zdefiniowania trzech kluczowych stałych i tablicy map bitowych:

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

W pewnym momencie prawdopodobnie zechcesz zmienić COUNT wartość na 50, aby zobaczyć pełny zakres animacji. Wartości powyżej 50 nie są przydatne. Wokół poziomu powiększenia 48 lub tak rozdzielczość liczb zmiennoprzecinkowych o podwójnej precyzji staje się niewystarczająca dla obliczenia zestawu Mandelbrot. Ten problem został omówiony na stronie 684 tworzenia aplikacji mobilnych za pomocą polecenia Xamarin.Forms.

Wartość center jest bardzo ważna. Jest to fokus powiększenia animacji. Trzy wartości w pliku są używane w trzech ostatnich zrzutach ekranu w rozdziale 20 tworzenie aplikacji Xamarin.Forms mobilnych na stronie 684, ale możesz eksperymentować z programem w tym rozdziale, aby wymyślić jedną z własnych wartości.

Przykład Mandelbrot Animation przechowuje te COUNT mapy bitowe w lokalnym magazynie aplikacji. Pięćdziesiąt map bitowych wymaga ponad 20 megabajtów miejsca do magazynowania na urządzeniu, więc warto wiedzieć, ile miejsca zajmują te mapy bitowe, a w pewnym momencie możesz je usunąć. Jest to cel tych dwóch metod w dolnej części MainPage klasy:

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

Mapy bitowe można usunąć w magazynie lokalnym, gdy program animuje te same mapy bitowe, ponieważ program zachowuje je w pamięci. Jednak przy następnym uruchomieniu programu konieczne będzie ponowne utworzenie map bitowych.

Mapy bitowe przechowywane w lokalnym magazynie aplikacji zawierają center wartość w nazwach plików, więc jeśli zmienisz center ustawienie, istniejące mapy bitowe nie zostaną zastąpione w magazynie i będą nadal zajmować miejsce.

Poniżej przedstawiono metody używane MainPage do konstruowania nazw plików, a także MakePixel metody definiowania wartości pikseli na podstawie składników kolorów:

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

Parametr zoomLevel do FilePath zakresu od 0 do stałej COUNT minus 1.

Konstruktor MainPage wywołuje metodę LoadAndStartAnimation :

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

        LoadAndStartAnimation();
    }
    ···
}

Metoda LoadAndStartAnimation jest odpowiedzialna za uzyskiwanie dostępu do magazynu lokalnego aplikacji w celu załadowania wszelkich map bitowych, które mogły zostać utworzone podczas uruchamiania programu wcześniej. Wykonuje pętlę przez zoomLevel wartości z zakresu od 0 do COUNT. Jeśli plik istnieje, ładuje go do tablicy bitmaps . W przeciwnym razie należy utworzyć mapę bitową dla określonych center wartości i zoomLevel przez wywołanie metody Mandelbrot.CalculateAsync. Ta metoda uzyskuje liczbę iteracji dla każdego piksela, który ta metoda konwertuje na kolory:

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

Zwróć uwagę, że program przechowuje te mapy bitowe w lokalnym magazynie aplikacji, a nie w bibliotece zdjęć urządzenia. Biblioteka .NET Standard 2.0 umożliwia korzystanie ze znanych File.OpenRead metod i File.WriteAllBytes dla tego zadania.

Po utworzeniu lub załadowaniu wszystkich map bitowych do pamięci metoda uruchamia obiekt i wywołuje metodę Stopwatch Device.StartTimer. Metoda OnTimerTick jest wywoływana co 16 milisekund.

OnTimerTicktime Oblicza wartość w milisekundach, która waha się od 0 do 6000 razy COUNT, która dzieli sześć sekund na wyświetlanie każdej mapy bitowej. Wartość progress używa Math.Sin wartości do utworzenia animacji sinusoidalnej, która będzie wolniejsza na początku cyklu i wolniej na końcu, gdy odwraca kierunek.

Wartości progress wahają się od 0 do COUNT. Oznacza to, że część całkowita obiektu progress jest indeksem w tablicy bitmaps , a część ułamkowa progress elementu wskazuje poziom powiększenia dla tej konkretnej mapy bitowej. Te wartości są przechowywane w polach bitmapIndex i bitmapProgress i są wyświetlane przez Label element i Slider w pliku XAML. Właściwość SKCanvasView jest unieważniona w celu zaktualizowania ekranu mapy bitowej:

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

Na koniec program obsługi SKCanvasView oblicza prostokąt docelowy, PaintSurface aby wyświetlić mapę bitową tak dużą, jak to możliwe, przy zachowaniu współczynnika proporcji. Prostokąt źródłowy jest oparty na bitmapProgress wartości. Wartość fraction obliczona tutaj waha się od 0, gdy bitmapProgress ma wartość 0, aby wyświetlić całą mapę bitową, do 0,25, gdy bitmapProgress ma wartość 1, aby wyświetlić połowę szerokości i wysokości mapy bitowej, co skutecznie powiększa:

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

Oto uruchomiony program:

Animacja Mandelbrot

Animacja GIF

Specyfikacja Formatu wymiany grafiki (GIF) zawiera funkcję, która umożliwia pojedynczemu plikowi GIF zawierać wiele sekwencyjnych ramek sceny, które mogą być wyświetlane z rzędu, często w pętli. Te pliki są nazywane animowanymi plikami GIF. Przeglądarki internetowe mogą odtwarzać animowane pliki GIF, a SkiaSharp umożliwia aplikacji wyodrębnianie ramek z animowanego pliku GIF i wyświetlanie ich sekwencyjnie.

Przykład zawiera animowany zasób GIF o nazwie Newtons_cradle_animation_book_2.gif utworzony przez DemonDeLuxe i pobrany ze strony Kolebki Newtona w Wikipedii. Animowana strona GIF zawiera plik XAML, który udostępnia informacje i tworzy wystąpienie elementu SKCanvasView:

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

Plik związany z kodem nie jest uogólniony do odtwarzania żadnego animowanego pliku GIF. Ignoruje niektóre dostępne informacje, w szczególności liczbę powtórzeń i po prostu odtwarza animowany plik GIF w pętli.

Korzystanie z SkisSharp do wyodrębniania ramek animowanego pliku GIF nie wydaje się być udokumentowane w dowolnym miejscu, więc opis poniższego kodu jest bardziej szczegółowy niż zwykle:

Dekodowanie animowanego pliku GIF odbywa się w konstruktorze strony i wymaga, aby Stream obiekt odwołujący się do mapy bitowej był używany do utworzenia SKManagedStream obiektu, a następnie SKCodec obiektu. Właściwość FrameCount wskazuje liczbę ramek tworzących animację.

Te ramki są ostatecznie zapisywane jako poszczególne mapy bitowe, więc konstruktor używa FrameCount do przydzielenia tablicy typu SKBitmap , a także dwóch int tablic na czas trwania każdej ramki i (w celu ułatwienia logiki animacji) skumulowanych czasów trwania.

FrameInfo Właściwość SKCodec klasy jest tablicą SKCodecFrameInfo wartości, jedną dla każdej ramki, ale jedyną rzeczą, jaką ten program pobiera z tej struktury, jest Duration ramka w milisekundach.

SKCodec Definiuje właściwość o nazwie Info typu SKImageInfo, ale ta SKImageInfo wartość wskazuje (przynajmniej dla tego obrazu), że typ koloru to SKColorType.Index8, co oznacza, że każdy piksel jest indeksem typu koloru. Aby uniknąć przeszkadzania w tabelach kolorów, program używa informacji Width i Height z tej struktury, aby utworzyć własną wartość pełnokolorową ImageInfo . Każda z nich jest tworzona SKBitmap na podstawie tego.

Metoda GetPixels SKBitmap zwraca IntPtr odwołanie do bitów pikseli tej mapy bitowej. Te bity pikseli nie zostały jeszcze ustawione. Jest to IntPtr przekazywane do jednej z GetPixels metod SKCodec. Ta metoda kopiuje ramkę z pliku GIF do przestrzeni pamięci, do którego odwołuje się IntPtrelement . Konstruktor SKCodecOptions wskazuje numer ramki:

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

IntPtr Pomimo wartości żaden kod nie unsafe jest wymagany, ponieważ IntPtr nigdy nie jest konwertowany na wartość wskaźnika języka C#.

Po wyodrębnieniu każdej ramki konstruktor sumuje czas trwania wszystkich ramek, a następnie inicjuje kolejną tablicę ze skumulowanymi czasami trwania.

Pozostała część pliku za pomocą kodu jest przeznaczona do animacji. Metoda Device.StartTimer służy do uruchamiania czasomierza, a OnTimerTick wywołanie zwrotne używa Stopwatch obiektu w celu określenia czasu, który upłynął w milisekundach. Pętla przez skumulowaną tablicę czasu trwania jest wystarczająca do znalezienia bieżącej ramki:

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

Za każdym razem, gdy zmienna currentframe zmienia się, SKCanvasView jest unieważniona, a nowa ramka jest wyświetlana:

Animowany obraz GIF

Oczywiście chcesz uruchomić program samodzielnie, aby zobaczyć animację.