Android でベジェ曲線を描画する

Android Bezier Curve

Android 開発では Canvas を使用した描画を行うことがありますが、滑らかな曲線を描きたい時にはベジェ曲線のロジックを使用出来ます。今回は Android でベジェ曲線を描く簡単な例を紹介します。

ベジェ曲線

このページに辿り着いた方は既にベジェ曲線についてはご存知だと思うのでここでは省略します。わからないという方は、わかりやすい説明がインターネット上にたくさんあるので、そちらを参照ください。

ベジェ曲線描画アプリ

さて、今回は制御点がどのように曲線に影響を与えるのかを確認出来る様に、SeekBar を使ったダイナミック描画アプリを作成したいと思います。

Bezier Curve Demo

アプリ実装

それぞれのコンポーネントを実装する方法を紹介していきます。

構成要素

  • MainActivity: BezierSampleFragment をホストする Activity
  • BezierSampleFragment: BezierView をホストする Fragment
  • BezierViewModel: SeekBarprogress を保存し、BezierView にベジェ制御点を通知する ViewModel
  • BezierView: ベジェ曲線を描画するカスタム View

MainActivityBezierSampleFragment を載せるだけのものなので、説明は省略します。

BezierView で静的にベジェ曲線を描く

まずは静的にベジェを描くカスタム View を作成します。これは View を継承したクラスで、onDraw 内で Canvas を使ってベジェ曲線を描画していきます。

今回は制御点を 4 つ用意し、座標は View のサイズに対する割合で表現します。p1 を開始点とし、p2p3 を制御点、そして p4 を終了点とするメンバ変数を Point で追加します。開始と終了点は x 、y は 15%ずつ左右辺と下辺から離した座標で固定します。ちなみに Android では x は左辺が 0、y は上辺が 0 になります。

曲線描画には CanvasdrawPath という API を使用するので、ベジェの軌道を保持する Path のインスタンスをメンバ変数に追加しています。描画には、onDraw から呼ぶことができる drawBezier という関数を作成します。カスタム View では onDraw 内で様々な処理を入れているものを見かける事がありますが、onDraw は大量に呼ばれる可能性があるため、インスタンスの生成や重い処理は避ける実装が理想です。PointPath のインスタンスをメンバ変数にしているのはそのためです。

描画に必要になるベジェ曲線用の Paint もメンバ変数に追加しておきます。

drawBezier では、まず Pathreset を呼んでインスタンスが保持している軌道をクリアします。開始点の p1 の座標を moveTo で指定し、cubicTo に残り p2 - p4 までの座標を渡してベジェの軌道を描きます。この関数は厳密に参照透過ではないですが、widthheightPaint は頻繁に変更される事はないので副作用が少ない物としてメンバ変数を参照しています。ちなみにこの widthheightBezierView 自身のもので、onDraw の時点では既に計算されているので 0 ではない値が入ります。これらに x と y の相対的な割合を掛けて座標を渡しています。

これでベジェ曲線が描かれますが、今回は制御点とそれぞれを繋いだガイドラインも描画したいと思います。

まずは drawPoints 関数を作成して制御点を色分けして描画します。drawCircle で点を塗りつぶして描くので FILLPaint を用意します。こちらも onDraw で呼ぶため先にインスタンスを保持しておきます。

同様に、グレーの点線で制御点を繋ぐガイドラインを描画する drawGuideLines 関数を作成します。点線は DashPathEffectPaintpathEffect に設定することで描くことが出来ます。

これらを onDraw から呼ぶことで、静的にベジェを描くカスタム View クラスが完成します。

BezierView.kt - Static Version

BezierView を BezierSampleFragment に実装する

ここで一度 Fragment に実装してベジェ曲線が描画されるか確認したいと思います。

fragment_bezier.xml

BezierSampleFragment.kt

結果

Bezier Curve Fixed

無事に静的なベジェ曲線が描画されました。

これから SeekBarViewModel を使って p2 と p3 の制御点を移動し、ダイナミックに曲線を描画する実装を追加して行きます。

レイアウトに SeekBar を追加する

P2 と P3 の x 座標と y 座標をそれぞれ変更できる様、ConstraintLayout 上に SeekBar を 4 つ実装します。直感的な UX のために P2 のコントロールはレッド、P3 はグリーンで統一しておきます。

fragment_bezier.xml - with SeekBars

SeekBar の値を ViewModel に保存する

これらの SeekBarprogressViewModel に保存しておくため、BezierViewModel を作成します。シンプルな getter と setter の実装で、それぞれの初期値を MutableLiveData のコンストラクタに渡します。

BezierViewModel.kt

この ViewModelSeekBarBezierSampleFragment 上で繋げます。まずは VM を activityViewModels で取得しておき、onViewCreated 内でそれぞれの SeekBar で VM の progressobserve し、progress のリスナーから VM に変更を通知する実装をします。

これで SeekBar の値を VM に保存し、同期することが出来ました。次はこの値を BezierView の制御点座標に反映する実装をします。

先述の通り、BezierViewp1 - p4Point は 0 - 1f の Float でパーセントの値を持っています。しかし SeekBarprogress は 0 - 100% の Int でパーセントを表現しています。そこで、LiveData を変換することのできる Transformations を使用します。Transformations.map のラムダの引数に null が入ることはこの実装ではありませんが、 MutableLiveData.postValue には null を渡すことが出来るので念の為 null safe の実装をしています。

Kotlin なので、Int のパーセントを 1/100 の Float に変換する Extension も実装しておきます。

BezierViewModel.kt - With Transformations

これらの値を BezierViewinitobserve し、変更が通知されたら invalidate で自分自身を描き変える実装をします。ViewModelObserver のスコープには Fragment と同様に Activity を使用します。

これにより、SeekBarprogress が更新される度に ObserverFloat に変換された値が通知され、Point の座標が更新された状態でベジェ曲線が描き直されます。

結果

Bezier Curve Dynamic

これでベジェ曲線がダイナミックに描画される様に実装できました。

BezierView.kt - Dynamic Version

TL;DR

ベジェ曲線を Android で描くのは、PathcubicTo を使って出来ます。

しかし今回は ViewModel 、LiveData 、Transformation を使ってダイナミックに描画を変える実装を紹介したので、アーキテクチャを含めて実際の開発の参考になればと思います。

参考

COPYRIGHT © 2023 Kohei Ando