この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。
はじめに
Android では、ビューにカスタムの 2D グラフィックとアニメーションを実装するための手法がいくつか用意されています。
ドローアブルを使用するだけでなく、Canvas クラスの描画メソッドを使用して 2D 描画を作成することもできます。Canvas は、描画用のメソッドを提供する 2D 描画サーフェスです。これは、ユーザーが目にするものが時間とともに変化するため、アプリが定期的に再描画する必要がある場合に便利です。この Codelab では、View に表示されるキャンバスを作成して描画する方法を学びます。
キャンバスで実行できる操作の種類は次のとおりです。
- キャンバス全体を色で塗りつぶします。
Paintオブジェクトで定義されたスタイルで、長方形、円弧、パスなどのシェイプを描画します。Paintオブジェクトには、ジオメトリ(線、長方形、楕円、パスなど)の描画方法に関するスタイルと色の情報や、テキストの書体などの情報が保持されます。- 変換(変換、スケーリング、カスタム変換など)を適用します。
- クリップ。つまり、キャンバスにシェイプまたはパスを適用して、表示する部分を定義します。

Android の描画の考え方(超簡略化)
Android やその他の最新のシステムでの描画は、抽象化のレイヤやハードウェアまでの最適化を含む複雑なプロセスです。Android の描画は、多くの文献で取り上げられている興味深いトピックですが、その詳細はこの Codelab の範囲外です。
この Codelab のコンテキストでは、全画面ビューで表示するためにキャンバスに描画するアプリについて、次のように考えることができます。

- 描画内容を表示するためのビューが必要です。これは、Android システムが提供するビューの 1 つである可能性があります。または、この Codelab では、アプリのコンテンツ ビュー(
MyCanvasView)として機能するカスタムビューを作成します。 - このビューには、他のすべてのビューと同様に、独自のキャンバス(
canvas)が付属しています。 - ビューのキャンバスに描画する最も基本的な方法は、
onDraw()メソッドをオーバーライドして、そのキャンバスに描画することです。 - 描画をビルドするときは、以前に描画したものをキャッシュに保存する必要があります。データをキャッシュに保存する方法はいくつかあります。1 つはビットマップ(
extraBitmap)に保存する方法です。もう 1 つは、描画した内容の履歴を座標と指示として保存する方法です。 - キャンバス描画 API を使用してキャッシュ保存ビットマップ(
extraBitmap)に描画するには、キャッシュ保存ビットマップ用のキャッシュ保存キャンバス(extraCanvas)を作成します。 - 次に、キャッシュ キャンバス(
extraCanvas)に描画します。この描画はキャッシュ ビットマップ(extraBitmap)に描画されます。 - 画面に描画されたものをすべて表示するには、ビューのキャンバス(
canvas)にキャッシュ保存されたビットマップ(extraBitmap)を描画するよう指示します。
前提となる知識
- Activity と基本的なレイアウトを含むアプリを作成し、Android Studio を使用して実行する方法。
- イベント ハンドラをビューに関連付ける方法。
- カスタムビューの作成方法。
学習内容
Canvasを作成し、ユーザーのタッチに応じて描画する方法。
演習内容
- ユーザーが画面に触れると、画面に線を描画するアプリを作成します。
- モーション イベントをキャプチャし、その応答として、画面の全画面カスタムビューに表示されるキャンバスに線を描画します。
MiniPaint アプリでは、次のスクリーンショットに示すように、ユーザーのタッチに応じて線を表示するためにカスタムビューを使用しています。

ステップ 1. MiniPaint プロジェクトを作成する
- Empty Activity テンプレートを使用して、MiniPaint という新しい Kotlin プロジェクトを作成します。
app/res/values/colors.xmlファイルを開き、次の 2 つの色を追加します。
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>styles.xmlを開く- 指定された
AppThemeスタイルの親で、DarkActionBarをNoActionBarに置き換えます。これによりアクションバーが削除され、全画面表示で描画できるようになります。
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">ステップ 2. MyCanvasView クラスを作成する
このステップでは、描画用のカスタムビュー MyCanvasView を作成します。
app/java/com.example.android.minipaintパッケージで、MyCanvasViewという [New] > [Kotlin File/Class] を作成します。MyCanvasViewクラスがViewクラスを拡張し、context: Contextを渡すようにします。提案されたインポートを承認します。
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}ステップ 3. MyCanvasView をコンテンツ ビューとして設定する
MyCanvasView で描画する内容を表示するには、MainActivity のコンテンツ ビューとして設定する必要があります。
strings.xmlを開き、ビューのコンテンツの説明に使用する文字列を定義します。
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>MainActivity.ktを開くonCreate()で、setContentView(R.layout.activity_main)を削除します。MyCanvasViewのインスタンスを作成します。
val myCanvasView = MyCanvasView(this)- その下に、
myCanvasViewのレイアウトの全画面表示をリクエストします。そのためには、myCanvasViewでSYSTEM_UI_FLAG_FULLSCREENフラグを設定します。このようにして、ビューが画面全体を埋めます。
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- コンテンツの説明を追加します。
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- その下に、コンテンツ ビューを
myCanvasViewに設定します。
setContentView(myCanvasView)- アプリを実行します。キャンバスのサイズが設定されておらず、まだ何も描画していないため、完全に白い画面が表示されます。
ステップ 1. onSizeChanged() をオーバーライドする
ビューのサイズが変更されるたびに、Android システムによって onSizeChanged() メソッドが呼び出されます。ビューはサイズなしで開始されるため、アクティビティが最初にビューを作成して拡張した後にも、ビューの onSizeChanged() メソッドが呼び出されます。したがって、この onSizeChanged() メソッドは、ビューのキャンバスを作成して設定するのに最適な場所です。
MyCanvasViewで、クラスレベルでキャンバスとビットマップの変数を定義します。これらをextraCanvasとextraBitmapと呼びます。これらは、以前に描画されたものをキャッシュに保存するためのビットマップとキャンバスです。
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- キャンバスの背景色を表すクラスレベルの変数
backgroundColorを定義し、前に定義したcolorBackgroundに初期化します。
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)MyCanvasViewで、onSizeChanged()メソッドをオーバーライドします。このコールバック メソッドは、変更された画面の寸法(新しい幅と高さ(変更先)と古い幅と高さ(変更元))とともに Android システムによって呼び出されます。
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}onSizeChanged()内で、画面サイズである新しい幅と高さでBitmapのインスタンスを作成し、extraBitmapに割り当てます。3 番目の引数はビットマップの色構成です。ARGB_8888は各色を 4 バイトで保存するため、推奨されます。
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)extraBitmapからCanvasインスタンスを作成し、extraCanvasに割り当てます。
extraCanvas = Canvas(extraBitmap)extraCanvasを塗りつぶす背景色を指定します。
extraCanvas.drawColor(backgroundColor)onSizeChanged()を見ると、関数が実行されるたびに新しいビットマップとキャンバスが作成されています。サイズが変更されたため、新しいビットマップが必要です。ただし、これはメモリリークであり、古いビットマップが残ります。これを修正するには、superの呼び出しの直後に次のコードを追加して、次のextraBitmapを作成する前にextraBitmapをリサイクルします。
if (::extraBitmap.isInitialized) extraBitmap.recycle()ステップ 2. onDraw() をオーバーライドする
MyCanvasView の描画作業はすべて onDraw() で行われます。
まず、onSizeChanged() で設定した背景色で画面を塗りつぶして、キャンバスを表示します。
onDraw()をオーバーライドし、キャッシュに保存されたextraBitmapの内容をビューに関連付けられたキャンバスに描画します。drawBitmap()Canvasメソッドにはいくつかのバージョンがあります。このコードでは、ビットマップ、左上隅の x 座標と y 座標(ピクセル単位)、Paintのnullを指定します。nullは後で設定します。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}onDraw() に渡され、システムがビットマップを表示するために使用するキャンバスは、onSizeChanged() メソッドで作成し、ビットマップに描画するために使用したキャンバスとは異なります。
- アプリを実行します。画面全体が指定した背景色で塗りつぶされているはずです。

描画するには、描画時のスタイルの指定方法を記述する Paint オブジェクトと、描画対象を指定する Path が必要です。
ステップ 1. Paint オブジェクトを初期化する
- MyCanvasView.kt のファイルレベルの先頭で、ストロークの幅の定数を定義します。
private const val STROKE_WIDTH = 12f // has to be floatMyCanvasViewのクラスレベルで、描画に使用する色を保持する変数drawColorを定義し、以前に定義したcolorPaintリソースで初期化します。
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- クラスレベルで、以下のように
Paintオブジェクトの変数paintを追加して初期化します。
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}paintのcolorは、先ほど定義したdrawColorです。isAntiAliasは、エッジ スムージングを適用するかどうかを定義します。isAntiAliasをtrueに設定すると、形状に影響を与えることなく、描画されたもののエッジが滑らかになります。isDither(trueの場合)は、デバイスよりも高精度の色をダウンサンプリングする方法に影響します。たとえば、ディザリングは、画像の色の範囲を 256 色以下に減らす最も一般的な方法です。styleは、ストローク(基本的には線)に対して行うペイントのタイプを設定します。Paint.Styleは、描画されるプリミティブが塗りつぶされるか、ストロークされるか、またはその両方(同じ色で)であるかを指定します。デフォルトでは、ペイントが適用されるオブジェクトが塗りつぶされます。(「塗りつぶし」はシェイプの内側を塗り、「ストローク」はアウトラインに沿って描画します)。Paint.JoinのstrokeJoinは、ストロークされたパスで線と曲線セグメントが結合する方法を指定します。デフォルトはMITERです。strokeCapは、線の端の形状をキャップに設定します。Paint.Capは、ストロークされた線とパスの始点と終点の形状を指定します。デフォルトはBUTTです。strokeWidthには、ストロークの幅をピクセル単位で指定します。デフォルトはヘアライン幅で非常に細いため、先ほど定義したSTROKE_WIDTH定数に設定します。
ステップ 2. Path オブジェクトを初期化する
Path は、ユーザーが描画しているパスです。
MyCanvasViewで、変数pathを追加し、Pathオブジェクトで初期化して、ユーザーが画面に触れたときに描画されるパスを保存します。Pathのandroid.graphics.Pathをインポートします。
private var path = Path()ステップ 1. ディスプレイ上の動きに反応する
ユーザーがディスプレイにタッチするたびに、ビューの onTouchEvent() メソッドが呼び出されます。
MyCanvasViewで、onTouchEvent()メソッドをオーバーライドして、渡されたeventのx座標とy座標をキャッシュに保存します。次に、when式を使用して、画面に触れる、画面上で移動する、画面から指を離すモーション イベントを処理します。これらは、画面に線を描画する対象のイベントです。次のコードに示すように、イベント タイプごとにユーティリティ メソッドを呼び出します。タッチイベントの完全なリストについては、MotionEventクラスのドキュメントをご覧ください。
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}- クラスレベルで、現在のタッチイベント(
MotionEvent座標)の x 座標と y 座標をキャッシュに保存するためのmotionTouchEventX変数とmotionTouchEventY変数を追加します。0fに初期化します。
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- 3 つの関数
touchStart()、touchMove()、touchUp()のスタブを作成します。
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- コードはビルドして実行できますが、まだ色付きの背景と何も変わりません。
ステップ 2. touchStart() を実装する
このメソッドは、ユーザーが最初に画面にタッチしたときに呼び出されます。
- クラスレベルで、最新の x 値と y 値をキャッシュに保存する変数を追加します。ユーザーが移動を停止してタッチを離すと、これらは次のパス(描画する線の次のセグメント)の開始点になります。
private var currentX = 0f
private var currentY = 0ftouchStart()メソッドを次のように実装します。pathをリセットし、タップイベントの x-y 座標(motionTouchEventXとmotionTouchEventY)に移動して、その値にcurrentXとcurrentYを割り当てます。
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}ステップ 3. touchMove() を実装する
- クラスレベルで、
touchTolerance変数を追加し、ViewConfiguration.get(context).scaledTouchSlopに設定します。
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopパスを使用すると、すべてのピクセルを描画したり、ディスプレイの更新をリクエストしたりする必要がなくなります。代わりに、ポイント間のパスを補間すると、パフォーマンスが大幅に向上します。
- 指がほとんど動いていない場合は、描画する必要はありません。
- 指の移動距離が
touchToleranceより小さい場合は、描画しません。 scaledTouchSlopは、ユーザーがスクロールしているとシステムが判断するまでにタップが動ける距離をピクセル単位で返します。
touchMove()メソッドを定義します。移動距離(dx、dy)を計算し、2 点間の曲線を作成してpathに保存し、実行中のcurrentXとcurrentYの合計を更新して、pathを描画します。次に、invalidate()を呼び出して、更新されたpathで画面を強制的に再描画します。
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to cache it.
extraCanvas.drawPath(path, paint)
}
invalidate()
}この方法の詳細:
- 移動した距離(
dx, dy)を計算します。 - 移動量がタッチ許容値を超えた場合は、パスにセグメントを追加します。
- 次のセグメントの開始点をこのセグメントのエンドポイントに設定します。
lineTo()の代わりにquadTo()を使用すると、角のない滑らかな線が描画されます。ベジエ曲線をご覧ください。invalidate()を呼び出して(最終的にonDraw()を呼び出して)ビューを再描画します。
ステップ 4: touchUp() を実装する
ユーザーがタッチを離したら、パスをリセットして再度描画されないようにするだけで済みます。何も描画されないため、無効化は不要です。
touchUp()メソッドを実装します。
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- コードを実行し、指で画面に描画します。デバイスを回転させると、描画状態が保存されないため、画面がクリアされます。このサンプルアプリでは、ユーザーが画面を簡単にクリアできるように、このようになっています。

ステップ 5: スケッチの周りにフレームを描く
ユーザーが画面に描画すると、アプリはパスを構築し、ビットマップ extraBitmap に保存します。onDraw() メソッドは、ビューのキャンバスに余分なビットマップを表示します。onDraw() でさらに描画できます。たとえば、ビットマップを描画した後に図形を描画できます。
このステップでは、写真の端にフレームを描画します。
MyCanvasViewで、Rectオブジェクトを保持するframeという変数を追加します。
private lateinit var frame: RectonSizeChanged()の最後にインセットを定義し、新しいディメンションとインセットを使用してフレームに使用するRectを作成するコードを追加します。
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)onDraw()で、ビットマップを描画した後、長方形を描画します。
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- アプリを実行します。フレームが表示されます。

タスク(省略可): Path にデータを保存する
現在のアプリでは、描画情報はビットマップに保存されます。これは優れた解決策ですが、描画情報を保存する方法はこれだけではありません。描画履歴の保存方法は、アプリやさまざまな要件によって異なります。たとえば、図形を描画している場合、図形のリストとその位置と寸法を保存できます。MiniPaint アプリの場合、パスを Path として保存できます。お試しになる場合は、以下の概要をご覧ください。
MyCanvasViewで、extraCanvasとextraBitmapのコードをすべて削除します。- これまでのパスと現在描画中のパスの変数を追加します。
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()onDraw()で、ビットマップを描画する代わりに、保存されているパスと現在のパスを描画します。
// Draw the drawing so far
canvas.drawPath(drawing, paint)
// Draw any current squiggle
canvas.drawPath(curPath, paint)
// Draw a frame around the canvas
canvas.drawRect(frame, paint)touchUp()で、現在のパスを前のパスに追加し、現在のパスをリセットします。
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- アプリを実行します。違いはまったくありません。
完成した Codelab のコードをダウンロードします。
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
Canvasは、描画用のメソッドを提供する 2D 描画サーフェスです。Canvasは、それを表示するViewインスタンスに関連付けることができます。Paintオブジェクトには、ジオメトリ(線、長方形、楕円、パスなど)とテキストの描画方法に関するスタイルと色の情報が保持されます。- キャンバスを操作する一般的なパターンは、カスタムビューを作成し、
onDraw()メソッドとonSizeChanged()メソッドをオーバーライドすることです。 onTouchEvent()メソッドをオーバーライドして、ユーザーのタップをキャプチャし、描画によって応答します。- 追加のビットマップを使用して、経時的に変化する描画の情報をキャッシュに保存できます。または、シェイプやパスを保存することもできます。
Udacity コース:
Android デベロッパー ドキュメント:
CanvasクラスBitmapクラスViewクラスPaintクラスBitmap.config構成Pathクラス- ベジェ曲線(Wikipedia)
- Canvas と Drawables
- グラフィック アーキテクチャ シリーズの記事(上級者向け)
- ドローアブル
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。
- 必要に応じて宿題を与える
- 宿題の提出方法を生徒に伝える
- 宿題を採点する
インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。
この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。
以下の質問に回答してください
問題 1
Canvas を使用するために必要なコンポーネントは次のうちどれですか。該当するものをすべて選択してください。
▢ Bitmap
▢ Paint
▢ Path
▢ View
問題 2
invalidate() の呼び出しは(一般的に)何を行いますか?
▢ アプリを無効にして再起動します。
▢ ビットマップから描画を消去します。
▢ は、前のコードを実行しないことを示します。
▢ 画面を再描画する必要があることをシステムに伝えます。
問題 3
Canvas、Bitmap、Paint オブジェクトの機能は何ですか?
▢ 2D 描画サーフェス、画面に表示されるビットマップ、描画のスタイル設定情報。
▢ 3D 描画サーフェス、パスをキャッシュに保存するためのビットマップ、描画のスタイル設定情報。
▢ 2D 描画サーフェイス、画面に表示されるビットマップ、ビューのスタイル設定。
▢ 描画情報、描画するビットマップ、描画のスタイル設定情報のキャッシュ。
このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。