Sdílet prostřednictvím


Animace rastrových obrázků SkiaSharp

Aplikace, které animují grafiku SkiaSharp obecně volají InvalidateSurface SKCanvasView s pevnou rychlostí, často každých 16 milisekund. Zrušením platnosti povrchu se aktivuje volání PaintSurface obslužné rutiny pro překreslení zobrazení. Jak se vizuály překreslují 60krát za sekundu, zdá se, že jsou hladce animované.

Pokud je však grafika příliš složitá, aby se vykreslila v 16 milisekundách, animace se může stát zadržovací. Programátor se může rozhodnout snížit frekvenci aktualizace na 30krát nebo 15krát za sekundu, ale někdy ani to nestačí. Někdy jsou grafika tak složitá, že se jednoduše nedají vykreslit v reálném čase.

Jedním z řešení je připravit se na animaci předem vykreslením jednotlivých snímků animace na řadu rastrových obrázků. Pokud chcete animaci zobrazit, je nutné tyto rastrové obrázky zobrazit postupně 60krát za sekundu.

Samozřejmě, to je potenciálně hodně rastrových obrázků, ale to je to, jak velké-rozpočet 3D animované filmy jsou vyrobeny. 3D grafika je příliš složitá, aby se vykreslovala v reálném čase. K vykreslení každého rámce se vyžaduje velké množství času zpracování. To, co vidíte, když se díváte na film, je v podstatě řada rastrových obrázků.

V SkiaSharpu můžete udělat něco podobného. Tento článek ukazuje dva typy rastrové animace. První příklad je animace sady Mandelbrot:

Animace ukázky

Druhý příklad ukazuje, jak pomocí SkiaSharp vykreslit animovaný soubor GIF.

Rastrová animace

Mandelbrot Set je vizuálně fascinující, ale složitě zdlouhavý. (Diskuzi o mandelbrotové sadě a matematice, které zde používáte, najdete v tématu Kapitola 20 vytváření mobilních aplikací počínaje Xamarin.Forms stránkou 666. Následující popis předpokládá, že znalosti na pozadí.)

Ukázka používá rastrovou animaci k simulaci nepřetržitého přiblížení pevného bodu v mandelbrotové sadě. Za přiblížením následuje oddálení a potom se cyklus opakuje navždy nebo dokud program neskončíte.

Program se připraví na tuto animaci vytvořením až 50 rastrových obrázků, které ukládá v místním úložišti aplikace. Každý rastrový obrázek zahrnuje polovinu šířky a výšky komplexní roviny jako předchozí rastrový obrázek. (V programu se tyto rastrové obrázky označují jako celočíselné úrovně přiblížení.) Rastrové obrázky se pak zobrazí v posloupnosti. Měřítko každého rastrového obrázku je animované, aby poskytovalo hladký průběh od jednoho rastrového obrázku k druhému.

Stejně jako poslední program popsaný v kapitole 20 vytváření mobilních aplikací s Xamarin.Forms, výpočet Mandelbrot Set v Mandelbrot Animace je asynchronní metoda s osmi parametry. Parametry zahrnují komplexní středový bod a šířku a výšku komplexní roviny kolem tohoto středového bodu. Další tři parametry jsou šířka a výška rastrového obrázku, které se mají vytvořit, a maximální počet iterací pro rekurzivní výpočet. Parametr progress slouží k zobrazení průběhu tohoto výpočtu. Parametr cancelToken se v tomto programu nepoužívá:

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 vrátí objekt typu BitmapInfo , který poskytuje informace pro vytvoření rastrového obrázku:

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

Soubor XAML animace Mandelbrot obsahuje dvě Label zobrazení, ProgressBara a také 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>

Soubor s kódem začíná definováním tří zásadních konstant a polem rastrových obrázků:

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

V určitém okamžiku budete pravděpodobně chtít změnit COUNT hodnotu na 50, abyste viděli celý rozsah animace. Hodnoty nad 50 nejsou užitečné. Kolem úrovně přiblížení 48 nebo tak se rozlišení čísel s plovoucí desetinnou čárkou s dvojitou přesností stává nedostatečným pro výpočet sady Mandelbrot. Tento problém je popsán na stránce 684 vytváření mobilních aplikací s Xamarin.Forms.

Hodnota center je velmi důležitá. Toto je fokus přiblížení animace. Tři hodnoty v souboru se používají na třech konečných snímcích obrazovky kapitoly 20 vytváření mobilních aplikací na Xamarin.Forms stránce 684, ale můžete experimentovat s programem v této kapitole a přijít s jednou z vlastních hodnot.

Ukázka animace Mandelbrot ukládá tyto COUNT rastrové obrázky do místního úložiště aplikací. Padesát rastrových obrázků vyžaduje na vašem zařízení více než 20 megabajtů úložiště, takže můžete chtít vědět, kolik úložiště tyto rastrové obrázky zabírají, a v určitém okamžiku je možná budete chtít odstranit všechny. To je účel těchto dvou metod v dolní části MainPage třídy:

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

Rastrové obrázky můžete odstranit v místním úložišti, zatímco program animuje stejné rastrové obrázky, protože program je uchovává v paměti. Ale při příštím spuštění programu bude nutné znovu vytvořit rastrové obrázky.

Rastrové obrázky uložené v místním úložišti aplikací obsahují center hodnotu v jejich souborech, takže pokud změníte center nastavení, stávající rastrové obrázky nebudou nahrazeny v úložišti a budou dál zabírat místo.

Tady jsou metody, které MainPage používají k vytváření názvů souborů, a také metodu MakePixel pro definování hodnoty pixelu na základě barevných komponent:

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 , který se pohybuje FilePath od 0 do COUNT konstanty minus 1.

Konstruktor MainPage volá metodu LoadAndStartAnimation :

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

        LoadAndStartAnimation();
    }
    ···
}

Tato LoadAndStartAnimation metoda zodpovídá za přístup k místnímu úložišti aplikace za načtení rastrových obrázků, které mohly být vytvořeny při předchozím spuštění programu. zoomLevel Prochází hodnoty od 0 do COUNT. Pokud soubor existuje, načte ho do bitmaps pole. V opačném případě musí vytvořit rastr pro konkrétní center a zoomLevel hodnoty voláním Mandelbrot.CalculateAsync. Tato metoda získá počet iterací pro každý pixel, který tato metoda převede na barvy:

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

Všimněte si, že program ukládá tyto rastrové obrázky v místním úložišti aplikací, nikoli v knihovně fotek zařízení. Knihovna .NET Standard 2.0 umožňuje používat známé File.OpenRead a File.WriteAllBytes metody pro tuto úlohu.

Po vytvoření nebo načtení všech rastrových obrázků do paměti metoda spustí Stopwatch objekt a volá Device.StartTimer. Metoda OnTimerTick se nazývá každých 16 milisekund.

OnTimerTicktime vypočítá hodnotu v milisekundách, která se pohybuje od 0 do 6000 krát COUNT, což vypočítá šest sekund pro zobrazení každého rastrového obrázku. Hodnota progress používá Math.Sin hodnotu k vytvoření sinusoidální animace, která bude pomalejší na začátku cyklu a pomalejší na konci, jak se obrátí směr.

Hodnota progress se pohybuje od 0 do COUNT. To znamená, že celočíselná část progress je index do bitmaps pole, zatímco zlomková část progress označuje úroveň přiblížení pro daný rastrový obrázek. Tyto hodnoty jsou uloženy v bitmapIndex polích a bitmapProgress jsou zobrazeny pomocí Label souboru XAML a Slider v souboru XAML. Aktualizace SKCanvasView rastrového obrázku je neplatná:

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

Nakonec obslužná PaintSurface rutina SKCanvasView vypočítá cílový obdélník tak, aby se rastrový obrázek co nejvíce zobrazil při zachování poměru stran. Zdrojový obdélník je založen na hodnotě bitmapProgress . Hodnota fraction vypočítaná zde se pohybuje od 0, když bitmapProgress je 0 pro zobrazení celého rastrového obrázku, až 0,25, pokud bitmapProgress je 1, aby se zobrazila polovina šířky a výšky rastrového obrázku, a efektivně se přibližuje:

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

Tady je spuštěný program:

Animace Mandelbrot

Animace VE FORMÁTU GIF

Specifikace GIF (Graphics Interchange Format) obsahuje funkci, která umožňuje jednomu souboru GIF obsahovat několik sekvenčních snímků scény, které se dají zobrazit postupně, často ve smyčce. Tyto soubory se označují jako animované GIFy. Webové prohlížeče můžou přehrávat animované GIFy a SkiaSharp umožňuje aplikaci extrahovat snímky z animovaného souboru GIF a zobrazit je postupně.

Ukázka obsahuje animovaný prostředek GIF s názvem Newtons_cradle_animation_book_2.gif vytvořený DemonDeLuxe a stažený ze stránky Kolébka Newtonu na Wikipedii. Stránka Animovaný gif obsahuje soubor XAML, který poskytuje tyto informace a vytvoří SKCanvasViewinstanci:

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

Soubor za kódem není zobecněn pro přehrání žádného animovaného souboru GIF. Ignoruje některé informace, které jsou k dispozici, zejména počet opakování, a jednoduše přehraje animovaný gif ve smyčce.

Použití SkisSharp k extrakci snímků animovaného souboru GIF se zdá být zdokumentované nikde, takže popis kódu, který následuje, je podrobnější než obvykle:

Dekódování animovaného souboru GIF se vyskytuje v konstruktoru stránky a vyžaduje, aby Stream se objekt odkazující na rastrový obrázek použil k vytvoření objektu SKManagedStream a objektu SKCodec . Vlastnost FrameCount označuje počet snímků, které tvoří animaci.

Tyto snímky jsou nakonec uloženy jako jednotlivé rastrové obrázky, takže konstruktor používá FrameCount k přidělení pole typu SKBitmap a také dvě int matice po dobu trvání každého snímku a (pro usnadnění logiky animace) kumulované doby trvání.

Vlastnost FrameInfo SKCodec třídy je matice SKCodecFrameInfo hodnot, jedna pro každý rámec, ale jediná věc, kterou tento program přebírá z této struktury, je Duration rámec v milisekundách.

SKCodec definuje vlastnost s názvem Info typu SKImageInfo, ale tato SKImageInfo hodnota označuje (alespoň pro tento obrázek), že typ barvy je SKColorType.Index8, což znamená, že každý pixel je index do typu barvy. Aby se zabránilo obtěžování s tabulkami barev, program použije Width k vytvoření vlastní celobarevné ImageInfo hodnoty a Height informace z této struktury. Každý z nich SKBitmap se vytvoří.

Metoda GetPixels SKBitmap vrátí IntPtr odkazující na pixelové bity tohoto rastrového obrázku. Tyto pixelové bity ještě nebyly nastaveny. To IntPtr je předáno jedné z GetPixels metod SKCodec. Tato metoda zkopíruje rámeček ze souboru GIF do paměťového prostoru, na který IntPtrodkazuje . Konstruktor SKCodecOptions označuje číslo rámce:

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

Bez ohledu na IntPtr hodnotu není vyžadován žádný unsafe kód, protože IntPtr se nikdy nepřevádí na hodnotu ukazatele jazyka C#.

Po extrakci každého rámce konstruktor součet doby trvání všech snímků a pak inicializuje další pole s kumulovanými dobami trvání.

Zbytek souboru s kódem je vyhrazený pro animaci. Metoda Device.StartTimer se používá ke spuštění časovače OnTimerTick a zpětné volání používá Stopwatch objekt k určení uplynulého času v milisekundách. Cyklické procházení pole kumulovaných dob trvání stačí k nalezení aktuálního rámce:

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

Pokaždé, když currentframe se proměnná změní, SKCanvasView zruší se platnost a zobrazí se nový rámec:

Animovaný obrázek VE FORMÁTU GIF

Samozřejmě budete chtít program spustit sami, abyste viděli animaci.