Share via


Animieren von SkiaSharp-Bitmaps

Anwendungen, die SkiaSharp-Grafiken animieren, rufen InvalidateSurface in der SKCanvasView Regel eine feste Rate auf, häufig alle 16 Millisekunden. Wenn die Oberfläche ungültig wird, wird ein Aufruf des PaintSurface Handlers ausgelöst, um die Anzeige neu zu zeichnen. Da die visuellen Elemente 60 mal pro Sekunde neu gezeichnet werden, scheinen sie reibungslos animiert zu sein.

Wenn die Grafiken jedoch zu komplex sind, um in 16 Millisekunden gerendert zu werden, kann die Animation zu jittery werden. Der Programmierer kann sich entscheiden, die Aktualisierungsrate auf 30 mal oder 15 Mal pro Sekunde zu reduzieren, aber manchmal reicht das sogar nicht aus. Manchmal sind Grafiken so komplex, dass sie einfach nicht in Echtzeit gerendert werden können.

Eine Lösung besteht darin, die Animation vorab vorzubereiten, indem die einzelnen Frames der Animation in einer Reihe von Bitmaps gerendert werden. Um die Animation anzuzeigen, ist es nur erforderlich, diese Bitmaps sequenziell 60 mal pro Sekunde anzuzeigen.

Natürlich ist das möglicherweise eine Menge Bitmaps, aber das ist, wie große 3D-Animationsfilme erstellt werden. Die 3D-Grafiken sind viel zu komplex, um in Echtzeit gerendert zu werden. Zum Rendern der einzelnen Frames ist viel Verarbeitungszeit erforderlich. Was Sie sehen, wenn Sie den Film ansehen, ist im Wesentlichen eine Reihe von Bitmaps.

Sie können etwas ähnliches in SkiaSharp tun. In diesem Artikel werden zwei Arten von Bitmapanimation veranschaulicht. Das erste Beispiel ist eine Animation des Mandelbrot-Satzes:

Animieren des Beispiels

Das zweite Beispiel zeigt, wie Sie mithilfe von SkiaSharp eine animierte GIF-Datei rendern.

Bitmapanimation

Das Mandelbrot Set ist visuell faszinierend, aber berechnungsvoll langwierig. (Eine Diskussion über den Mandelbrot-Satz und die hier verwendeten Mathematik finden Sie unter Kapitel 20 des Erstellens mobiler Apps ab Xamarin.Forms Seite 666. In der folgenden Beschreibung wird davon ausgegangen, dass Hintergrundkenntnisse bekannt sind.)

Im Beispiel wird Bitmapanimation verwendet, um einen kontinuierlichen Zoom eines festen Punkts im Mandelbrot-Satz zu simulieren. Auf das Vergrößern folgt das Verkleinern, und dann wiederholt sich der Zyklus für immer oder bis Sie das Programm beenden.

Das Programm bereitet sich auf diese Animation vor, indem bis zu 50 Bitmaps erstellt werden, die es im lokalen Anwendungsspeicher speichert. Jede Bitmap umfasst die Hälfte der Breite und Höhe der komplexen Ebene als vorherige Bitmap. (Im Programm werden diese Bitmaps als integrale Zoomstufen bezeichnet.) Die Bitmaps werden dann in Sequenz angezeigt. Die Skalierung der einzelnen Bitmaps wird animiert, um einen reibungslosen Verlauf von einer Bitmap zu einer anderen bereitzustellen.

Wie das abschließende Programm, das in Kapitel 20 der Erstellung mobiler Apps mitXamarin.Forms beschrieben wird, ist die Berechnung des Mandelbrot-Satzes in Mandelbrot Animation eine asynchrone Methode mit acht Parametern. Die Parameter umfassen einen komplexen Mittelpunkt sowie eine Breite und Höhe der komplexen Ebene, die diesen Mittelpunkt umgibt. Die nächsten drei Parameter sind die Pixelbreite und -höhe der zu erstellenden Bitmap und eine maximale Anzahl von Iterationen für die rekursive Berechnung. Der progress Parameter wird verwendet, um den Fortschritt dieser Berechnung anzuzeigen. Der cancelToken Parameter wird in diesem Programm nicht verwendet:

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

Die Methode gibt ein Objekt vom Typ BitmapInfo zurück, das Informationen zum Erstellen einer Bitmap bereitstellt:

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

Die XAML-Datei mandelbrot animation enthält zwei Label Ansichten, a ProgressBarund a Button sowie die 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>

Die CodeBehind-Datei beginnt mit der Definition von drei wichtigen Konstanten und einem Array von Bitmaps:

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

Irgendwann möchten Sie den COUNT Wert wahrscheinlich in 50 ändern, um den gesamten Bereich der Animation anzuzeigen. Werte über 50 sind nicht nützlich. Um einen Zoomfaktor von 48 oder so ist die Auflösung von Gleitkommazahlen mit doppelter Genauigkeit für die Mandelbrot-Set-Berechnung nicht ausreichend. Dieses Problem wird auf Seite 684 des Erstellens mobiler Apps mit Xamarin.Formsbehandelt.

Der center Wert ist sehr wichtig. Dies ist der Fokus des Animationszooms. Die drei Werte in der Datei sind diejenigen, die in den drei letzten Screenshots in Kapitel 20 der Erstellung mobiler Apps auf Xamarin.Forms Seite 684 verwendet werden, aber Sie können mit dem Programm in diesem Kapitel experimentieren, um einen Ihrer eigenen Werte zu erhalten.

Im Beispiel für Mandelbrotanimationen werden diese COUNT Bitmaps im lokalen Anwendungsspeicher gespeichert. Fünfzig Bitmaps erfordern mehr als 20 MB Speicherplatz auf Ihrem Gerät, daher sollten Sie wissen, wie viel Speicherplatz diese Bitmaps belegen, und zu einem bestimmten Zeitpunkt möchten Sie sie alle löschen. Dies ist der Zweck dieser beiden Methoden am unteren Rand der MainPage Klasse:

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

Sie können die Bitmaps im lokalen Speicher löschen, während das Programm dieselben Bitmaps animiert, da das Programm sie im Arbeitsspeicher behält. Beim nächsten Ausführen des Programms müssen die Bitmaps jedoch neu erstellt werden.

Die im lokalen Anwendungsspeicher gespeicherten Bitmaps enthalten den center Wert in ihren Dateinamen. Wenn Sie die center Einstellung ändern, werden die vorhandenen Bitmaps nicht im Speicher ersetzt und belegen weiterhin Platz.

Im Folgenden werden die Methoden MainPage zum Erstellen der Dateinamen sowie eine MakePixel Methode zum Definieren eines Pixelwerts basierend auf Farbkomponenten aufgeführt:

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

Der zoomLevel Parameter, FilePath der zwischen 0 und der COUNT Konstante minus 1 liegt.

Der MainPage Konstruktor ruft die LoadAndStartAnimation Methode auf:

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

        LoadAndStartAnimation();
    }
    ···
}

Die LoadAndStartAnimation Methode ist für den Zugriff auf den lokalen Anwendungsspeicher verantwortlich, um alle Bitmaps zu laden, die möglicherweise erstellt wurden, als das Programm zuvor ausgeführt wurde. Sie durchläuft zoomLevel Werte von 0 bis COUNT. Wenn die Datei vorhanden ist, wird sie in das bitmaps Array geladen. Andernfalls muss eine Bitmap für die jeweiligen center Werte zoomLevel erstellt werden, indem sie aufgerufen wird Mandelbrot.CalculateAsync. Diese Methode ruft die Iterationsanzahl für jedes Pixel ab, das diese Methode in Farben konvertiert:

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

Beachten Sie, dass das Programm diese Bitmaps nicht in der Fotobibliothek des Geräts, sondern im lokalen Anwendungsspeicher speichert. Die .NET Standard 2.0-Bibliothek ermöglicht die Verwendung der vertrauten File.OpenRead Und File.WriteAllBytes Methoden für diese Aufgabe.

Nachdem alle Bitmaps erstellt oder in den Speicher geladen wurden, startet die Methode ein Stopwatch Objekt und ruft auf Device.StartTimer. Die OnTimerTick Methode wird alle 16 Millisekunden aufgerufen.

OnTimerTick berechnet einen time Wert in Millisekunden, der zwischen 0 und 6000 Mal COUNTliegt, was sechs Sekunden für die Anzeige jeder Bitmap angibt. Der progress Wert verwendet den Math.Sin Wert, um eine sinusförmige Animation zu erstellen, die am Anfang des Zyklus langsamer und am Ende langsamer ist, während sie die Richtung umkehrt.

Der progress Wert liegt zwischen 0 und COUNT. Dies bedeutet, dass der ganzzahlige Teil progress ein Index in das bitmaps Array ist, während der Bruchteil des progress Elements einen Zoomfaktor für diese bestimmte Bitmap angibt. Diese Werte werden in den bitmapIndex Feldern gespeichert bitmapProgress und von der LabelSlider XAML-Datei angezeigt. Dies SKCanvasView ist ungültig, um die Bitmapanzeige zu aktualisieren:

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

Schließlich berechnet der PaintSurface Handler des SKCanvasView Zielrechtecks, um die Bitmap so groß wie möglich anzuzeigen, während Standard das Seitenverhältnis beibehalten. Ein Quellrechteck basiert auf dem bitmapProgress Wert. Der fraction hier berechnete Wert reicht von 0, wenn bitmapProgress 0 ist, um die gesamte Bitmap anzuzeigen, bis zu 0,25, wenn bitmapProgress 1 die Hälfte der Breite und Höhe der Bitmap anzeigt und effektiv vergrößert wird:

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

Dies ist das Programm, das ausgeführt wird:

Mandelbrot Animation

GIF-Animation

Die Gif-Spezifikation (Graphics Interchange Format) enthält ein Feature, mit dem eine einzelne GIF-Datei mehrere sequenzielle Frames einer Szene enthalten kann, die nacheinander angezeigt werden kann, häufig in einer Schleife. Diese Dateien werden als animierte GIFs bezeichnet. Webbrowser können animierte GIFs wiedergeben, und SkiaSharp ermöglicht es einer Anwendung, die Frames aus einer animierten GIF-Datei zu extrahieren und sequenziell anzuzeigen.

Das Beispiel enthält eine animierte GIF-Ressource namens Newtons_cradle_animation_book_2.gif von DemonDeLuxe erstellt und von der Newton es Cradle-Seite in Wikipedia heruntergeladen. Die animierte GIF-Seite enthält eine XAML-Datei, die diese Informationen bereitstellt und instanziiert: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>

Die CodeBehind-Datei wird nicht generalisiert, um animierte GIF-Dateien wiederzugeben. Es ignoriert einige der verfügbaren Informationen, insbesondere eine Wiederholungsanzahl, und gibt einfach das animierte GIF in einer Schleife wieder.

Die Verwendung von SkisSharp zum Extrahieren der Frames einer animierten GIF-Datei scheint nicht überall dokumentiert zu sein, daher ist die Beschreibung des folgenden Codes detaillierter als üblich:

Die Decodierung der animierten GIF-Datei erfolgt im Konstruktor der Seite und erfordert, dass das Stream Objekt, das auf die Bitmap verweist, verwendet wird, um ein SKManagedStream Objekt und dann ein SKCodec Objekt zu erstellen. Die FrameCount Eigenschaft gibt die Anzahl der Frames an, aus denen die Animation besteht.

Diese Frames werden schließlich als einzelne Bitmaps gespeichert, sodass der Konstruktor FrameCount ein Array vom Typ SKBitmap sowie zwei int Arrays für die Dauer jedes Frames zuordnet und (um die Animationslogik zu vereinfachen) die angesammelten Daueren zuzuordnen.

Die FrameInfo Eigenschaft der SKCodec Klasse ist ein Array von SKCodecFrameInfo Werten, eines für jeden Frame, aber das einzige, was dieses Programm aus dieser Struktur übernimmt, ist der Duration Frame in Millisekunden.

SKCodec definiert eine Eigenschaft mit dem Namen Info des Typs SKImageInfo, aber dieser SKImageInfo Wert gibt (zumindest für dieses Bild) an, dass der Farbtyp lautet SKColorType.Index8, was bedeutet, dass jedes Pixel ein Index in einem Farbtyp ist. Um das Stören mit Farbtabellen zu vermeiden, verwendet das Programm die Width und Height Die Informationen aus dieser Struktur, um einen eigenen Vollfarbwert ImageInfo zu erstellen. Jeder SKBitmap wird daraus erstellt.

Die GetPixels Methode der SKBitmap Rückgabe gibt einen IntPtr Verweis auf die Pixelbits dieser Bitmap zurück. Diese Pixelbits wurden noch nicht festgelegt. Dies IntPtr wird an eine der GetPixels Methoden SKCodecvon übergeben. Diese Methode kopiert den Frame aus der GIF-Datei in den von der IntPtr. Der SKCodecOptions Konstruktor gibt die Framenummer an:

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

Trotz des IntPtr Werts ist kein unsafe Code erforderlich, da der IntPtr Wert nie in einen C#-Zeigerwert konvertiert wird.

Nachdem jeder Frame extrahiert wurde, summiert der Konstruktor die Dauer aller Frames und initialisiert dann ein weiteres Array mit den akkumulierten Daueren.

Die erneute Standard der der CodeBehind-Datei ist der Animation gewidmet. Die Device.StartTimer Methode wird verwendet, um einen Timer zu starten, und der OnTimerTick Rückruf verwendet ein Stopwatch Objekt, um die verstrichene Zeit in Millisekunden zu bestimmen. Das Durchlaufen des gesammelten Dauerarrays reicht aus, um den aktuellen Frame zu finden:

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

Jedes Mal, wenn sich die currentframe Variable ändert, wird die SKCanvasView Variable ungültig, und der neue Frame wird angezeigt:

Animiertes GIF

Natürlich möchten Sie das Programm selbst ausführen, um die Animation anzuzeigen.