Animazione di bitmap SkiaSharp
Le applicazioni che animano la grafica SkiaSharp in genere chiamano InvalidateSurface
su SKCanvasView
a una velocità fissa, spesso ogni 16 millisecondi. Invalidando la superficie viene attivata una chiamata al PaintSurface
gestore per ridisegnare la visualizzazione. Man mano che gli oggetti visivi vengono ridisegnati 60 volte al secondo, sembrano essere animati senza problemi.
Tuttavia, se il rendering della grafica è troppo complesso in 16 millisecondi, l'animazione può diventare instabilità. Il programmatore potrebbe scegliere di ridurre la frequenza di aggiornamento a 30 volte o 15 volte al secondo, ma talvolta anche non è sufficiente. A volte la grafica è così complessa che non è possibile eseguire il rendering in tempo reale.
Una soluzione consiste nel preparare l'animazione in anticipo eseguendo il rendering dei singoli fotogrammi dell'animazione in una serie di bitmap. Per visualizzare l'animazione, è necessario visualizzare queste bitmap in sequenza 60 volte al secondo.
Naturalmente, questo è potenzialmente un sacco di bitmap, ma questo è il modo in cui vengono realizzati film animati 3D di grandi dimensioni. La grafica 3D è troppo complessa per essere sottoposta a rendering in tempo reale. Per eseguire il rendering di ogni fotogramma è necessario molto tempo di elaborazione. Quello che vedi quando guardi il film è essenzialmente una serie di bitmap.
Puoi fare qualcosa di simile in SkiaSharp. Questo articolo illustra due tipi di animazione bitmap. Il primo esempio è un'animazione del set Di Mandelbrot:
Il secondo esempio mostra come usare SkiaSharp per eseguire il rendering di un file GIF animato.
Animazione bitmap
Il Set Di Mandelbrot è visivamente affascinante ma a livello di calcolo. (Per una discussione sul Set Di Mandelbrot e la matematica usata qui, vedere Capitolo 20 della creazione di app per dispositivi mobili con Xamarin.Forms a partire dalla pagina 666. La descrizione seguente presuppone che la conoscenza in background.
L'esempio usa l'animazione bitmap per simulare uno zoom continuo di un punto fisso nel set Di Mandelbrot. Lo zoom avanti è seguito dallo zoom indietro e quindi il ciclo si ripete per sempre o fino a quando non si termina il programma.
Il programma si prepara per questa animazione creando fino a 50 bitmap archiviate nell'archiviazione locale dell'applicazione. Ogni bitmap comprende metà della larghezza e dell'altezza del piano complesso come bitmap precedente. Nel programma queste bitmap rappresentano i livelli di zoom integrale. Le bitmap vengono quindi visualizzate in sequenza. Il ridimensionamento di ogni bitmap viene animato per fornire una progressione uniforme da una bitmap a un'altra.
Come il programma finale descritto nel capitolo 20 della creazione di app per dispositivi mobili con Xamarin.Forms, il calcolo del set Di Mandelbrot in Mandelbrot Animation è un metodo asincrono con otto parametri. I parametri includono un punto centrale complesso e una larghezza e altezza del piano complesso che circonda tale punto centrale. I tre parametri successivi sono la larghezza e l'altezza in pixel della bitmap da creare e un numero massimo di iterazioni per il calcolo ricorsivo. Il progress
parametro viene utilizzato per visualizzare lo stato di avanzamento di questo calcolo. Il cancelToken
parametro non viene usato in questo programma:
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);
}
}
Il metodo restituisce un oggetto di tipo BitmapInfo
che fornisce informazioni per la creazione di una bitmap:
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; }
}
Il file XAML dell'animazione Di Mandelbrot include due Label
visualizzazioni, un ProgressBar
e un Button
SKCanvasView
e :
<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>
Il file code-behind inizia definendo tre costanti cruciali e una matrice di bitmap:
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
···
}
A un certo punto, probabilmente vuoi modificare il COUNT
valore su 50 per visualizzare l'intera gamma dell'animazione. I valori superiori a 50 non sono utili. Intorno a un livello di zoom di 48 o così via, la risoluzione dei numeri a virgola mobile a precisione doppia diventa insufficiente per il calcolo del set di Mandelbrot. Questo problema viene descritto nella pagina 684 di Creazione di app per dispositivi mobili con Xamarin.Forms.
Il center
valore è molto importante. Questo è lo stato attivo dello zoom dell'animazione. I tre valori nel file sono quelli usati nelle tre schermate finali del capitolo 20 di Creazione di app per dispositivi mobili con Xamarin.Forms nella pagina 684, ma è possibile sperimentare il programma in quel capitolo per ottenere uno dei propri valori.
L'esempio Di animazione Di Mandelbrot archivia queste COUNT
bitmap nell'archiviazione dell'applicazione locale. Cinquanta bitmap richiedono più di 20 megabyte di spazio di archiviazione nel dispositivo, quindi potresti voler sapere quanto spazio di archiviazione occupano queste bitmap e a un certo punto potresti voler eliminarle tutte. Questo è lo scopo di questi due metodi nella parte inferiore della MainPage
classe:
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();
}
}
È possibile eliminare le bitmap nell'archiviazione locale mentre il programma sta animando le stesse bitmap perché il programma li mantiene in memoria. Ma la prossima volta che esegui il programma, sarà necessario ricreare le bitmap.
Le bitmap archiviate nell'archiviazione dell'applicazione locale incorporano il center
valore nei relativi nomi file, quindi se si modifica l'impostazione center
, le bitmap esistenti non verranno sostituite nell'archiviazione e continueranno a occupare spazio.
Ecco i metodi usati MainPage
per costruire i nomi file, nonché un MakePixel
metodo per definire un valore pixel in base ai componenti di colore:
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);
···
}
Parametro zoomLevel
da impostare FilePath
tra 0 e la COUNT
costante meno 1.
Il MainPage
costruttore chiama il LoadAndStartAnimation
metodo :
public partial class MainPage : ContentPage
{
···
public MainPage()
{
InitializeComponent();
LoadAndStartAnimation();
}
···
}
Il LoadAndStartAnimation
metodo è responsabile dell'accesso all'archiviazione locale dell'applicazione per caricare eventuali bitmap che potrebbero essere state create quando il programma è stato eseguito in precedenza. Esegue un ciclo tra zoomLevel
i valori compresi tra 0 e COUNT
. Se il file esiste, lo carica nella bitmaps
matrice. In caso contrario, deve creare una bitmap per i valori e zoomLevel
specifici center
chiamando Mandelbrot.CalculateAsync
. Questo metodo ottiene il numero di iterazioni per ogni pixel, che questo metodo converte in colori:
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);
}
···
}
Si noti che il programma archivia queste bitmap nell'archiviazione dell'applicazione locale anziché nella libreria di foto del dispositivo. La libreria .NET Standard 2.0 consente di usare i metodi e File.WriteAllBytes
familiari File.OpenRead
per questa attività.
Dopo che tutte le bitmap sono state create o caricate in memoria, il metodo avvia un Stopwatch
oggetto e chiama Device.StartTimer
. Il OnTimerTick
metodo viene chiamato ogni 16 millisecondi.
OnTimerTick
calcola un time
valore in millisecondi compreso tra 0 e 6000 volte COUNT
, che ripartisce sei secondi per la visualizzazione di ogni bitmap. Il progress
valore usa il Math.Sin
valore per creare un'animazione sinusoidale che sarà più lenta all'inizio del ciclo e più lenta alla fine mentre inverte la direzione.
Il progress
valore è compreso tra 0 e COUNT
. Ciò significa che la parte integer di progress
è un indice nella bitmaps
matrice, mentre la parte frazionaria di indica un livello di progress
zoom per tale bitmap specifica. Questi valori vengono archiviati nei bitmapIndex
campi e bitmapProgress
e vengono visualizzati da Label
e Slider
nel file XAML. L'oggetto SKCanvasView
viene invalidato per aggiornare la visualizzazione bitmap:
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;
}
···
}
Infine, il PaintSurface
gestore dell'oggetto SKCanvasView
calcola un rettangolo di destinazione per visualizzare la bitmap il più grande possibile mantenendo le proporzioni. Un rettangolo di origine si basa sul bitmapProgress
valore . Il fraction
valore calcolato qui varia da 0 quando bitmapProgress
è 0 per visualizzare l'intera bitmap, fino a 0,25 quando bitmapProgress
è 1 per visualizzare metà della larghezza e altezza della bitmap, in modo efficace lo zoom avanti:
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);
}
}
···
}
Ecco il programma in esecuzione:
Animazione GIF
La specifica GIF (Graphics Interchange Format) include una funzionalità che consente a un singolo file GIF di contenere più fotogrammi sequenziali di una scena che possono essere visualizzati in successione, spesso in un ciclo. Questi file sono noti come GIF animate. I Web browser possono riprodurre GIF animate e SkiaSharp consente a un'applicazione di estrarre i fotogrammi da un file GIF animato e di visualizzarli in sequenza.
L'esempio include una risorsa GIF animata denominata Newtons_cradle_animation_book_2.gif creata da DemonDe Luxe e scaricata dalla pagina Della culla di Newton in Wikipedia. La pagina GIF animata include un file XAML che fornisce le informazioni e crea un'istanza SKCanvasView
di :
<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>
Il file code-behind non è generalizzato per riprodurre alcun file GIF animato. Ignora alcune delle informazioni disponibili, in particolare un conteggio delle ripetizioni, e riproduce semplicemente la GIF animata in un ciclo.
L'uso di SkisSharp per estrarre i fotogrammi di un file GIF animato non sembra essere documentato da nessuna parte, quindi la descrizione del codice che segue è più dettagliata del solito:
La decodifica del file GIF animato si verifica nel costruttore della pagina e richiede che l'oggetto Stream
che fa riferimento alla bitmap venga usato per creare un SKManagedStream
oggetto e quindi un SKCodec
oggetto . La FrameCount
proprietà indica il numero di fotogrammi che costituiscono l'animazione.
Questi fotogrammi vengono infine salvati come singole bitmap, quindi il costruttore usa FrameCount
per allocare una matrice di tipo SKBitmap
e due int
matrici per la durata di ogni fotogramma e (per semplificare la logica di animazione) le durate accumulate.
La FrameInfo
proprietà della SKCodec
classe è una matrice di SKCodecFrameInfo
valori, una per ogni fotogramma, ma l'unica cosa che questo programma prende da tale struttura è il Duration
del frame in millisecondi.
SKCodec
definisce una proprietà denominata Info
di tipo SKImageInfo
, ma tale SKImageInfo
valore indica (almeno per questa immagine) che il tipo di colore è SKColorType.Index8
, il che significa che ogni pixel è un indice in un tipo di colore. Per evitare di disturbare le tabelle dei colori, il programma usa le Width
informazioni e Height
da tale struttura per costruire il proprio valore full-color ImageInfo
. Ogni SKBitmap
oggetto viene creato da questo.
Il GetPixels
metodo di SKBitmap
restituisce un IntPtr
riferimento ai bit pixel di tale bitmap. Questi bit di pixel non sono ancora stati impostati. Che IntPtr
viene passato a uno dei GetPixels
metodi di SKCodec
. Questo metodo copia il frame dal file GIF nello spazio di memoria a cui fa riferimento l'oggetto IntPtr
. Il SKCodecOptions
costruttore indica il numero di frame:
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]);
}
}
}
···
}
Nonostante il IntPtr
valore, non è necessario alcun unsafe
codice perché non IntPtr
viene mai convertito in un valore puntatore C#.
Dopo l'estrazione di ogni fotogramma, il costruttore totalizza le durate di tutti i fotogrammi e quindi inizializza un'altra matrice con le durate accumulate.
Il resto del file code-behind è dedicato all'animazione. Il Device.StartTimer
metodo viene usato per avviare un timer e il OnTimerTick
callback usa un Stopwatch
oggetto per determinare il tempo trascorso in millisecondi. Il ciclo attraverso la matrice di durate accumulate è sufficiente per trovare il frame corrente:
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);
}
}
Ogni volta che la currentframe
variabile cambia, viene SKCanvasView
invalidata e viene visualizzato il nuovo frame:
Naturalmente, vuoi eseguire il programma manualmente per vedere l'animazione.