SkiaSharp の基本アニメーション
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 の場合とで画像は同じように見え、円は永遠に拡大し続けるように見えます。