SkiaSharp グラフィックをアニメーション化する方法を確認する
PaintSurface メソッドを定期的に呼び出すことで、Xamarin.Forms で SkiaSharp グラフィックスをアニメーション化できます。そのたびに、グラフィックスの描画方法が少しずつ異なります。 この記事の後半で示される、中心から広がるように見える同心円を含むアニメーションを次に示します。

サンプル プログラムの [脈動する楕円] ページでは、楕円の 2 つの軸がアニメーション化され、脈動しているように見え、この脈動の速度を制御することもできます。 PulsatingEllipsePage.xaml ファイルは、Xamarin.FormsSlider と Label をインスタンス化して、スライダーの現在の値を表示します。 これは、SKCanvasView を他の Xamarin.Forms ビューと統合する一般的な方法です。
<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.PulsatingEllipsePage"
Title="Pulsating Ellipse">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Slider x:Name="slider"
Grid.Row="0"
Maximum="10"
Minimum="0.1"
Value="5"
Margin="20, 0" />
<Label Grid.Row="1"
Text="{Binding Source={x:Reference slider},
Path=Value,
StringFormat='Cycle time = {0:F1} seconds'}"
HorizontalTextAlignment="Center" />
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="2"
PaintSurface="OnCanvasViewPaintSurface" />
</Grid>
</ContentPage>
分離コード ファイルは、高精度クロックとして機能する Stopwatch オブジェクトをインスタンス化します。 OnAppearing オーバーライドは、pageIsActive フィールドを true に設定し、AnimationLoop という名前のメソッドを呼び出します。 OnDisappearing オーバーライドは、pageIsActive フィールドを false に設定します。
Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float scale; // ranges from 0 to 1 to 0
public PulsatingEllipsePage()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
AnimationLoop();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
AnimationLoop メソッドは Stopwatch を開始し、pageIsActive が true の間ループします。 これは基本的に、ページがアクティブな間は "無限ループ" ですが、ループは await 演算子を使用した Task.Delay の呼び出しで終了するため、プログラムがハングすることはありません。これにより、プログラムの他の部分が機能できるようになります。 Task.Delay への引数により、1/30 秒後に完了します。 これにより、アニメーションのフレーム レートが定義されます。
async Task AnimationLoop()
{
stopwatch.Start();
while (pageIsActive)
{
double cycleTime = slider.Value;
double t = stopwatch.Elapsed.TotalSeconds % cycleTime / cycleTime;
scale = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2;
canvasView.InvalidateSurface();
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
}
stopwatch.Stop();
}
while ループは、Slider からサイクル時間を取得することで始まります。 これは、5 など、秒単位の時間です。 2 番目のステートメントは、時間の t の値を計算します。 cycleTime が 5 の場合、t は 5 秒ごとに 0 から 1 に増加します。 2 番目のステートメントの Math.Sin 関数の引数の範囲は、5 秒ごとに 0 から 2π です。 Math.Sin 関数は、5 秒ごとに 0 から 1 までの範囲の値を 0 に戻し、その後 -1 と 0 を返しますが、値が 1 または -1 に近づくと値の変化が遅くなります。 値 1 が追加され、値が常に正の値になり、2 で除算されるため、値の範囲は 1/2 から 1/2 から 0 から 1/2 ですが、値が 1 と 0 の前後の場合は遅くなります。 これは scale フィールドに格納され、SKCanvasView は無効になります。
PaintSurface メソッドは、この scale 値を使用して楕円の 2 つの軸を計算します。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float maxRadius = 0.75f * Math.Min(info.Width, info.Height) / 2;
float minRadius = 0.25f * maxRadius;
float xRadius = minRadius * scale + maxRadius * (1 - scale);
float yRadius = maxRadius * scale + minRadius * (1 - scale);
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Blue;
paint.StrokeWidth = 50;
canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
paint.Style = SKPaintStyle.Fill;
paint.Color = SKColors.SkyBlue;
canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
}
}
このメソッドは、表示領域のサイズに基づいて最大半径を計算し、最大半径に基づいて最小半径を計算します。 scale 値は 0 と 1 の間でアニメーション化され、0 に戻るため、メソッドはそれを使用して、minRadius と maxRadius の間の範囲の xRadius と yRadius を計算します。 これらの値は、楕円を描画して塗りつぶすために使用されます。
SKPaint オブジェクトが using ブロック内に作成されることに注意してください。 多くの SkiaSharp クラスと同様に、SKPaint は SKObject から派生し、SKObject は IDisposable インターフェイスを実装する SKNativeObject から派生します。 SKPaint は Dispose メソッドをオーバーライドして、アンマネージ リソースを解放します。
SKPaint を using ブロックに配置すると、ブロックの最後で Dispose が呼び出され、これらのアンマネージ リソースが解放されます。 これは、SKPaint オブジェクトによって使用されているメモリが .NET ガベージ コレクターによって解放されるときに発生しますが、アニメーション コードでは、より秩序ある方法で積極的にメモリを解放することが最善です。
この特定の場合のより良い解決策は、2 つの SKPaint オブジェクトを一度作成し、それらをフィールドとして保存することです。
これは、拡大円アニメーションの動作です。 ExpandingCirclesPage クラスは、SKPaint オブジェクトを含むいくつかのフィールドを定義することから始まります。
public class ExpandingCirclesPage : ContentPage
{
const double cycleTime = 1000; // in milliseconds
SKCanvasView canvasView;
Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float t;
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke
};
public ExpandingCirclesPage()
{
Title = "Expanding Circles";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
...
}
このプログラムは、Xamarin.FormsDevice.StartTimer メソッドに基づいたアニメーションに対して別のアプローチを使用します。 t フィールドは、cycleTime ミリ秒ごとに 0 から 1 までアニメーション化されます。
public class ExpandingCirclesPage : ContentPage
{
...
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
{
t = (float)(stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime);
canvasView.InvalidateSurface();
if (!pageIsActive)
{
stopwatch.Stop();
}
return pageIsActive;
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
...
}
PaintSurface ハンドラーは、アニメーション化された半径を持つ 5 つの同心円を描画します。 baseRadius 変数が 100 として計算された場合、t が 0 から 1 までアニメーション化されると、5 つの円の半径は 0 から 100、100 から 200、200 から 300、300 から 400、および 400 から 500 と増加します。 ほとんどの円では、strokeWidth は 50 ですが、最初の円では、strokeWidth は 0 から 50 までアニメーション化されます。 ほとんどの円の色は青ですが、最後の円の場合、色は青から透明にアニメーション化されます。 不透明度を指定する SKColor コンストラクターの 4 番目の引数に注目してください。
public class ExpandingCirclesPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
float baseRadius = Math.Min(info.Width, info.Height) / 12;
for (int circle = 0; circle < 5; circle++)
{
float radius = baseRadius * (circle + t);
paint.StrokeWidth = baseRadius / 2 * (circle == 0 ? t : 1);
paint.Color = new SKColor(0, 0, 255,
(byte)(255 * (circle == 4 ? (1 - t) : 1)));
canvas.DrawCircle(center.X, center.Y, radius, paint);
}
}
}
その結果、t が 0 の場合と t が 1 の場合とで画像は同じように見え、円は永遠に拡大し続けるように見えます。
![[脈動する楕円] ページのトリプル スクリーンショット](animation-images/pulsatingellipse-small.png)
![[広がるう円] ページのトリプル スクリーンショット](animation-images/expandingcircles-small.png)