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

Android 開発では Canvas
を使用した描画を行うことがありますが、滑らかな曲線を描きたい時にはベジェ曲線のロジックを使用出来ます。今回は Android でベジェ曲線を描く簡単な例を紹介します。
ベジェ曲線
このページに辿り着いた方は既にベジェ曲線についてはご存知だと思うのでここでは省略します。わからないという方は、わかりやすい説明がインターネット上にたくさんあるので、そちらを参照ください。
ベジェ曲線描画アプリ
さて、今回は制御点がどのように曲線に影響を与えるのかを確認出来る様に、SeekBar
を使ったダイナミック描画アプリを作成したいと思います。

アプリ実装
それぞれのコンポーネントを実装する方法を紹介していきます。
構成要素
MainActivity
:BezierSampleFragment
をホストするActivity
BezierSampleFragment
:BezierView
をホストするFragment
BezierViewModel
:SeekBar
のprogress
を保存し、BezierView
にベジェ制御点を通知するViewModel
BezierView
: ベジェ曲線を描画するカスタムView
MainActivity
は BezierSampleFragment
を載せるだけのものなので、説明は省略します。
BezierView で静的にベジェ曲線を描く
ま ずは静的にベジェを描くカスタム View
を作成します。これは View
を継承したクラスで、onDraw
内で Canvas
を使ってベジェ曲線を描画していきます。
今回は制御点を 4 つ用意し、座標は View
のサイズに対する割合で表現します。p1
を開始点とし、p2
と p3
を制御点、そして p4
を終了点とするメンバ変数を Point
で追加します。開始と終了点は x 、y は 15%ずつ左右辺と下辺から離した座標で固定します。ちなみに Android では x は左辺が 0、y は上辺が 0 になります。
曲線描画には Canvas
の drawPath
という API を使用するので、ベジェの軌道を保持する Path
のインスタンスをメンバ変数に追加しています。描画には、onDraw
から呼ぶことができる drawBezier
という関数を作成します。カスタム View
では onDraw
内で様々な処理を入れているものを見かける事がありますが、onDraw
は大量に呼ばれる可能性があるため、インスタンスの生成や重い処理は避ける実装が理想です。Point
と Path
のインスタンスをメンバ変数にしているのはそのためです。
描画に必要になるベジェ曲線用の Paint もメンバ変数に追加しておきます。
drawBezier
では、まず Path
の reset
を呼んでインスタンスが保持している軌道をクリアします。開始点の p1
の座標を moveTo
で指定し、cubicTo
に残り p2
- p4
までの座標を渡してベジェの軌道を描きます。この関数は厳密に参照透過ではないですが、width
と height
、Paint
は頻繁に変更される事はないので副作用が少ない物としてメンバ変数を参照しています。ちなみにこの width
と height
は BezierView
自身のもので、onDraw
の時点では既に計算されているので 0 ではない値が入ります。これらに x と y の相対的な割合を掛けて座標を渡しています。
これでベジェ曲線が描かれますが、今回は制御点とそれぞれを繋いだガイドラインも描画したいと思います。
まずは drawPoints
関数を作成して制御点を色分けして描画します。drawCircle
で点を塗りつぶして描くので FILL
の Paint
を用意します。こちらも onDraw
で呼ぶため先にインスタンスを保持しておきます。
同様に、グレーの点線で制御点を繋ぐガイドラインを描画する drawGuideLines
関数を作成します。点線は DashPathEffect
を Paint
の pathEffect
に設定することで描くことが出来ます。
これらを onDraw
から呼ぶことで、静的にベジェ を描くカスタム View
クラスが完成します。
BezierView.kt - Static Version
BezierView を BezierSampleFragment に実装する
ここで一度 Fragment
に実装してベジェ曲線が描画されるか確認したいと思います。
fragment_bezier.xml
BezierSampleFragment.kt
結果

無事に静的なベジェ曲線が描画されました。
これから SeekBar
と ViewModel
を使って p2 と p3 の制御点を移動し、ダイナミックに曲線を描画する実装を追加して行きます。
レイアウトに SeekBar を追加する
P2 と P3 の x 座標と y 座標をそれぞれ変更できる様、ConstraintLayout
上に SeekBar
を 4 つ実装します。直感的な UX のために P2 のコントロールはレッド、P3 はグリーンで統一しておきます。
fragment_bezier.xml - with SeekBars
SeekBar の値を ViewModel に保存する
これらの SeekBar
の progress
を ViewModel
に保存しておくため、BezierViewModel
を作成します。シンプルな getter と setter の実装で、それぞれの初期値を MutableLiveData
のコンストラクタに渡します。
BezierViewModel.kt
この ViewModel
と SeekBar
を BezierSampleFragment
上で繋げます。まずは VM を activityViewModels
で取得しておき、onViewCreated
内でそれぞれの SeekBar
で VM の progress
を observe
し、progress
のリスナーから VM に変更を通知する実装をします。
これで SeekBar の値を VM に保存し、同期することが出来ました。次 はこの値を BezierView の制御点座標に反映する実装をします。
先述の通り、BezierView
の p1
- p4
の Point
は 0 - 1f の Float
でパーセントの値を持っています。しかし SeekBar
の progress
は 0 - 100% の Int
でパーセントを表現しています。そこで、LiveData
を変換することのできる Transformations
を使用します。Transformations.map
のラムダの引数に null
が入ることはこの実装ではありませんが、 MutableLiveData.postValue
には null
を渡すことが出来るので念の為 null safe の実装をしています。
Kotlin なので、Int
のパーセントを 1/100 の Float
に変換する Extension
も実装しておきます。
BezierViewModel.kt - With Transformations
これらの値を BezierView
の init
で observe
し、変更が通知されたら invalidate
で自分自身を描き変える実装をします。ViewModel
と Observer
のスコープには Fragment
と同様に Activity
を使用します。
これにより、SeekBar
の progress
が更新される度に Observer
に Float
に変換された値が通知され、Point
の座標が更新された状態でベジェ曲線が描き直されます。
結果

これでベジェ曲線がダイナミックに描画される様に実装できました。
BezierView.kt - Dynamic Version
TL;DR
ベジェ曲線を Android で描くのは、Path
の cubicTo
を使って出来ます。
しかし今回は ViewModel 、LiveData 、Transformation を使ってダイナミックに描画を変える実装を紹介したので、アーキテクチャを含めて実際の開発の参考になればと思います。