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:
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 ProgressBar
und 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 COUNT
liegt, 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 Label
Slider
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:
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 SKCodec
von ü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:
Natürlich möchten Sie das Programm selbst ausführen, um die Animation anzuzeigen.