SkiaSharp の基本アニメーション

Download Sampleサンプルのダウンロード

SkiaSharp グラフィックをアニメーション化する方法を確認する

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

Several concentric circles seemingly expanding from the center

SkiaSharpFormsDemos プログラムの [パルス楕円] ページでは、楕円の 2 つの軸がアニメーション化され、脈動しているように見え、この脈動の速度を制御することもできます。 PulsatingEllipsePage.xaml ファイルは、Xamarin.FormsSliderLabel をインスタンス化して、スライダーの現在の値を表示します。 これは、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 を開始し、pageIsActivetrue の間ループします。 これは基本的に、ページがアクティブな間は "無限ループ" ですが、ループは 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 に戻るため、メソッドはそれを使用して、minRadiusmaxRadius の間の範囲の xRadiusyRadius を計算します。 これらの値は、楕円を描画して塗りつぶすために使用されます。

Triple screenshot of the Pulsating Ellipse page

SKPaint オブジェクトが using ブロック内に作成されることに注意してください。 多くの SkiaSharp クラスと同様に、SKPaintSKObject から派生し、SKObjectIDisposable インターフェイスを実装する SKNativeObject から派生します。 SKPaintDispose メソッドをオーバーライドして、アンマネージ リソースを解放します。

SKPaintusing ブロックに配置すると、ブロックの最後で 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 の場合とで画像は同じように見え、円は永遠に拡大し続けるように見えます。

Triple screenshot of the Expanding Circles page