Animation des bitmaps SkiaSharp
Les applications qui animent les graphiques SkiaSharp appellent InvalidateSurface
généralement à SKCanvasView
un débit fixe, souvent toutes les 16 millisecondes. L’invalidation de la surface déclenche un appel au PaintSurface
gestionnaire pour redessiner l’affichage. Comme les visuels sont redessinés 60 fois par seconde, ils semblent être animés en douceur.
Toutefois, si les graphiques sont trop complexes à restituer en 16 millisecondes, l’animation peut devenir gigue. Le programmeur peut choisir de réduire le taux d’actualisation à 30 fois ou 15 fois par seconde, mais parfois même cela n’est pas suffisant. Parfois, les graphiques sont si complexes qu’ils ne peuvent simplement pas être rendus en temps réel.
Une solution consiste à préparer l’animation au préalable en affichant les images individuelles de l’animation sur une série de bitmaps. Pour afficher l’animation, il est nécessaire d’afficher ces bitmaps de manière séquentielle 60 fois par seconde.
Bien sûr, c’est potentiellement beaucoup de bitmaps, mais c’est la façon dont les films d’animation 3D grand budget sont faits. Les graphiques 3D sont beaucoup trop complexes à rendre en temps réel. Un grand nombre de temps de traitement est nécessaire pour restituer chaque image. Ce que vous voyez quand vous regardez le film est essentiellement une série de bitmaps.
Vous pouvez faire quelque chose de similaire dans SkiaSharp. Cet article illustre deux types d’animation bitmap. Le premier exemple est une animation de l’ensemble De Mandelbrot :
Le deuxième exemple montre comment utiliser SkiaSharp pour afficher un fichier GIF animé.
Animation bitmap
L’ensemble de Mandelbrot est visuellement fascinant mais computionalment long. (Pour une discussion sur l’ensemble de Mandelbrot et les mathématiques utilisées ici, voir Chapitre 20 de la création d’applications mobiles à Xamarin.Forms partir de la page 666. La description suivante suppose que les connaissances en arrière-plan.)
L’exemple utilise l’animation bitmap pour simuler un zoom continu d’un point fixe dans l’ensemble De Mandelbrot. Le zoom avant est suivi d’un zoom arrière, puis le cycle se répète indéfiniment ou jusqu’à la fin du programme.
Le programme prépare cette animation en créant jusqu’à 50 bitmaps qu’il stocke dans le stockage local de l’application. Chaque bitmap englobe la moitié de la largeur et de la hauteur du plan complexe comme bitmap précédente. (Dans le programme, ces bitmaps sont dites représenter des niveaux de zoom intégral.) Les bitmaps sont ensuite affichées en séquence. La mise à l’échelle de chaque bitmap est animée pour fournir une progression fluide d’une bitmap à une autre.
Comme le programme final décrit dans le chapitre 20 de la création d’applications mobiles avec Xamarin.Forms, le calcul de l’ensemble de Mandelbrot dans l’animation De Mandelbrot est une méthode asynchrone avec huit paramètres. Les paramètres incluent un point central complexe, ainsi qu’une largeur et une hauteur du plan complexe entourant ce point central. Les trois paramètres suivants sont la largeur et la hauteur des pixels de la bitmap à créer, ainsi qu’un nombre maximal d’itérations pour le calcul récursif. Le progress
paramètre est utilisé pour afficher la progression de ce calcul. Le cancelToken
paramètre n’est pas utilisé dans ce programme :
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);
}
}
La méthode retourne un objet de type BitmapInfo
qui fournit des informations pour la création d’une 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; }
}
Le fichier XAML d’animation De Mandelbrot comprend deux Label
vues, un ProgressBar
, ainsi Button
que les SKCanvasView
éléments suivants :
<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>
Le fichier code-behind commence par définir trois constantes cruciales et un tableau de 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
···
}
À un moment donné, vous souhaiterez probablement modifier la COUNT
valeur sur 50 pour afficher la plage complète de l’animation. Les valeurs supérieures à 50 ne sont pas utiles. Autour d’un niveau de zoom de 48, la résolution des nombres à virgule flottante double précision devient insuffisante pour le calcul de l’ensemble de Mandelbrot. Ce problème est abordé à la page 684 de la création d’applications mobiles avec Xamarin.Forms.
La center
valeur est très importante. Il s’agit du zoom d’animation. Les trois valeurs du fichier sont celles utilisées dans les trois captures d’écran finales du chapitre 20 de la création d’applications mobiles avec Xamarin.Forms la page 684, mais vous pouvez expérimenter le programme dans ce chapitre pour obtenir l’une de vos propres valeurs.
L’exemple Animation De Mandelbrot stocke ces COUNT
bitmaps dans le stockage d’applications locales. Cinquante bitmaps nécessitent plus de 20 mégaoctets de stockage sur votre appareil. Vous pouvez donc savoir combien de stockage ces bitmaps occupent, et à un moment donné, vous souhaiterez peut-être les supprimer. C’est l’objectif de ces deux méthodes en bas de la 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();
}
}
Vous pouvez supprimer les bitmaps dans le stockage local pendant que le programme anime ces mêmes bitmaps, car le programme les conserve en mémoire. Mais la prochaine fois que vous exécutez le programme, il devra recréer les bitmaps.
Les bitmaps stockées dans le stockage d’applications locales incorporent la center
valeur dans leurs noms de fichiers. Par conséquent, si vous modifiez le center
paramètre, les bitmaps existantes ne sont pas remplacées dans le stockage et continuent d’occuper de l’espace.
Voici les méthodes utilisées MainPage
pour construire les noms de fichiers, ainsi qu’une méthode pour définir une MakePixel
valeur de pixel en fonction des composants de couleur :
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);
···
}
Paramètre zoomLevel
à FilePath
passer de 0 à la COUNT
constante moins 1.
Le MainPage
constructeur appelle la LoadAndStartAnimation
méthode :
public partial class MainPage : ContentPage
{
···
public MainPage()
{
InitializeComponent();
LoadAndStartAnimation();
}
···
}
La LoadAndStartAnimation
méthode est chargée d’accéder au stockage local de l’application pour charger les bitmaps qui ont peut-être été créées lors de l’exécution du programme précédemment. Il effectue une boucle dans zoomLevel
les valeurs comprises entre 0 et COUNT
. Si le fichier existe, il le charge dans le bitmaps
tableau. Sinon, il doit créer une bitmap pour les valeurs et les particuliers center
en appelant Mandelbrot.CalculateAsync
.zoomLevel
Cette méthode obtient le nombre d’itérations pour chaque pixel, que cette méthode convertit en couleurs :
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);
}
···
}
Notez que le programme stocke ces bitmaps dans le stockage d’applications locales plutôt que dans la bibliothèque de photos de l’appareil. La bibliothèque .NET Standard 2.0 permet d’utiliser les méthodes et File.WriteAllBytes
les méthodes familières File.OpenRead
pour cette tâche.
Une fois que toutes les bitmaps ont été créées ou chargées en mémoire, la méthode démarre un Stopwatch
objet et appelle Device.StartTimer
. La OnTimerTick
méthode est appelée toutes les 16 millisecondes.
OnTimerTick
calcule une time
valeur en millisecondes comprise entre 0 et 6 000 fois COUNT
, ce qui répartit six secondes pour l’affichage de chaque bitmap. La progress
valeur utilise la Math.Sin
valeur pour créer une animation sinusoïde qui sera plus lente au début du cycle, et plus lente à la fin, car elle inverse la direction.
La progress
valeur est comprise entre 0 et COUNT
. Cela signifie que la partie entière d’est progress
un index dans le bitmaps
tableau, tandis que la partie fractionnaire d’indique un niveau de progress
zoom pour cette bitmap particulière. Ces valeurs sont stockées dans les bitmapIndex
champs et bitmapProgress
sont affichées par le Label
fichier XAML et Slider
dans le fichier XAML. Il SKCanvasView
est invalidé pour mettre à jour l’affichage 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;
}
···
}
Enfin, le PaintSurface
gestionnaire du rectangle de destination calcule un rectangle de SKCanvasView
destination pour afficher le bitmap le plus grand possible tout en conservant le rapport d’aspect. Un rectangle source est basé sur la bitmapProgress
valeur. La fraction
valeur calculée ici est comprise entre 0 et bitmapProgress
0 pour afficher l’intégralité de la bitmap, à 0,25 quand est bitmapProgress
1 pour afficher la moitié de la largeur et de la hauteur de la bitmap, en zoomant efficacement :
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);
}
}
···
}
Voici le programme en cours d’exécution :
Animation GIF
La spécification GIF (Graphics Interchange Format) inclut une fonctionnalité qui permet à un seul fichier GIF de contenir plusieurs images séquentielles d’une scène qui peut être affichée en succession, souvent dans une boucle. Ces fichiers sont appelés gif animés. Les navigateurs web peuvent lire des GIF animés et SkiaSharp permet à une application d’extraire les images d’un fichier GIF animé et de les afficher de manière séquentielle.
L’exemple inclut une ressource GIF animée nommée Newtons_cradle_animation_book_2.gif créée par DemonDeLuxe et téléchargée à partir de la page Newton’s Cradle dans Wikipédia. La page GIF animée inclut un fichier XAML qui fournit ces informations et instancie un 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>
Le fichier code-behind n’est pas généralisé pour lire un fichier GIF animé. Il ignore certaines informations disponibles, en particulier un nombre de répétitions, et lit simplement le GIF animé dans une boucle.
L’utilisation de SkisSharp pour extraire les images d’un fichier GIF animé ne semble pas être documentée n’importe où, de sorte que la description du code qui suit est plus détaillée que d’habitude :
Le décodage du fichier GIF animé se produit dans le constructeur de la page et nécessite que l’objet Stream
référençant la bitmap soit utilisé pour créer un SKManagedStream
objet, puis un SKCodec
objet. La FrameCount
propriété indique le nombre d’images qui composent l’animation.
Ces images sont finalement enregistrées en tant que bitmaps individuelles, de sorte que le constructeur utilise FrameCount
pour allouer un tableau de type SKBitmap
ainsi que deux int
tableaux pour la durée de chaque image et (pour faciliter la logique d’animation) les durées cumulées.
La FrameInfo
propriété de SKCodec
classe est un tableau de SKCodecFrameInfo
valeurs, une pour chaque image, mais la seule chose que ce programme prend à partir de cette structure est l’image Duration
en millisecondes.
SKCodec
définit une propriété nommée Info
de type SKImageInfo
, mais cette SKImageInfo
valeur indique (au moins pour cette image) que le type de couleur est SKColorType.Index8
, ce qui signifie que chaque pixel est un index dans un type de couleur. Pour éviter de déranger les tables de couleurs, le programme utilise les informations et Height
les Width
informations de cette structure pour construire sa propre valeur de couleur ImageInfo
complète. Chacun SKBitmap
est créé à partir de cela.
La GetPixels
méthode de SKBitmap
retourne un IntPtr
référencement des bits de pixels de cette bitmap. Ces bits de pixel n’ont pas encore été définis. C’est IntPtr
passé à l’une GetPixels
des méthodes de SKCodec
. Cette méthode copie le frame du fichier GIF dans l’espace mémoire référencé par le IntPtr
. Le SKCodecOptions
constructeur indique le numéro d’image :
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]);
}
}
}
···
}
Malgré la IntPtr
valeur, aucun code n’est unsafe
requis, car il IntPtr
n’est jamais converti en valeur de pointeur C#.
Une fois que chaque image a été extraite, le constructeur totalise les durées de toutes les images, puis initialise un autre tableau avec les durées cumulées.
Le reste du fichier code-behind est dédié à l’animation. La Device.StartTimer
méthode est utilisée pour démarrer un minuteur en cours et le OnTimerTick
rappel utilise un Stopwatch
objet pour déterminer le temps écoulé en millisecondes. La boucle dans le tableau de durées cumulées est suffisante pour trouver le frame actuel :
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);
}
}
Chaque fois que la currentframe
variable change, elle SKCanvasView
est invalidée et la nouvelle image s’affiche :
Bien sûr, vous voudrez exécuter le programme vous-même pour voir l’animation.