Animowanie map bitowych SkiaSharp
Aplikacje, które animują grafikę SkiaSharp, zwykle są wywoływane InvalidateSurface
z SKCanvasView
stałą szybkością, często co 16 milisekund. Unieważnienie powierzchni wyzwala wywołanie PaintSurface
programu obsługi w celu ponownego wyrysowania wyświetlacza. Ponieważ wizualizacje są ponownie rysowane 60 razy na sekundę, wydają się być płynnie animowane.
Jeśli jednak grafika jest zbyt złożona do renderowania w ciągu 16 milisekund, animacja może stać się burzliwa. Programista może zdecydować się zmniejszyć częstotliwość odświeżania do 30 razy lub 15 razy na sekundę, ale czasami nawet to nie wystarczy. Czasami grafiki są tak złożone, że po prostu nie można ich renderować w czasie rzeczywistym.
Jednym z rozwiązań jest przygotowanie animacji wcześniej przez renderowanie pojedynczych ramek animacji na serii map bitowych. Aby wyświetlić animację, wystarczy wyświetlić te mapy bitowe sekwencyjnie 60 razy na sekundę.
Oczywiście jest to potencjalnie wiele map bitowych, ale tak tworzone są filmy animowane 3D w dużym budżecie. Grafika 3D jest zbyt złożona, aby można je było renderować w czasie rzeczywistym. Do renderowania każdej ramki jest wymagany dużo czasu przetwarzania. To, co widzisz podczas oglądania filmu, to zasadniczo seria map bitowych.
Możesz zrobić coś podobnego w skiaSharp. W tym artykule przedstawiono dwa typy animacji mapy bitowej. Pierwszy przykład to animacja zestawu Mandelbrot:
W drugim przykładzie pokazano, jak użyć narzędzia SkiaSharp do renderowania animowanego pliku GIF.
Animacja mapy bitowej
Zestaw Mandelbrot jest wizualnie fascynujący, ale computionally długi. (Aby zapoznać się z omówieniem zestawu Mandelbrot i matematyki używanej tutaj, zobacz Rozdział 20 Tworzenie aplikacji mobilnych , Xamarin.Forms począwszy od strony 666. W poniższym opisie przyjęto założenie, że wiedza w tle).
W przykładzie użyto animacji mapy bitowej do symulowania ciągłego powiększenia stałego punktu w zestawie Mandelbrot. Powiększenia następuje pomniejszenie, a następnie cykl powtarza się na zawsze lub do momentu zakończenia programu.
Program przygotowuje się do tej animacji, tworząc do 50 map bitowych przechowywanych w magazynie lokalnym aplikacji. Każda mapa bitowa obejmuje połowę szerokości i wysokości złożonej płaszczyzny jako poprzedniej mapy bitowej. (W programie mówi się, że mapy bitowe reprezentują całkowite poziomy powiększenia). Mapy bitowe są następnie wyświetlane w sekwencji. Skalowanie każdej mapy bitowej jest animowane w celu zapewnienia płynnego postępu z jednej mapy bitowej do innej.
Podobnie jak w ostatnim programie opisanym w rozdziale 20 tworzenie aplikacji mobilnych za pomocą Xamarin.Formsmetody , obliczenie zestawu Mandelbrot w animacji Mandelbrot jest metodą asynchroniczną z ośmioma parametrami. Parametry obejmują złożony punkt środkowy oraz szerokość i wysokość złożonej płaszczyzny otaczającej ten punkt środkowy. Następne trzy parametry to szerokość i wysokość mapy bitowej, która ma zostać utworzona, oraz maksymalna liczba iteracji dla obliczeń cyklicznych. Parametr progress
służy do wyświetlania postępu tego obliczenia. Parametr cancelToken
nie jest używany w tym programie:
static class Mandelbrot
{
public static Task<BitmapInfo> CalculateAsync(Complex center,
double width, double height,
int pixelWidth, int pixelHeight,
int iterations,
IProgress<double> progress,
CancellationToken cancelToken)
{
return Task.Run(() =>
{
int[] iterationCounts = new int[pixelWidth * pixelHeight];
int index = 0;
for (int row = 0; row < pixelHeight; row++)
{
progress.Report((double)row / pixelHeight);
cancelToken.ThrowIfCancellationRequested();
double y = center.Imaginary + height / 2 - row * height / pixelHeight;
for (int col = 0; col < pixelWidth; col++)
{
double x = center.Real - width / 2 + col * width / pixelWidth;
Complex c = new Complex(x, y);
if ((c - new Complex(-1, 0)).Magnitude < 1.0 / 4)
{
iterationCounts[index++] = -1;
}
// http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
else if (c.Magnitude * c.Magnitude * (8 * c.Magnitude * c.Magnitude - 3) < 3.0 / 32 - c.Real)
{
iterationCounts[index++] = -1;
}
else
{
Complex z = 0;
int iteration = 0;
do
{
z = z * z + c;
iteration++;
}
while (iteration < iterations && z.Magnitude < 2);
if (iteration == iterations)
{
iterationCounts[index++] = -1;
}
else
{
iterationCounts[index++] = iteration;
}
}
}
}
return new BitmapInfo(pixelWidth, pixelHeight, iterationCounts);
}, cancelToken);
}
}
Metoda zwraca obiekt typu BitmapInfo
, który zawiera informacje dotyczące tworzenia mapy bitowej:
class BitmapInfo
{
public BitmapInfo(int pixelWidth, int pixelHeight, int[] iterationCounts)
{
PixelWidth = pixelWidth;
PixelHeight = pixelHeight;
IterationCounts = iterationCounts;
}
public int PixelWidth { private set; get; }
public int PixelHeight { private set; get; }
public int[] IterationCounts { private set; get; }
}
Plik XAML animacji Mandelbrot zawiera dwa Label
widoki, a ProgressBar
także Button
SKCanvasView
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="MandelAnima.MainPage"
Title="Mandelbrot Animation">
<StackLayout>
<Label x:Name="statusLabel"
HorizontalTextAlignment="Center" />
<ProgressBar x:Name="progressBar" />
<skia:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<StackLayout Orientation="Horizontal"
Padding="5">
<Label x:Name="storageLabel"
VerticalOptions="Center" />
<Button x:Name="deleteButton"
Text="Delete All"
HorizontalOptions="EndAndExpand"
Clicked="OnDeleteButtonClicked" />
</StackLayout>
</StackLayout>
</ContentPage>
Plik kodu rozpoczyna się od zdefiniowania trzech kluczowych stałych i tablicy map bitowych:
public partial class MainPage : ContentPage
{
const int COUNT = 10; // The number of bitmaps in the animation.
// This can go up to 50!
const int BITMAP_SIZE = 1000; // Program uses square bitmaps exclusively
// Uncomment just one of these, or define your own
static readonly Complex center = new Complex(-1.17651152924355, 0.298520986549558);
// static readonly Complex center = new Complex(-0.774693089457127, 0.124226621261617);
// static readonly Complex center = new Complex(-0.556624880053304, 0.634696788141351);
SKBitmap[] bitmaps = new SKBitmap[COUNT]; // array of bitmaps
···
}
W pewnym momencie prawdopodobnie zechcesz zmienić COUNT
wartość na 50, aby zobaczyć pełny zakres animacji. Wartości powyżej 50 nie są przydatne. Wokół poziomu powiększenia 48 lub tak rozdzielczość liczb zmiennoprzecinkowych o podwójnej precyzji staje się niewystarczająca dla obliczenia zestawu Mandelbrot. Ten problem został omówiony na stronie 684 tworzenia aplikacji mobilnych za pomocą polecenia Xamarin.Forms.
Wartość center
jest bardzo ważna. Jest to fokus powiększenia animacji. Trzy wartości w pliku są używane w trzech ostatnich zrzutach ekranu w rozdziale 20 tworzenie aplikacji Xamarin.Forms mobilnych na stronie 684, ale możesz eksperymentować z programem w tym rozdziale, aby wymyślić jedną z własnych wartości.
Przykład Mandelbrot Animation przechowuje te COUNT
mapy bitowe w lokalnym magazynie aplikacji. Pięćdziesiąt map bitowych wymaga ponad 20 megabajtów miejsca do magazynowania na urządzeniu, więc warto wiedzieć, ile miejsca zajmują te mapy bitowe, a w pewnym momencie możesz je usunąć. Jest to cel tych dwóch metod w dolnej części MainPage
klasy:
public partial class MainPage : ContentPage
{
···
void TallyBitmapSizes()
{
long fileSize = 0;
foreach (string filename in Directory.EnumerateFiles(FolderPath()))
{
fileSize += new FileInfo(filename).Length;
}
storageLabel.Text = $"Total storage: {fileSize:N0} bytes";
}
void OnDeleteButtonClicked(object sender, EventArgs args)
{
foreach (string filepath in Directory.EnumerateFiles(FolderPath()))
{
File.Delete(filepath);
}
TallyBitmapSizes();
}
}
Mapy bitowe można usunąć w magazynie lokalnym, gdy program animuje te same mapy bitowe, ponieważ program zachowuje je w pamięci. Jednak przy następnym uruchomieniu programu konieczne będzie ponowne utworzenie map bitowych.
Mapy bitowe przechowywane w lokalnym magazynie aplikacji zawierają center
wartość w nazwach plików, więc jeśli zmienisz center
ustawienie, istniejące mapy bitowe nie zostaną zastąpione w magazynie i będą nadal zajmować miejsce.
Poniżej przedstawiono metody używane MainPage
do konstruowania nazw plików, a także MakePixel
metody definiowania wartości pikseli na podstawie składników kolorów:
public partial class MainPage : ContentPage
{
···
// File path for storing each bitmap in local storage
string FolderPath() =>
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string FilePath(int zoomLevel) =>
Path.Combine(FolderPath(),
String.Format("R{0}I{1}Z{2:D2}.png", center.Real, center.Imaginary, zoomLevel));
// Form bitmap pixel for Rgba8888 format
uint MakePixel(byte alpha, byte red, byte green, byte blue) =>
(uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
···
}
Parametr zoomLevel
do FilePath
zakresu od 0 do stałej COUNT
minus 1.
Konstruktor MainPage
wywołuje metodę LoadAndStartAnimation
:
public partial class MainPage : ContentPage
{
···
public MainPage()
{
InitializeComponent();
LoadAndStartAnimation();
}
···
}
Metoda LoadAndStartAnimation
jest odpowiedzialna za uzyskiwanie dostępu do magazynu lokalnego aplikacji w celu załadowania wszelkich map bitowych, które mogły zostać utworzone podczas uruchamiania programu wcześniej. Wykonuje pętlę przez zoomLevel
wartości z zakresu od 0 do COUNT
. Jeśli plik istnieje, ładuje go do tablicy bitmaps
. W przeciwnym razie należy utworzyć mapę bitową dla określonych center
wartości i zoomLevel
przez wywołanie metody Mandelbrot.CalculateAsync
. Ta metoda uzyskuje liczbę iteracji dla każdego piksela, który ta metoda konwertuje na kolory:
public partial class MainPage : ContentPage
{
···
async void LoadAndStartAnimation()
{
// Show total bitmap storage
TallyBitmapSizes();
// Create progressReporter for async operation
Progress<double> progressReporter =
new Progress<double>((double progress) => progressBar.Progress = progress);
// Create (unused) CancellationTokenSource for async operation
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
// Loop through all the zoom levels
for (int zoomLevel = 0; zoomLevel < COUNT; zoomLevel++)
{
// If the file exists, load it
if (File.Exists(FilePath(zoomLevel)))
{
statusLabel.Text = $"Loading bitmap for zoom level {zoomLevel}";
using (Stream stream = File.OpenRead(FilePath(zoomLevel)))
{
bitmaps[zoomLevel] = SKBitmap.Decode(stream);
}
}
// Otherwise, create a new bitmap
else
{
statusLabel.Text = $"Creating bitmap for zoom level {zoomLevel}";
CancellationToken cancelToken = cancelTokenSource.Token;
// Do the (generally lengthy) Mandelbrot calculation
BitmapInfo bitmapInfo =
await Mandelbrot.CalculateAsync(center,
4 / Math.Pow(2, zoomLevel),
4 / Math.Pow(2, zoomLevel),
BITMAP_SIZE, BITMAP_SIZE,
(int)Math.Pow(2, 10), progressReporter, cancelToken);
// Create bitmap & get pointer to the pixel bits
SKBitmap bitmap = new SKBitmap(BITMAP_SIZE, BITMAP_SIZE, SKColorType.Rgba8888, SKAlphaType.Opaque);
IntPtr basePtr = bitmap.GetPixels();
// Set pixel bits to color based on iteration count
for (int row = 0; row < bitmap.Width; row++)
for (int col = 0; col < bitmap.Height; col++)
{
int iterationCount = bitmapInfo.IterationCounts[row * bitmap.Width + col];
uint pixel = 0xFF000000; // black
if (iterationCount != -1)
{
double proportion = (iterationCount / 32.0) % 1;
byte red = 0, green = 0, blue = 0;
if (proportion < 0.5)
{
red = (byte)(255 * (1 - 2 * proportion));
blue = (byte)(255 * 2 * proportion);
}
else
{
proportion = 2 * (proportion - 0.5);
green = (byte)(255 * proportion);
blue = (byte)(255 * (1 - proportion));
}
pixel = MakePixel(0xFF, red, green, blue);
}
// Calculate pointer to pixel
IntPtr pixelPtr = basePtr + 4 * (row * bitmap.Width + col);
unsafe // requires compiling with unsafe flag
{
*(uint*)pixelPtr.ToPointer() = pixel;
}
}
// Save as PNG file
SKData data = SKImage.FromBitmap(bitmap).Encode();
try
{
File.WriteAllBytes(FilePath(zoomLevel), data.ToArray());
}
catch
{
// Probably out of space, but just ignore
}
// Store in array
bitmaps[zoomLevel] = bitmap;
// Show new bitmap sizes
TallyBitmapSizes();
}
// Display the bitmap
bitmapIndex = zoomLevel;
canvasView.InvalidateSurface();
}
// Now start the animation
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
···
}
Zwróć uwagę, że program przechowuje te mapy bitowe w lokalnym magazynie aplikacji, a nie w bibliotece zdjęć urządzenia. Biblioteka .NET Standard 2.0 umożliwia korzystanie ze znanych File.OpenRead
metod i File.WriteAllBytes
dla tego zadania.
Po utworzeniu lub załadowaniu wszystkich map bitowych do pamięci metoda uruchamia obiekt i wywołuje metodę Stopwatch
Device.StartTimer
. Metoda OnTimerTick
jest wywoływana co 16 milisekund.
OnTimerTick
time
Oblicza wartość w milisekundach, która waha się od 0 do 6000 razy COUNT
, która dzieli sześć sekund na wyświetlanie każdej mapy bitowej. Wartość progress
używa Math.Sin
wartości do utworzenia animacji sinusoidalnej, która będzie wolniejsza na początku cyklu i wolniej na końcu, gdy odwraca kierunek.
Wartości progress
wahają się od 0 do COUNT
. Oznacza to, że część całkowita obiektu progress
jest indeksem w tablicy bitmaps
, a część ułamkowa progress
elementu wskazuje poziom powiększenia dla tej konkretnej mapy bitowej. Te wartości są przechowywane w polach bitmapIndex
i bitmapProgress
i są wyświetlane przez Label
element i Slider
w pliku XAML. Właściwość SKCanvasView
jest unieważniona w celu zaktualizowania ekranu mapy bitowej:
public partial class MainPage : ContentPage
{
···
Stopwatch stopwatch = new Stopwatch(); // for the animation
int bitmapIndex;
double bitmapProgress = 0;
···
bool OnTimerTick()
{
int cycle = 6000 * COUNT; // total cycle length in milliseconds
// Time in milliseconds from 0 to cycle
int time = (int)(stopwatch.ElapsedMilliseconds % cycle);
// Make it sinusoidal, including bitmap index and gradation between bitmaps
double progress = COUNT * 0.5 * (1 + Math.Sin(2 * Math.PI * time / cycle - Math.PI / 2));
// These are the field values that the PaintSurface handler uses
bitmapIndex = (int)progress;
bitmapProgress = progress - bitmapIndex;
// It doesn't often happen that we get up to COUNT, but an exception would be raised
if (bitmapIndex < COUNT)
{
// Show progress in UI
statusLabel.Text = $"Displaying bitmap for zoom level {bitmapIndex}";
progressBar.Progress = bitmapProgress;
// Update the canvas
canvasView.InvalidateSurface();
}
return true;
}
···
}
Na koniec program obsługi SKCanvasView
oblicza prostokąt docelowy, PaintSurface
aby wyświetlić mapę bitową tak dużą, jak to możliwe, przy zachowaniu współczynnika proporcji. Prostokąt źródłowy jest oparty na bitmapProgress
wartości. Wartość fraction
obliczona tutaj waha się od 0, gdy bitmapProgress
ma wartość 0, aby wyświetlić całą mapę bitową, do 0,25, gdy bitmapProgress
ma wartość 1, aby wyświetlić połowę szerokości i wysokości mapy bitowej, co skutecznie powiększa:
public partial class MainPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
if (bitmaps[bitmapIndex] != null)
{
// Determine destination rect as square in canvas
int dimension = Math.Min(info.Width, info.Height);
float x = (info.Width - dimension) / 2;
float y = (info.Height - dimension) / 2;
SKRect destRect = new SKRect(x, y, x + dimension, y + dimension);
// Calculate source rectangle based on fraction:
// bitmapProgress == 0: full bitmap
// bitmapProgress == 1: half of length and width of bitmap
float fraction = 0.5f * (1 - (float)Math.Pow(2, -bitmapProgress));
SKBitmap bitmap = bitmaps[bitmapIndex];
int width = bitmap.Width;
int height = bitmap.Height;
SKRect sourceRect = new SKRect(fraction * width, fraction * height,
(1 - fraction) * width, (1 - fraction) * height);
// Display the bitmap
canvas.DrawBitmap(bitmap, sourceRect, destRect);
}
}
···
}
Oto uruchomiony program:
Animacja GIF
Specyfikacja Formatu wymiany grafiki (GIF) zawiera funkcję, która umożliwia pojedynczemu plikowi GIF zawierać wiele sekwencyjnych ramek sceny, które mogą być wyświetlane z rzędu, często w pętli. Te pliki są nazywane animowanymi plikami GIF. Przeglądarki internetowe mogą odtwarzać animowane pliki GIF, a SkiaSharp umożliwia aplikacji wyodrębnianie ramek z animowanego pliku GIF i wyświetlanie ich sekwencyjnie.
Przykład zawiera animowany zasób GIF o nazwie Newtons_cradle_animation_book_2.gif utworzony przez DemonDeLuxe i pobrany ze strony Kolebki Newtona w Wikipedii. Animowana strona GIF zawiera plik XAML, który udostępnia informacje i tworzy wystąpienie elementu SKCanvasView
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Bitmaps.AnimatedGifPage"
Title="Animated GIF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="0"
PaintSurface="OnCanvasViewPaintSurface" />
<Label Text="GIF file by DemonDeLuxe from Wikipedia Newton's Cradle page"
Grid.Row="1"
Margin="0, 5"
HorizontalTextAlignment="Center" />
</Grid>
</ContentPage>
Plik związany z kodem nie jest uogólniony do odtwarzania żadnego animowanego pliku GIF. Ignoruje niektóre dostępne informacje, w szczególności liczbę powtórzeń i po prostu odtwarza animowany plik GIF w pętli.
Korzystanie z SkisSharp do wyodrębniania ramek animowanego pliku GIF nie wydaje się być udokumentowane w dowolnym miejscu, więc opis poniższego kodu jest bardziej szczegółowy niż zwykle:
Dekodowanie animowanego pliku GIF odbywa się w konstruktorze strony i wymaga, aby Stream
obiekt odwołujący się do mapy bitowej był używany do utworzenia SKManagedStream
obiektu, a następnie SKCodec
obiektu. Właściwość FrameCount
wskazuje liczbę ramek tworzących animację.
Te ramki są ostatecznie zapisywane jako poszczególne mapy bitowe, więc konstruktor używa FrameCount
do przydzielenia tablicy typu SKBitmap
, a także dwóch int
tablic na czas trwania każdej ramki i (w celu ułatwienia logiki animacji) skumulowanych czasów trwania.
FrameInfo
Właściwość SKCodec
klasy jest tablicą SKCodecFrameInfo
wartości, jedną dla każdej ramki, ale jedyną rzeczą, jaką ten program pobiera z tej struktury, jest Duration
ramka w milisekundach.
SKCodec
Definiuje właściwość o nazwie Info
typu SKImageInfo
, ale ta SKImageInfo
wartość wskazuje (przynajmniej dla tego obrazu), że typ koloru to SKColorType.Index8
, co oznacza, że każdy piksel jest indeksem typu koloru. Aby uniknąć przeszkadzania w tabelach kolorów, program używa informacji Width
i Height
z tej struktury, aby utworzyć własną wartość pełnokolorową ImageInfo
. Każda z nich jest tworzona SKBitmap
na podstawie tego.
Metoda GetPixels
SKBitmap
zwraca IntPtr
odwołanie do bitów pikseli tej mapy bitowej. Te bity pikseli nie zostały jeszcze ustawione. Jest to IntPtr
przekazywane do jednej z GetPixels
metod SKCodec
. Ta metoda kopiuje ramkę z pliku GIF do przestrzeni pamięci, do którego odwołuje się IntPtr
element . Konstruktor SKCodecOptions
wskazuje numer ramki:
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
···
public AnimatedGifPage ()
{
InitializeComponent ();
string resourceID = "SkiaSharpFormsDemos.Media.Newtons_cradle_animation_book_2.gif";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
using (SKManagedStream skStream = new SKManagedStream(stream))
using (SKCodec codec = SKCodec.Create(skStream))
{
// Get frame count and allocate bitmaps
int frameCount = codec.FrameCount;
bitmaps = new SKBitmap[frameCount];
durations = new int[frameCount];
accumulatedDurations = new int[frameCount];
// Note: There's also a RepetitionCount property of SKCodec not used here
// Loop through the frames
for (int frame = 0; frame < frameCount; frame++)
{
// From the FrameInfo collection, get the duration of each frame
durations[frame] = codec.FrameInfo[frame].Duration;
// Create a full-color bitmap for each frame
SKImageInfo imageInfo = code.new SKImageInfo(codec.Info.Width, codec.Info.Height);
bitmaps[frame] = new SKBitmap(imageInfo);
// Get the address of the pixels in that bitmap
IntPtr pointer = bitmaps[frame].GetPixels();
// Create an SKCodecOptions value to specify the frame
SKCodecOptions codecOptions = new SKCodecOptions(frame, false);
// Copy pixels from the frame into the bitmap
codec.GetPixels(imageInfo, pointer, codecOptions);
}
// Sum up the total duration
for (int frame = 0; frame < durations.Length; frame++)
{
totalDuration += durations[frame];
}
// Calculate the accumulated durations
for (int frame = 0; frame < durations.Length; frame++)
{
accumulatedDurations[frame] = durations[frame] +
(frame == 0 ? 0 : accumulatedDurations[frame - 1]);
}
}
}
···
}
IntPtr
Pomimo wartości żaden kod nie unsafe
jest wymagany, ponieważ IntPtr
nigdy nie jest konwertowany na wartość wskaźnika języka C#.
Po wyodrębnieniu każdej ramki konstruktor sumuje czas trwania wszystkich ramek, a następnie inicjuje kolejną tablicę ze skumulowanymi czasami trwania.
Pozostała część pliku za pomocą kodu jest przeznaczona do animacji. Metoda Device.StartTimer
służy do uruchamiania czasomierza, a OnTimerTick
wywołanie zwrotne używa Stopwatch
obiektu w celu określenia czasu, który upłynął w milisekundach. Pętla przez skumulowaną tablicę czasu trwania jest wystarczająca do znalezienia bieżącej ramki:
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
Stopwatch stopwatch = new Stopwatch();
bool isAnimating;
int currentFrame;
···
protected override void OnAppearing()
{
base.OnAppearing();
isAnimating = true;
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
stopwatch.Stop();
isAnimating = false;
}
bool OnTimerTick()
{
int msec = (int)(stopwatch.ElapsedMilliseconds % totalDuration);
int frame = 0;
// Find the frame based on the elapsed time
for (frame = 0; frame < accumulatedDurations.Length; frame++)
{
if (msec < accumulatedDurations[frame])
{
break;
}
}
// Save in a field and invalidate the SKCanvasView.
if (currentFrame != frame)
{
currentFrame = frame;
canvasView.InvalidateSurface();
}
return isAnimating;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
// Get the bitmap and center it
SKBitmap bitmap = bitmaps[currentFrame];
canvas.DrawBitmap(bitmap,info.Rect, BitmapStretch.Uniform);
}
}
Za każdym razem, gdy zmienna currentframe
zmienia się, SKCanvasView
jest unieważniona, a nowa ramka jest wyświetlana:
Oczywiście chcesz uruchomić program samodzielnie, aby zobaczyć animację.