SkiaSharp 入門


導入

これは Xamarin Advent Calendar 2016 の10日目の記事です。
本当はもうちょっとそれぞれサンプルコードを添えたかったのですが、主に GitHub の草を途切れらせない為のアラートアプリ keep.grass の開発上の経験で書いてますので必要に応じて keep.grass のソースコードでも見てください。( 主に /source/AlphaCircleGraph.cs のあたり )

使用例

先にも触れましたが keep.grass では SkiaSharp を使って次のような画面(画面上半分。ナビゲーションバーおよび画面下部は Xamarin.Forms によるもの)を実現しています。 ( このスクリーンショット v2 に向けての改修中のもので、円グラフとアイコンがアニメーションを伴って描画されます。現在ストアで公開されている v1 はアニメーションしません。 )

SkiaSharp とは

SkiaSharp は Skia Graphics Library という Google のオープンソースな2Dグラフィックライブラリ(より正確には元々 Skia Inc. が開発していたモノで、 Google による買収後、修正BSDライセンスでオープンソースとなった。)を C# 向けにラップしているモノで Xamarin が公式に推しているライブラリでもあります。

構成

構成としては以下のようになっています。利用される場合は必要に応じてそれぞれ nuget で取得してください。

  • SkiaSharp
    • 本体
  • SkiaSharp.Views
    • SkiaSharp 本体は画面への描画をサポートしていないので画面への描画を行う際はこちらを利用します。
    • 各種プラットフォーム用のコンポーネントの為、PCL版は存在しませんし、またPCLには必要ありません。
  • SkiaSharp.Views.Forms
    • Xamarin.Forms

なお、 SkiaSharp.Views および SkiaSharp.Views.Forms が無くとも SkiaSharp 本体だけでイメージを作成し、ユーザーコードでそのイメージを画面に貼り付けて使っても構いません。実際、ちょっと前まで SkiaSharp 本体しか存在しなかったので、 keep.grass では最初そのようにしていました。

hallo, SkiaSharp

https://developer.xamarin.com/guides/cross-platform/drawing/introduction/
コードとしてはこのあたりにあるヤツでも参考にしてください。

SkiaSharp 本体だけで使う場合は次のような流れになります。

  • SKSurface.Create() で Surface を作成( 画面に貼り付ける前提の場合、 ColorType は通常 SKColorType.Rgba8888 で良いですが UWP アプリの場合は SKColorType.Bgra8888 します。 )
  • Surface の Canvas メンバーに対して SKPaint などを使い描画
  • 絵が完成したら Surface から .Snapshot().Encode()SKData を取得
  • 後は用途に応じて SKData.AsStream() が返す Stream や ToArray() が返すバイト列を使って Xamarin.FormsImageSource.FromStream() を作成して画面に貼り付けてもヨシ、そのままファイル等に保存するもヨシ

SkiaSharp.Views.Forms の場合は次のような流れになります。

  • SKCanvasView を継承したクラスを作成し OnPaintSurface() 関数のオーバーライドを実装する。
  • そのクラスを画面を構成する View の一つとして組み込む
  • SKCanvasViewOnPaintSurface() 関数が呼び出されたらその引数の SKPaintSurfaceEventArgs のメンバーに Surface が存在してるので、さらにその SurfaceCanvas メンバーに対して描画を行えば、画面に反映されます。
  • 描画内容を更新したい場合は SKCanvasViewInvalidateSurface() 関数を呼び出せば再度 OnPaintSurface() 関数が呼び出されるのでそこで新しい内容で描画を行います。

描画の際の塗る色( .Color )だの線の太さ( .StrokeWidth )だの枠線を引くだけなのか塗りつぶすのか( .IsStroke )だのアンチエイリアスの有効無効( .IsAntialias )だのと言った多くの指定は主に SKPaint のプロパティで指定します。

物理ピクセル

Xamarin.Forms の場合、画面の座標系は論理ピクセルベースとなりますが、 SkiaSharp.Views.Forms を使うと自動的に View の大きさに合わせた物理ピクセルベースのサイズの Canvas が作成されます。この場合、 Canvas の GetClipBounds() 関数を呼び出すことで物理ピクセルの Rect が手に入るのでその枠内で描画します。

日本語文字

テキスト描画は SKPaint のプロパティで TextSize だの TextAlign だのを指定して SKCanvasDrawText() を呼び出せばいいだけなんですが普通にやると日本語文字がお豆腐になります。

ということで no more 豆腐 —— noto フォントさんでも利用しましょう。(別にnotoフォントである必要はありません。)

フォントの埋め込み

フォントは EmbeddedResource として埋め込んでおきます。埋め込む先は別に PCL だろうがプラットフォーム別のプロジェクトだろうが構いません。

埋め込んだリソースが EmbeddedResource になっているかどうかは Xamarin Studio 上のプロパティの ビルド アクション の項目で確認できます。 ビルド アクションEmbeddedResource になっていなければ ソリューション 内から目的のファイルを右クリックしてコンテキストメニューから ビルド アクションEmbeddedResource を選択します。

UI上は上記の通りなのですが、たまに *.csproj 上で正常な形で登録できておらず上手く動作しないことがあります。この場合、 *.csproj ファイルをテキストエディタで開き次ぎのような形(これは keep.grass の keep.grass.csproj の抜粋です )で登録されているか確認し、そうなっていなければ適切な形に整形して保存してください。

*.csproj
  <ItemGroup>
    <EmbeddedResource Include="Images\keep.grass.120.png" />
    <EmbeddedResource Include="Images\wraith13.120.png" />
    <EmbeddedResource Include="Images\GitHub-Mark.120.png" />
    <EmbeddedResource Include="Images\right.120.png" />
    <EmbeddedResource Include="Images\refresh.120.png" />
    <EmbeddedResource Include="Images\export.120.png" />
    <EmbeddedResource Include="Fonts\NotoSansCJKjp-Regular.otf" />
  </ItemGroup>

埋め込みフォントの取得

埋め込んだフォントは typeof(フォントを埋め込んだプロジェクトで定義されてる適当な型).GetTypeInfo().Assembly.GetManifestResourceStream(Xamarin Studioで埋め込んだフォントのプロパティでリソースIDとして表示されている文字列); で得られるストリームを SKManagedStream コンストラクタに渡し、さらにそれを SKTypeface.FromStream() に喰わせれば SkiaSharp で使える形式になるので SKPaintTypeface プロパティにセットしてからテキスト描画を行います。

線画

SKPath クラスのオブジェクトを作成し、一筆書きの要領で MoveTo(), LineTo(), ArcTo() など使い目的の線(パス)を描き、必要に応じて Close() を呼び出してパスを閉じます。パスで囲んだ領域を塗り潰すなら必ず Close() を呼び出してパスを閉じましょう。

ArcTo() だけちょっと引数の指定が分かり難いので次の図を参考にしてください。

赤線の弧を描画したい場合、円を囲む青い四角が oval で、緑の角度が startAngle で、オレンジの角度が sweepAngle となり、指定角度単位は degree になります。

画像

SKData のコンストラクタに画像のバイト列を喰わせたものをさらに SKBitmap.Decode() に与えれば SkiaSharp で利用できる形式になるので後は Canvas の DrawBitmap() で座標を指定して貼り付けるだけです。

ソースとなる画像は最終的に SKBitmap.Decode() で扱えるバイト列やストリームが手に入ればいいののでネットから取得したものでも埋め込みリソースでも構いません。埋め込みリソースを使う場合は上の[フォントの埋め込み]の所を参考にしてください。埋め込みリソースの扱いは中身がフォントだろうが画像だろうが違いはありません。

アンチエイリアシングでの重ね塗りによる滲み

これはまぁ別に SkiaSharp 云々に関係なくアンチエイリアスを有効にしてる状態での描画全般に関わる話ですが、 SkiaSharp.Views.Forms で描画処理の高速化を狙った描画の最小化の為に Canvas の Clear() 関数の呼び出しだのその他の背景色での塗りつぶしだのを行わずにアンチエイリアスを有効にした状態での描画を繰り返すと下の画像のような滲みが発生してしまうので必要に応じて背景を塗り潰すなり描画の緩衝領域を設けるなどしましょう。

左が重ね塗りによる滲みが出てる画像、右が適宜背景を塗りつぶしてる画像

アニメーション

SkiaSharp 自体は直接的にはアニメーションをサポートしません。ですが、自前でタイミングをコントロールしつつ描画したり、 Xamarin.Forms の View が備える Animate() 関数( +都度 SKCanvasViewInvalidateSurface() 関数を呼び出す )などを使えばできないことはないです。実際にやってみたところかなり環境依存&アニメーションの内容に依りそうですが、それなりには動作してくれそうです。本格的にアニメーションしたいなら CocosSharp などを使ったほうがよいかと。