다음을 통해 공유


SkiaSharp 비트맵 애니메이션

SkiaSharp 그래픽에 SKCanvasView 애니메이션 효과를 주는 애플리케이션은 일반적으로 16밀리초마다 고정된 속도로 호출 InvalidateSurface 됩니다. 표면을 무효화하면 디스플레이를 다시 그리기 위해 PaintSurface 처리기에 대한 호출이 트리거됩니다. 시각적 개체는 초당 60번 다시 그려지므로 원활하게 애니메이션이 적용된 것처럼 보입니다.

그러나 그래픽이 너무 복잡하여 16밀리초 단위로 렌더링되지 않으면 애니메이션이 불안해질 수 있습니다. 프로그래머가 새로 고침 속도를 초당 30번 또는 15배로 줄이도록 선택할 수 있지만, 때로는 충분하지 않을 수도 있습니다. 그래픽이 너무 복잡해서 실시간으로 렌더링할 수 없는 경우도 있습니다.

한 가지 해결 방법은 일련의 비트맵에서 애니메이션의 개별 프레임을 렌더링하여 애니메이션을 미리 준비하는 것입니다. 애니메이션을 표시하려면 이러한 비트맵을 1초에 60번 순차적으로 표시하기만 하면 됩니다.

물론, 그것은 잠재적으로 비트맵을 많이, 하지만 그건 얼마나 큰 예산 3D 애니메이션 영화 만들어집니다. 3D 그래픽은 너무 복잡하여 실시간으로 렌더링할 수 있습니다. 각 프레임을 렌더링하려면 많은 처리 시간이 필요합니다. 영화를 볼 때 보는 것은 본질적으로 일련의 비트맵입니다.

SkiaSharp에서 비슷한 작업을 수행할 수 있습니다. 이 문서에서는 두 가지 유형의 비트맵 애니메이션을 보여 줍니다. 첫 번째 예제는 만델브로트 집합의 애니메이션입니다.

애니메이션 샘플

두 번째 예제에서는 SkiaSharp를 사용하여 애니메이션 GIF 파일을 렌더링하는 방법을 보여 줍니다.

비트맵 애니메이션

만델브로트 세트는 시각적으로 매혹적이지만 계산적으로는 길다. (만델브로트 세트와 여기에 사용된 수학에 대한 논의는 다음을 참조하세요.666페이지부터 Mobile Apps Xamarin.Forms 만들기 20장. 다음 설명에서는 해당 배경 지식을 가정합니다.)

이 샘플에서는 비트맵 애니메이션을 사용하여 Mandelbrot Set에서 고정 지점의 연속 확대/축소를 시뮬레이트합니다. 확대한 다음 축소한 다음 주기가 영원히 반복되거나 프로그램을 종료할 때까지 반복됩니다.

프로그램은 애플리케이션 로컬 스토리지에 저장하는 최대 50개의 비트맵을 만들어 이 애니메이션을 준비합니다. 각 비트맵은 복소수 평면의 너비와 높이의 절반을 이전 비트맵으로 포함합니다. (프로그램에서 이러한 비트맵은 정수 확대/축소 수준을 나타낸다.) 그런 다음 비트맵이 순서대로 표시됩니다. 각 비트맵의 크기 조정은 한 비트맵에서 다른 비트맵으로의 원활한 진행을 제공하기 위해 애니메이션 효과를 줍니다.

Mobile AppsXamarin.Forms 만들기 20장에서 설명한 최종 프로그램과 마찬가지로, 만델브로트 애니메이션의 만델브로트 집합 계산은 8개의 매개 변수가 있는 비동기 메서드입니다. 매개 변수에는 복합 중심점, 해당 중심점을 둘러싼 복합 평면의 너비 및 높이가 포함됩니다. 다음 세 매개 변수는 만들 비트맵의 픽셀 너비와 높이, 재귀 계산에 대한 최대 반복 횟수입니다. 매개 progress 변수는 이 계산의 진행률을 표시하는 데 사용됩니다. 매개 cancelToken 변수는 이 프로그램에서 사용되지 않습니다.

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

이 메서드는 비트맵을 만들기 위한 정보를 제공하는 형식 BitmapInfo 의 개체를 반환합니다.

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 애니메이션 XAML 파일에는 다음과 같은 두 개의 Label 뷰, a ProgressBar및 a ButtonSKCanvasView포함됩니다.

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

코드 숨김 파일은 세 가지 중요한 상수와 비트맵 배열을 정의하여 시작합니다.

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

어떤 시점에서는 애니메이션의 전체 범위를 보려면 값을 50으로 변경 COUNT 하려고 할 것입니다. 50을 초과하는 값은 유용하지 않습니다. 확대/축소 수준이 48 정도이면 만델브로트 집합 계산에 배정밀도 부동 소수점 숫자의 해상도가 부족해집니다. 이 문제는 Mobile Apps 만들기의 684페이지에서 설명합니다 Xamarin.Forms.

값은 center 매우 중요합니다. 애니메이션 확대/축소의 포커스입니다. 파일의 세 가지 값은 684페이지에서 Mobile Apps Xamarin.Forms 만들기 20장의 마지막 스크린샷 3개에 사용되는 값이지만, 해당 챕터의 프로그램을 실험하여 사용자 고유의 값 중 하나를 생각해낼 수 있습니다.

Mandelbrot 애니메이션 샘플은 이러한 COUNT 비트맵을 로컬 애플리케이션 스토리지에 저장합니다. 50비트맵에는 디바이스에 20MB 이상의 스토리지가 필요하므로 이러한 비트맵이 차지하는 스토리지의 양을 알고 싶을 수 있으며, 어떤 시점에서는 모두 삭제할 수도 있습니다. 이것이 클래스의 맨 아래에 있는 다음 두 메서드의 목적입니다 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();
    }
}

프로그램에서 동일한 비트맵에 애니메이션 효과를 주는 동안 로컬 스토리지에서 비트맵을 삭제할 수 있습니다. 그러나 다음에 프로그램을 실행할 때 비트맵을 다시 만들어야 합니다.

로컬 애플리케이션 스토리지에 저장된 비트맵은 해당 파일 이름에 값을 통합 center 하므로 설정을 변경 center 하면 기존 비트맵이 스토리지에서 바뀌지 않고 공간을 계속 차지합니다.

다음은 파일 이름을 생성하는 데 사용하는 메서드 MainPageMakePixel 색 구성 요소를 기반으로 픽셀 값을 정의하는 방법입니다.

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 0에서 상수에서 1을 뺀 범위의 COUNT 매개 변수 FilePath 입니다.

MainPage 생성자는 메서드를 호출합니다.LoadAndStartAnimation

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

        LoadAndStartAnimation();
    }
    ···
}

LoadAndStartAnimation 메서드는 애플리케이션 로컬 스토리지에 액세스하여 프로그램이 이전에 실행되었을 때 생성되었을 수 있는 모든 비트맵을 로드합니다. 값을 0에서 .로 반복 zoomLevel 합니다 COUNT. 파일이 있는 경우 배열에 bitmaps 로드합니다. 그렇지 않으면 호출Mandelbrot.CalculateAsync하여 특정 center 값과 zoomLevel 값에 대한 비트맵을 만들어야 합니다. 이 메서드는 각 픽셀에 대한 반복 횟수를 가져오며, 이 메서드는 색으로 변환합니다.

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

프로그램은 이러한 비트맵을 디바이스의 사진 라이브러리가 아닌 로컬 애플리케이션 스토리지에 저장합니다. .NET Standard 2.0 라이브러리를 사용하면 이 작업에 익숙한 File.OpenRead 메서드와 File.WriteAllBytes 메서드를 사용할 수 있습니다.

모든 비트맵을 만들거나 메모리에 로드한 후 메서드는 개체를 Stopwatch 시작하고 호출 Device.StartTimer합니다. 메서드 OnTimerTick 는 16밀리초마다 호출됩니다.

OnTimerTicktime 0~6,000번 COUNT범위의 값을 밀리초 단위로 계산합니다. 이 값은 각 비트맵 표시에 6초를 할당합니다. 이 값은 progress 값을 사용하여 Math.Sin 주기 시작 시 느리고 방향을 반대로 할 때 끝에서 느려지는 부비동 애니메이션을 만듭니다.

값 범위는 progress 0 COUNT에서 . 즉, 정수 부분은 배열의 progress 인덱 bitmaps 스이고 소수 부분은 해당 특정 비트맵의 progress 확대/축소 수준을 나타냅니다. 이러한 값은 필드 및 bitmapProgress 필드에 저장 bitmapIndex 되며 XAML 파일과 Slider XAML 파일에 의해 Label 표시됩니다. SKCanvasView 비트맵 표시를 업데이트하기 위해 무효화되었습니다.

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

마지막으로, PaintSurface 처리기는 SKCanvasView 가로 세로 비율을 기본 동안 비트맵을 최대한 크게 표시하는 대상 사각형을 계산합니다. 원본 사각형은 값을 기반으로합니다 bitmapProgress . 여기서 계산된 값은 fraction 전체 비트맵을 표시하기 위해 0인 경우 bitmapProgress 0에서 0.25까지이며, 비트맵의 너비와 높이의 절반을 표시하려면 0.25 bitmapProgress 까지 다양합니다.

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

실행 중인 프로그램은 다음과 같습니다.

만델브로트 애니메이션

GIF 애니메이션

GIF(그래픽 교환 형식) 사양에는 단일 GIF 파일에 연속적으로 표시될 수 있는 장면의 여러 순차 프레임을 포함할 수 있는 기능이 포함되어 있습니다. 이러한 파일을 애니메이션 GIF라고 합니다. 웹 브라우저는 애니메이션 GIF를 재생할 수 있으며 SkiaSharp를 사용하면 애플리케이션이 애니메이션 GIF 파일에서 프레임을 추출하고 순차적으로 표시할 수 있습니다.

샘플에는 DemonDeLuxe에서 만들고 Wikipedia의 뉴턴의 크래들 페이지에서 다운로드한 Newtons_cradle_animation_book_2.gif이라는 애니메이션 GIF 리소스가 포함되어 있습니다. 애니메이션 GIF 페이지에는 해당 정보를 제공하고 다음을 인스턴스화하는 XAML 파일이 포함되어 있습니다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>

코드 숨김 파일은 애니메이션 GIF 파일을 재생하도록 일반화되지 않습니다. 사용 가능한 정보, 특히 반복 횟수를 무시하고 단순히 루프에서 애니메이션 GIF를 재생합니다.

SkisSharp를 사용하여 애니메이션 GIF 파일의 프레임을 추출하는 것은 어디에도 문서화되지 않는 것 같으므로 다음 코드에 대한 설명은 평소보다 더 자세합니다.

애니메이션 GIF 파일의 디코딩은 페이지의 생성자에서 발생하며 Stream 비트맵을 참조하는 개체를 사용하여 개체를 만든 다음 SKCodec 개체를 만들어야 SKManagedStream 합니다. 이 속성은 FrameCount 애니메이션을 구성하는 프레임 수를 나타냅니다.

이러한 프레임은 결국 개별 비트맵으로 저장되므로 생성자는 각 프레임의 기간 동안 형식 SKBitmap 배열과 두 int 개의 배열을 할당하고(애니메이션 논리를 완화하기 위해) 누적된 기간을 할당하는 데 사용합니다FrameCount.

클래스의 속성은 FrameInfo 각 프레임에 대해 하나씩 값 배열 SKCodecFrameInfo 이지만 이 프로그램에서 해당 구조에서 가져오는 유일한 것은 프레임(밀리초)입니다Duration.SKCodec

SKCodec는 형식SKImageInfo의 이름이 지정된 Info 속성을 정의하지만 해당 SKImageInfo 값은 SKColorType.Index8색 형식이 색 형식임을 나타냅니다. 즉, 각 픽셀이 색 형식의 인덱스임을 의미합니다. 색 테이블의 문제를 방지하기 위해 프로그램은 해당 구조체의 Width 정보와 Height 정보를 사용하여 고유한 전체 색 ImageInfo 값을 생성합니다. 각각 SKBitmap 은 해당 위치에서 만들어집니다.

해당 GetPixels 비트맵의 SKBitmap 픽셀 비트를 참조하는 메서드를 반환 IntPtr 합니다. 이러한 픽셀 비트는 아직 설정되지 않았습니다. 이는 IntPtr 의 메서드 중 GetPixels 하나에 전달됩니다 SKCodec. 이 메서드는 GIF 파일의 프레임을 참조하는 메모리 공간 IntPtr으로 복사합니다. SKCodecOptions 생성자는 프레임 번호를 나타냅니다.

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

unsafe 에도 IntPtr 불구하고 C# 포인터 값으로 변환되지 않으므로 코드가 필요하지 IntPtr 않습니다.

각 프레임을 추출한 후 생성자는 모든 프레임의 기간을 합한 다음 누적된 기간으로 다른 배열을 초기화합니다.

코드 숨김 파일의 re기본der는 애니메이션 전용입니다. 이 Device.StartTimer 메서드는 타이머 진행을 시작하는 데 사용되며 OnTimerTick 콜백은 개체를 Stopwatch 사용하여 경과된 시간을 밀리초 단위로 결정합니다. 누적된 기간 배열을 반복하면 현재 프레임을 찾을 수 있습니다.

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

변수가 currentframe 변경 SKCanvasView 될 때마다 해당 변수가 무효화되고 새 프레임이 표시됩니다.

애니메이션 GIF

물론 프로그램을 직접 실행하여 애니메이션을 볼 수 있습니다.