SkiaSharp bit eşlemlerini animasyonlama

SkiaSharp grafiklerine animasyon uygulayan uygulamalar genellikle sabit bir hızda (genellikle her 16 milisaniyede bir) öğesini çağırır InvalidateSurface SKCanvasView . Yüzeyin geçersiz kılınmış olması, görüntüyü yeniden çizmek için PaintSurface işleyiciye bir çağrı tetikler. Görseller saniyede 60 kez yeniden çizildiğinden, düzgün bir şekilde animasyonlu görünüyorlar.

Ancak, grafikler 16 milisaniye içinde işlenemeyecek kadar karmaşıksa animasyon dalgalı hale gelebilir. Programcı yenileme hızını saniyede 30 veya 15 kata düşürmeyi seçebilir, ancak bazen bu bile yeterli olmaz. Bazen grafikler o kadar karmaşıktır ki gerçek zamanlı olarak işlenemez.

Çözümlerden biri, animasyonun tek tek karelerini bir bit eşlem dizisinde işleyerek animasyona önceden hazırlanmaktır. Animasyonu görüntülemek için bu bit eşlemleri yalnızca saniyede 60 kez sırayla görüntülemek gerekir.

Elbette, bu büyük olasılıkla çok fazla bit eşlemdir, ancak büyük bütçeli 3B animasyonlu filmler bu şekilde yapılır. 3B grafikler gerçek zamanlı olarak işlenemeyecek kadar karmaşıktır. Her çerçeveyi işlemek için çok fazla işlem süresi gerekir. Filmi izlerken gördükleriniz temelde bir bit eşlem dizisidir.

SkiaSharp'ta benzer bir şey yapabilirsiniz. Bu makalede iki bit eşlem animasyonu türü gösterilmektedir. İlk örnek, Mandelbrot Kümesinin bir animasyonudur:

Örnek Animasyon Oluşturma

İkinci örnekte, skiaSharp kullanarak animasyonlu GIF dosyasının nasıl işlenme şekli gösterilmektedir.

Bit eşlem animasyonu

Mandelbrot Seti görsel olarak büyüleyicidir ancak işlemsel olarak uzundur. (Mandelbrot Kümesi ve burada kullanılan matematik hakkında bir tartışma için bkz.Sayfa 666'dan başlayarak Mobil Uygulama Xamarin.Forms Oluşturma'nın 20. bölümü. Aşağıdaki açıklama, arka plan bilgisinin olduğunu varsayar.)

Örnek, Mandelbrot Kümesindeki sabit bir noktanın sürekli yakınlaştırmasını simüle etmek için bit eşlem animasyonu kullanır. Yakınlaştırmanın ardından uzaklaştırılır ve ardından döngü sonsuza kadar veya programı sonlandırana kadar yinelenir.

Program, uygulama yerel depolama alanında depoladığı en fazla 50 bit eşlem oluşturarak bu animasyona hazırlanır. Her bit eşlem, karmaşık düzlemin genişliğinin ve yüksekliğinin yarısını önceki bit eşlem olarak kapsar. (Programda, bu bit eşlemlerin integral yakınlaştırma düzeylerini temsil ettiğini söylenir.) Bit eşlemler sırayla görüntülenir. Her bit eşlemi ölçeklendirme, bir bit eşlemden diğerine sorunsuz bir ilerleme sağlamak için animasyonludur.

ile Xamarin.FormsMobil Uygulama Oluşturma'nın 20. Bölümünde açıklanan son program gibi, Mandelbrot Animasyonunda Mandelbrot Kümesi hesaplaması sekiz parametreli zaman uyumsuz bir yöntemdir. Parametreler karmaşık bir merkez noktası ve bu orta noktayı çevreleyen karmaşık düzlemin genişliğini ve yüksekliğini içerir. Sonraki üç parametre, oluşturulacak bit eşlemin piksel genişliği ve yüksekliği ile özyinelemeli hesaplama için en fazla yineleme sayısıdır. progress parametresi, bu hesaplamanın ilerleme durumunu görüntülemek için kullanılır. cancelToken parametresi bu programda kullanılmaz:

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

yöntemi, bit eşlem oluşturmak için bilgi sağlayan türde BitmapInfo bir nesne döndürür:

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

Mandelbrot Animasyon XAML dosyası iki Label görünüm içerir:ProgressBarButton 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>

Arka planda kod dosyası üç önemli sabit ve bir bit eşlem dizisi tanımlayarak başlar:

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

Bir noktada, animasyonun COUNT tüm aralığını görmek için büyük olasılıkla değeri 50 olarak değiştirmek isteyeceksiniz. 50'nin üzerindeki değerler yararlı değildir. 48 veya daha fazla yakınlaştırma düzeyinde çift duyarlıklı kayan noktalı sayıların çözünürlüğü Mandelbrot Kümesi hesaplaması için yetersiz hale gelir. Bu sorun, ile Xamarin.FormsMobil Uygulama Oluşturma sayfasının 684. sayfasında açıklandı.

Değer center çok önemlidir. Bu, animasyon yakınlaştırmasının odağıdır. Dosyadaki üç değer, Sayfa 684'teki Mobile Apps Xamarin.Forms Oluşturma'nın 20. Bölümü'ndeki son üç ekran görüntüsünde kullanılanlardır, ancak bu bölümdeki programla denemeler yapabilir ve kendi değerlerinizden birini elde edebilirsiniz.

Mandelbrot Animasyon örneği bu COUNT bit eşlemleri yerel uygulama depolama alanında depolar. Elli bit eşlem, cihazınızda 20 megabayttan fazla depolama alanı gerektirir, bu nedenle bu bit eşlemlerin ne kadar depolama alanı kapsadığından emin olmak isteyebilirsiniz ve bir noktada hepsini silmek isteyebilirsiniz. Sınıfın en altındaki bu iki yöntemin amacı da bu şekildedir MainPage :

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

Program bunları bellekte tuttuğundan, program aynı bit eşlemleri animasyonu oluştururken yerel depolamadaki bit eşlemleri silebilirsiniz. Ancak programı bir sonraki çalıştırmanızda bit eşlemleri yeniden oluşturması gerekir.

Yerel uygulama depolama alanında depolanan bit eşlemler, dosya adlarına değer ekler center , dolayısıyla ayarı değiştirirseniz center mevcut bit eşlemler depolama alanında değiştirilmez ve yer kaplamaya devam eder.

Dosya adlarını oluşturmak için kullanılan yöntemlerin MainPage yanı MakePixel sıra renk bileşenlerine dayalı bir piksel değeri tanımlama yöntemi aşağıdadır:

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

zoomLevel parametresi FilePath 0 COUNT ile sabit eksi 1 arasında değişir.

MainPage Oluşturucu yöntemini LoadAndStartAnimation çağırır:

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

        LoadAndStartAnimation();
    }
    ···
}

LoadAndStartAnimation yöntemi, program daha önce çalıştırıldığında oluşturulmuş olabilecek bit eşlemleri yüklemek için uygulama yerel depolamasına erişmekle sorumludur. 0 COUNTile arasında zoomLevel döngüler oluşturur. Dosya varsa, diziye bitmaps yükler. Aksi takdirde, çağrısı Mandelbrot.CalculateAsyncyaparak belirli center ve zoomLevel değerler için bir bit eşlem oluşturması gerekir. Bu yöntem her piksel için yineleme sayılarını elde eder ve bu yöntem renklere dönüştürülür:

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

Programın bu bit eşlemleri cihazın fotoğraf kitaplığı yerine yerel uygulama depolama alanında depoladığını fark edin. .NET Standard 2.0 kitaplığı, bu görev için tanıdık File.OpenRead ve File.WriteAllBytes yöntemlerin kullanılmasına izin verir.

Tüm bit eşlemler oluşturulduktan veya belleğe yüklendikten sonra yöntemi bir Stopwatch nesnesi başlatır ve çağırır Device.StartTimer. OnTimerTick yöntemi her 16 milisaniyede bir çağrılır.

OnTimerTick0 ile 6000 kez COUNTarasında değişen milisaniye cinsinden bir time değer hesaplar. Bu değer, her bit eşlemin görüntülenmesi için altı saniyelik bir değerdir. progress değeri, döngünün Math.Sin başında daha yavaş ve yönü tersine çevirdiği için sonunda daha yavaş olacak bir sinüsoid animasyon oluşturmak için değerini kullanır.

progress Değer 0 COUNTile arasında değişir. Bu, öğesinin tamsayı bölümünün progress dizideki bitmaps bir dizin olduğu, kesirli bölümünün progress ise söz konusu bit eşlem için yakınlaştırma düzeyini gösterdiği anlamına gelir. Bu değerler ve alanlarında depolanır bitmapIndex ve tarafından Label ve Slider XAML dosyasında bitmapProgress görüntülenir. SKCanvasView bit eşlem görüntüsünü güncelleştirmek için geçersiz kılındı:

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

Son olarak, PaintSurface işleyicisi SKCanvasView en boy oranını korurken bit eşlemi olabildiğince büyük görüntülemek için bir hedef dikdörtgen hesaplar. Kaynak dikdörtgen değeri temel bitmapProgress alır. fraction Burada hesaplanan değer, 0 olduğunda bitmapProgress bit eşlemin tamamını görüntülemek için 0'dan 0,25'e, bit eşlem genişliğinin ve yüksekliğinin yarısını görüntülemek için 0,25'e bitmapProgress kadar değişir ve etkin bir şekilde yakınlaştırılır:

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

Çalışan program şu şekildedir:

Mandelbrot Animasyonu

GIF animasyonu

Grafik Değişim Biçimi (GIF) belirtimi, tek bir GIF dosyasının bir sahnenin birden çok sıralı çerçevesini içermesine olanak tanıyan ve genellikle döngüde, ardışık olarak görüntülenebilen bir özellik içerir. Bu dosyalar animasyonlu GIF'ler olarak bilinir. Web tarayıcıları animasyonlu GIF'leri yürütebilir ve SkiaSharp bir uygulamanın animasyonlu GIF dosyasından kareleri ayıklamasına ve bunları sırayla görüntülemesine olanak tanır.

Örnek, DemonDeLuxe tarafından oluşturulan ve Wikipedia'daki Newton'un Beşiği sayfasından indirilen Newtons_cradle_animation_book_2.gif adlı animasyonlu bir GIF kaynağı içerir. Animasyonlu GIF sayfası, bu bilgileri sağlayan ve bir örneği oluşturan bir SKCanvasViewXAML dosyası içerir:

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

Arka planda kod dosyası herhangi bir animasyonlu GIF dosyasını oynatmak için genelleştirilmemiştir. Kullanılabilir bilgilerin bazılarını, özellikle de yineleme sayısını yoksayar ve animasyonlu GIF'i döngüde oynatması yeterlidir.

SkisSharp'ın animasyonlu GIF dosyasının çerçevelerini ayıklamak için kullanılması herhangi bir yerde belgelenmemiştir, bu nedenle aşağıdaki kodun açıklaması normalden daha ayrıntılıdır:

Animasyonlu GIF dosyasının kodunun çözülmesi sayfanın oluşturucusunda gerçekleşir ve bit eşlem başvuran nesnenin Stream bir nesne ve sonra bir SKManagedStream SKCodec nesne oluşturmak için kullanılmasını gerektirir. FrameCount özelliği, animasyonu oluşturan kare sayısını gösterir.

Bu çerçeveler sonunda tek tek bit eşlemler olarak kaydedilir, bu nedenle oluşturucu her karenin süresi için iki int dizi ve birikmiş süreleri (animasyon mantığını kolaylaştırmak için) bir tür SKBitmap dizisi ayırmak için kullanırFrameCount.

FrameInfo sınıfının özelliğiSKCodec, her çerçeve için bir değer dizisidirSKCodecFrameInfo, ancak bu programın bu yapıdan Duration aldığı tek şey milisaniye cinsinden çerçevedir.

SKCodectüründe SKImageInfobir Info özellik tanımlar, ancak bu SKImageInfo değer renk türünün SKColorType.Index8olduğunu (en azından bu resim için) belirtir. Bu, her pikselin bir renk türüne ait bir dizin olduğu anlamına gelir. Renk tablolarıyla uğraşmaktan kaçınmak için program, kendi tam renk ImageInfo değerini oluşturmak için bu yapıdaki ve Height bilgilerini kullanırWidth. Her SKBitmap biri bundan oluşturulur.

yöntemiSKBitmap, GetPixels bu bit eşlemin piksel bitlerine başvuran bir IntPtr döndürür. Bu piksel bitleri henüz ayarlanmadı. Bu IntPtr , yöntemlerinden GetPixels SKCodecbirine geçirilir. Bu yöntem, GIF dosyasındaki çerçeveyi tarafından IntPtrbaşvuruda bulunılan bellek alanına kopyalar. Oluşturucu çerçeve SKCodecOptions numarasını gösterir:

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 değerine rağmen, unsafe hiçbir zaman C# işaretçi değerine dönüştürülemediğinden kod gerekmezIntPtr.

Her çerçeve ayıklandıktan sonra, oluşturucu tüm çerçevelerin sürelerini toplar ve sonra birikmiş sürelerle başka bir dizi başlatır.

Arka planda kod dosyasının geri kalanı animasyona ayrılmıştır. Device.StartTimer yöntemi bir süreölçer başlatmak için kullanılır ve OnTimerTick geri çağırma milisaniye cinsinden geçen süreyi belirlemek için bir Stopwatch nesnesi kullanır. Birikmiş süreler dizisinde döngü yapmak geçerli çerçeveyi bulmak için yeterlidir:

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

Değişken her değiştiğinde currentframe geçersiz SKCanvasView kılınıp yeni çerçeve görüntülenir:

Animasyonlu GIF

Tabii ki, animasyonu görmek için programı kendiniz çalıştırmak isteyeceksiniz.