SkiaSharp ビットマップのアニメーション化
SkiaSharp グラフィックスをアニメーション化するアプリケーションでは、通常、固定レート (多くの場合 16 ミリ秒ごと) の SKCanvasView
で InvalidateSurface
が呼び出されます。 サーフェスを無効にすると、PaintSurface
ハンドラーへの呼び出しがトリガーされ、ディスプレイが再描画されます。 ビジュアルが 1 秒に 60 回再描画されると、スムーズにアニメーション化されているように見えます。
ただし、グラフィックスが複雑すぎて 16 ミリ秒でレンダリングできない場合は、アニメーションの動きがぎこちなくなる可能性があります。 プログラマーは、リフレッシュ レートを 1 秒間に 30 回または 15 回に減らすことを選択できますが、それでも十分でない場合もあります。 グラフィックスが非常に複雑で、リアルタイムでレンダリングできない場合があります。
1 つの解決策は、一連のビットマップでアニメーションの個々のフレームをレンダリングすることによって、事前にアニメーションを準備することです。 このアニメーションを表示するには、これらのビットマップを 1 秒に 60 回連続して表示するだけで済みます。
もちろん、ビットマップの数が多くなる可能性がありますが、これは大きな予算が付く 3D アニメーション映画で用いられる方法です。 3D グラフィックスは複雑すぎるため、リアルタイムでレンダリングできません。 各フレームをレンダリングするには、多くの処理時間が必要です。 映画を見ているときに目にするものは、基本的に一連のビットマップです。
SkiaSharp でも同様のことを行うことができます。 この記事では、2 種類のビットマップ アニメーションについて説明します。 最初の例は、マンデルブロ集合のアニメーションです。
2 番目の例は、SkiaSharp を使用してアニメーション GIF ファイルをレンダリングする方法を示します。
ビットマップ アニメーション
マンデルブロ集合は視覚的に美しく見えますが、計算には時間がかかります。 (マンデルブロ集合と、ここで使用する数学的な処理については、「Xamarin.Forms を使用したモバイル アプリの作成」の 666 ページから始まる第 20 章を参照してください。以下の説明では、その背景知識を前提としています)。
このサンプルでは、ビットマップ アニメーションを使用して、マンデルブロ集合内の固定点の連続ズームをシミュレートします。 ズームインの後にズームアウトが続き、プログラムが終了するまでこのサイクルがずっと繰り返されます。
このアニメーションの準備としてプログラムでは、アプリケーションのローカル ストレージに格納される最大 50 個のビットマップを作成します。 各ビットマップは、前のビットマップの半分の幅と高さの複素平面を包含します (このプログラムでは、これらのビットマップは、整数のズーム レベルを表しています)。その後、ビットマップが順番に表示されます。 各ビットマップのスケーリングは、1 つのビットマップから別のビットマップへスムーズに進むようにアニメーション化されます。
「Xamarin.Forms を使用したモバイル アプリの作成」の第 20 章で説明されている最後のプログラムと同様に、マンデルブロ アニメーションのマンデルブロ集合の計算は、8 つのパラメーターを持つ非同期メソッドで行われます。 パラメーターには、複素数の中心点と、その中心点を囲む複素平面の幅と高さが含まれます。 次の 3 つのパラメーターは、作成するビットマップのピクセルの幅と高さ、および再帰的な計算の反復の最大回数です。 この progress
パラメーターは、この計算の進行状況を表示するために使用されます。 この cancelToken
パラメーターは、このプログラムでは使用されません。
static class Mandelbrot
{
public static Task<BitmapInfo> CalculateAsync(Complex center,
double width, double height,
int pixelWidth, int pixelHeight,
int iterations,
IProgress<double> progress,
CancellationToken cancelToken)
{
return Task.Run(() =>
{
int[] iterationCounts = new int[pixelWidth * pixelHeight];
int index = 0;
for (int row = 0; row < pixelHeight; row++)
{
progress.Report((double)row / pixelHeight);
cancelToken.ThrowIfCancellationRequested();
double y = center.Imaginary + height / 2 - row * height / pixelHeight;
for (int col = 0; col < pixelWidth; col++)
{
double x = center.Real - width / 2 + col * width / pixelWidth;
Complex c = new Complex(x, y);
if ((c - new Complex(-1, 0)).Magnitude < 1.0 / 4)
{
iterationCounts[index++] = -1;
}
// http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
else if (c.Magnitude * c.Magnitude * (8 * c.Magnitude * c.Magnitude - 3) < 3.0 / 32 - c.Real)
{
iterationCounts[index++] = -1;
}
else
{
Complex z = 0;
int iteration = 0;
do
{
z = z * z + c;
iteration++;
}
while (iteration < iterations && z.Magnitude < 2);
if (iteration == iterations)
{
iterationCounts[index++] = -1;
}
else
{
iterationCounts[index++] = iteration;
}
}
}
}
return new BitmapInfo(pixelWidth, pixelHeight, iterationCounts);
}, cancelToken);
}
}
このメソッドは、ビットマップを作成するための情報を提供する型 BitmapInfo
のオブジェクトを返します。
class BitmapInfo
{
public BitmapInfo(int pixelWidth, int pixelHeight, int[] iterationCounts)
{
PixelWidth = pixelWidth;
PixelHeight = pixelHeight;
IterationCounts = iterationCounts;
}
public int PixelWidth { private set; get; }
public int PixelHeight { private set; get; }
public int[] IterationCounts { private set; get; }
}
マンデルブロ アニメーション XAML ファイルには、2 つの Label
ビューと ProgressBar
、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>
分離コード ファイルは、3 つの重要な定数とビットマップの配列を定義することから始まります。
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
···
}
ある時点で、アニメーションの全範囲を表示するために、COUNT
値を 50 に変更することができます。 50 を超える値は役に立ちません。 ズーム レベルが 48 程度で、倍精度浮動小数点数の解像度がマンデルブロ集合の計算には不十分になります。 この問題については、「Xamarin.Forms を使用したモバイル アプリの作成」の 684 ページで説明されています。
center
値は非常に重要です。 これがアニメーション ズームのフォーカスになります。 ファイル内の 3 つの値は、「Xamarin.Forms を使用したモバイル アプリの作成」の 684 ページ第 20 章の 3 つの最後のスクリーンショットで使用されていますが、その章のプログラムを試して、独自の値を見出すことができます。
マンデルブロアニメーション サンプルでは、これらの COUNT
ビットマップをローカル アプリケーション ストレージに格納します。 50 個のビットマップでは、デバイス上に 20 MB を超えるストレージが必要であるため、これらのビットマップが占有しているストレージの量を把握し、ある時点ですべてのビットマップを削除する必要があるかもしれません。 これは、MainPage
クラスの下部にある次の 2 つのメソッドの目的です。
public partial class MainPage : ContentPage
{
···
void TallyBitmapSizes()
{
long fileSize = 0;
foreach (string filename in Directory.EnumerateFiles(FolderPath()))
{
fileSize += new FileInfo(filename).Length;
}
storageLabel.Text = $"Total storage: {fileSize:N0} bytes";
}
void OnDeleteButtonClicked(object sender, EventArgs args)
{
foreach (string filepath in Directory.EnumerateFiles(FolderPath()))
{
File.Delete(filepath);
}
TallyBitmapSizes();
}
}
プログラムが同じビットマップをメモリに保持するため、プログラムがこれらのビットマップをアニメーション化している間に、ローカル ストレージ内のビットマップを削除できます。 ただし、次にプログラムを実行するときは、ビットマップを再作成する必要があります。
ローカル アプリケーション ストレージに格納されているビットマップには、ファイル名に center
値が組み込まれているため、center
設定を変更した場合、既存のビットマップはストレージで置き換えられず、引き続きスペースを占有し続けます。
ファイルに名前を付けるために使用する MainPage
が使用するメソッドと、カラー コンポーネントに基づいてピクセル値を定義するための MakePixel
メソッドを以下に示します。
public partial class MainPage : ContentPage
{
···
// File path for storing each bitmap in local storage
string FolderPath() =>
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string FilePath(int zoomLevel) =>
Path.Combine(FolderPath(),
String.Format("R{0}I{1}Z{2:D2}.png", center.Real, center.Imaginary, zoomLevel));
// Form bitmap pixel for Rgba8888 format
uint MakePixel(byte alpha, byte red, byte green, byte blue) =>
(uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
···
}
zoomLevel
パラメーターから FilePath
の範囲は、0 から定数 COUNT
- 1 になります。
MainPage
コンストラクターは LoadAndStartAnimation
メソッドを呼び出します。
public partial class MainPage : ContentPage
{
···
public MainPage()
{
InitializeComponent();
LoadAndStartAnimation();
}
···
}
LoadAndStartAnimation
メソッドは、プログラムが以前に実行されたときに作成された可能性のあるビットマップを読み込むために、アプリケーション ローカル ストレージにアクセスします。 0 から COUNT
までの zoomLevel
値をループ処理します。 ファイルが存在する場合は、bitmaps
配列に読み込まれます。 存在しない場合は、Mandelbrot.CalculateAsync
を呼び出して特定の center
と zoomLevel
値のビットマップを作成する必要があります。 そのメソッドは各ピクセルの反復カウントを取得し、それを色に変換します。
public partial class MainPage : ContentPage
{
···
async void LoadAndStartAnimation()
{
// Show total bitmap storage
TallyBitmapSizes();
// Create progressReporter for async operation
Progress<double> progressReporter =
new Progress<double>((double progress) => progressBar.Progress = progress);
// Create (unused) CancellationTokenSource for async operation
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
// Loop through all the zoom levels
for (int zoomLevel = 0; zoomLevel < COUNT; zoomLevel++)
{
// If the file exists, load it
if (File.Exists(FilePath(zoomLevel)))
{
statusLabel.Text = $"Loading bitmap for zoom level {zoomLevel}";
using (Stream stream = File.OpenRead(FilePath(zoomLevel)))
{
bitmaps[zoomLevel] = SKBitmap.Decode(stream);
}
}
// Otherwise, create a new bitmap
else
{
statusLabel.Text = $"Creating bitmap for zoom level {zoomLevel}";
CancellationToken cancelToken = cancelTokenSource.Token;
// Do the (generally lengthy) Mandelbrot calculation
BitmapInfo bitmapInfo =
await Mandelbrot.CalculateAsync(center,
4 / Math.Pow(2, zoomLevel),
4 / Math.Pow(2, zoomLevel),
BITMAP_SIZE, BITMAP_SIZE,
(int)Math.Pow(2, 10), progressReporter, cancelToken);
// Create bitmap & get pointer to the pixel bits
SKBitmap bitmap = new SKBitmap(BITMAP_SIZE, BITMAP_SIZE, SKColorType.Rgba8888, SKAlphaType.Opaque);
IntPtr basePtr = bitmap.GetPixels();
// Set pixel bits to color based on iteration count
for (int row = 0; row < bitmap.Width; row++)
for (int col = 0; col < bitmap.Height; col++)
{
int iterationCount = bitmapInfo.IterationCounts[row * bitmap.Width + col];
uint pixel = 0xFF000000; // black
if (iterationCount != -1)
{
double proportion = (iterationCount / 32.0) % 1;
byte red = 0, green = 0, blue = 0;
if (proportion < 0.5)
{
red = (byte)(255 * (1 - 2 * proportion));
blue = (byte)(255 * 2 * proportion);
}
else
{
proportion = 2 * (proportion - 0.5);
green = (byte)(255 * proportion);
blue = (byte)(255 * (1 - proportion));
}
pixel = MakePixel(0xFF, red, green, blue);
}
// Calculate pointer to pixel
IntPtr pixelPtr = basePtr + 4 * (row * bitmap.Width + col);
unsafe // requires compiling with unsafe flag
{
*(uint*)pixelPtr.ToPointer() = pixel;
}
}
// Save as PNG file
SKData data = SKImage.FromBitmap(bitmap).Encode();
try
{
File.WriteAllBytes(FilePath(zoomLevel), data.ToArray());
}
catch
{
// Probably out of space, but just ignore
}
// Store in array
bitmaps[zoomLevel] = bitmap;
// Show new bitmap sizes
TallyBitmapSizes();
}
// Display the bitmap
bitmapIndex = zoomLevel;
canvasView.InvalidateSurface();
}
// Now start the animation
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
···
}
プログラムは、デバイスの写真ライブラリではなく、ローカル アプリケーション ストレージにこれらのビットマップを格納していることに注目してください。 .NET Standard 2.0 ライブラリを使用することで、使い慣れた File.OpenRead
や File.WriteAllBytes
メソッドをこのタスクに使用できます。
すべてのビットマップが作成されたか、メモリに読み込まれたら、メソッドは Stopwatch
オブジェクトを開始して、Device.StartTimer
を呼び出します。 OnTimerTick
メソッドは、16 ミリ秒ごとに呼び出されます。
OnTimerTick
は、0 から 6,000 倍の COUNT
範囲で time
値をミリ秒単位で計算し、各ビットマップの表示に 6 秒を割り当てます。 progress
値は Math.Sin
値を使用して、正弦波のアニメーションを作成します。サイクルの開始時に速度が遅くなり、逆方向に向かう最後にも遅くなります。
progress
値の範囲は 0 から COUNT
です。 つまり、整数部分 progress
は bitmaps
配列へのインデックスであり、progress
の小数部分はその特定のビットマップのズーム レベルを示します。 これらの値は、bitmapIndex
と bitmapProgress
フィールドに格納され、XAML ファイルの Label
と Slider
で表示されます。 SKCanvasView
はビットマップの表示を更新するために無効にされます。
public partial class MainPage : ContentPage
{
···
Stopwatch stopwatch = new Stopwatch(); // for the animation
int bitmapIndex;
double bitmapProgress = 0;
···
bool OnTimerTick()
{
int cycle = 6000 * COUNT; // total cycle length in milliseconds
// Time in milliseconds from 0 to cycle
int time = (int)(stopwatch.ElapsedMilliseconds % cycle);
// Make it sinusoidal, including bitmap index and gradation between bitmaps
double progress = COUNT * 0.5 * (1 + Math.Sin(2 * Math.PI * time / cycle - Math.PI / 2));
// These are the field values that the PaintSurface handler uses
bitmapIndex = (int)progress;
bitmapProgress = progress - bitmapIndex;
// It doesn't often happen that we get up to COUNT, but an exception would be raised
if (bitmapIndex < COUNT)
{
// Show progress in UI
statusLabel.Text = $"Displaying bitmap for zoom level {bitmapIndex}";
progressBar.Progress = bitmapProgress;
// Update the canvas
canvasView.InvalidateSurface();
}
return true;
}
···
}
最後に、SKCanvasView
の PaintSurface
ハンドラーは、縦横比を維持しながら、ビットマップを可能な限り大きく表示するために到達点となる四角形を計算します。 ソース四角形は bitmapProgress
値に基づきます。 ここで計算される fraction
値は、0 (ビットマップ全体を表示するため bitmapProgress
が 0 の場合) から、0.25 (ビットマップの幅と高さの半分を表示するため bitmapProgress
が 1 の場合) までの範囲になり、実質的にズームします。
public partial class MainPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
if (bitmaps[bitmapIndex] != null)
{
// Determine destination rect as square in canvas
int dimension = Math.Min(info.Width, info.Height);
float x = (info.Width - dimension) / 2;
float y = (info.Height - dimension) / 2;
SKRect destRect = new SKRect(x, y, x + dimension, y + dimension);
// Calculate source rectangle based on fraction:
// bitmapProgress == 0: full bitmap
// bitmapProgress == 1: half of length and width of bitmap
float fraction = 0.5f * (1 - (float)Math.Pow(2, -bitmapProgress));
SKBitmap bitmap = bitmaps[bitmapIndex];
int width = bitmap.Width;
int height = bitmap.Height;
SKRect sourceRect = new SKRect(fraction * width, fraction * height,
(1 - fraction) * width, (1 - fraction) * height);
// Display the bitmap
canvas.DrawBitmap(bitmap, sourceRect, destRect);
}
}
···
}
実行中のプログラムを次に示します。
GIF アニメーション
グラフィックス交換形式 (GIF) 仕様には、1 つの GIF ファイルに連続して表示できるシーンの複数の連続フレーム (多くの場合ループ) を含めることができる機能があります。 これらのファイルは "アニメーション GIF" と呼ばれます。 Web ブラウザーはアニメーション GIF を再生できます。SkiaSharp を使用すると、アプリケーションはアニメーション GIF ファイルからフレームを抽出し、それらを順番に表示できます。
このサンプルには、DemonDeLuxe によって作成された Newtons_cradle_animation_book_2.gif という名前のアニメーション GIF リソースが含まれています。これは、Wikipedia の「ニュートンのゆりかご」のページからダウンロードしました。 アニメーション GIF ページには、その情報を提供し、SKCanvasView
をインスタンス化する XAML ファイルが含まれています。
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Bitmaps.AnimatedGifPage"
Title="Animated GIF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="0"
PaintSurface="OnCanvasViewPaintSurface" />
<Label Text="GIF file by DemonDeLuxe from Wikipedia Newton's Cradle page"
Grid.Row="1"
Margin="0, 5"
HorizontalTextAlignment="Center" />
</Grid>
</ContentPage>
分離コード ファイルは、アニメーション GIF ファイルを再生するために一般化されていません。 使用可能な情報の一部 (特に繰り返し数) は無視し、アニメーション GIF をループで再生します。
SkisSharp を使用してアニメーション GIF ファイルのフレームを抽出する方法は、どこにも文書化されていないようなので、次のコードの説明は通常よりも詳しく説明します。
アニメーション GIF ファイルのデコードは、ページのコンストラクターで行われ、ビットマップを参照する Stream
オブジェクトを使用して、SKManagedStream
オブジェクトを作成してから SKCodec
オブジェクトを作成する必要があります。 この FrameCount
プロパティは、アニメーションを構成するフレームの数を示します。
これらのフレームは最終的に個々のビットマップとして保存されるため、コンストラクターは FrameCount
を使用して、各フレームの期間と、累積された継続時間 (アニメーション ロジックを容易にするために) に対して型 SKBitmap
の配列と 2 つの int
配列を割り当てます。
SKCodec
クラスの FrameInfo
プロパティは、SKCodecFrameInfo
値の配列 (フレームごとに 1 つずつ) になりますが、このプログラムがその構造体から受け取る唯一のものは、ミリ秒単位のフレームの Duration
になります。
SKCodec
は SKImageInfo
型の Info
という名前のプロパティを定義しますが、その SKImageInfo
値は色の種類が SKColorType.Index8
(少なくともこの画像の場合) であることを示します。これは、各ピクセルが色の種類のインデックスであることを意味します。 カラー テーブルを扱う手間を省くために、プログラムはその構造体の Width
と Height
の情報を使用して、独自のフル カラー ImageInfo
値を作成します。 それぞれ SKBitmap
がそこから作成されます。
SKBitmap
の GetPixels
メソッドは、そのビットマップのピクセル ビットを参照する IntPtr
値を返します。 これらのピクセル ビットはまだ設定されていません。 その IntPtr
は、SKCodec
の GetPixels
メソッドのいずれかに渡されます。 そのメソッドは、GIF ファイルから、IntPtr
によって参照されるメモリ領域にフレームをコピーします。 SKCodecOptions
コンストラクターはフレーム番号を示します。
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
···
public AnimatedGifPage ()
{
InitializeComponent ();
string resourceID = "SkiaSharpFormsDemos.Media.Newtons_cradle_animation_book_2.gif";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
using (SKManagedStream skStream = new SKManagedStream(stream))
using (SKCodec codec = SKCodec.Create(skStream))
{
// Get frame count and allocate bitmaps
int frameCount = codec.FrameCount;
bitmaps = new SKBitmap[frameCount];
durations = new int[frameCount];
accumulatedDurations = new int[frameCount];
// Note: There's also a RepetitionCount property of SKCodec not used here
// Loop through the frames
for (int frame = 0; frame < frameCount; frame++)
{
// From the FrameInfo collection, get the duration of each frame
durations[frame] = codec.FrameInfo[frame].Duration;
// Create a full-color bitmap for each frame
SKImageInfo imageInfo = code.new SKImageInfo(codec.Info.Width, codec.Info.Height);
bitmaps[frame] = new SKBitmap(imageInfo);
// Get the address of the pixels in that bitmap
IntPtr pointer = bitmaps[frame].GetPixels();
// Create an SKCodecOptions value to specify the frame
SKCodecOptions codecOptions = new SKCodecOptions(frame, false);
// Copy pixels from the frame into the bitmap
codec.GetPixels(imageInfo, pointer, codecOptions);
}
// Sum up the total duration
for (int frame = 0; frame < durations.Length; frame++)
{
totalDuration += durations[frame];
}
// Calculate the accumulated durations
for (int frame = 0; frame < durations.Length; frame++)
{
accumulatedDurations[frame] = durations[frame] +
(frame == 0 ? 0 : accumulatedDurations[frame - 1]);
}
}
}
···
}
IntPtr
値にかかわらず、IntPtr
は C# ポインター値に変換されないため、unsafe
コードは必要ありません。
各フレームが抽出されると、コンストラクターはすべてのフレームの期間を合計し、累積された期間で別の配列を初期化します。
分離コード ファイルの残りの部分はアニメーション専用です。 この Device.StartTimer
メソッドはタイマーの実行を開始するために使用され、OnTimerTick
コールバックは Stopwatch
オブジェクトを使用して経過時間をミリ秒単位で判断します。 現在のフレームを見つけるには、累積された期間の配列をループするだけで十分です。
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
Stopwatch stopwatch = new Stopwatch();
bool isAnimating;
int currentFrame;
···
protected override void OnAppearing()
{
base.OnAppearing();
isAnimating = true;
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
stopwatch.Stop();
isAnimating = false;
}
bool OnTimerTick()
{
int msec = (int)(stopwatch.ElapsedMilliseconds % totalDuration);
int frame = 0;
// Find the frame based on the elapsed time
for (frame = 0; frame < accumulatedDurations.Length; frame++)
{
if (msec < accumulatedDurations[frame])
{
break;
}
}
// Save in a field and invalidate the SKCanvasView.
if (currentFrame != frame)
{
currentFrame = frame;
canvasView.InvalidateSurface();
}
return isAnimating;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
// Get the bitmap and center it
SKBitmap bitmap = bitmaps[currentFrame];
canvas.DrawBitmap(bitmap,info.Rect, BitmapStretch.Uniform);
}
}
currentframe
変数が変更されるたびに、SKCanvasView
は無効になり、新しいフレームが表示されます。
もちろん、プログラムを自分で実行してアニメーションを表示する必要があります。