この Codelab は、Kotlin での高度な Android 開発コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できますが、これは必須ではありません。すべてのコース Codelab は Kotlin での Codelab の高度な Codelab のランディング ページに掲載されています。
はじめに
Android には、ビューにカスタム 2D グラフィックスやアニメーションを実装するいくつかの手法があります。
ドローアブルの使用に加えて、Canvas
クラスの描画メソッドを使って 2D 描画を作成できます。Canvas
は、描画方法を提供する 2D 描画サーフェスです。これは、時間の経過とともに変化することをユーザーが認識できるため、アプリを定期的に再描画する必要がある場合に便利です。この Codelab では、View
に表示されるキャンバスを作成して描画する方法を学びます。
キャンバスで実行できる操作の種類は次のとおりです。
- キャンバス全体を色で塗りつぶします。
Paint
オブジェクトで定義された長方形、円、パスなどのシェイプを描画します。Paint
オブジェクトは、ジオメトリ(線、長方形、楕円、パスなど)の描画方法、たとえばテキストの書体に関するスタイルと色の情報を保持します。- 変換、スケーリング、カスタム変換などの変換を適用します。
- クリップとは、キャンバスにシェイプやパスを適用して可視部分を定義することです。
Android での描画についての考え方(大幅に簡略化)
Android やその他の最新システムでの描画は、抽象化レイヤとハードウェアまでの最適化を含む複雑なプロセスです。「Android の描画方法」は、どれほど書かれているかという点で興味深いトピックであり、その詳細はこの Codelab の範囲外です。
この Codelab のコンテキストと、キャンバスに表示して全画面表示されるアプリは、以下のように考えることができます。
- 描画したものを表示するにはビューが必要です。Android システムが提供するビューの一つである可能性があります。または、この Codelab では、アプリのコンテンツ ビューとして機能するカスタムビュー(
MyCanvasView
)を作成します。 - このビューには、すべてのビューと同様、独自のキャンバス(
canvas
)が付属しています。 - ビューのキャンバスに描画する最も基本的な方法として、
onDraw()
メソッドをオーバーライドして、キャンバスに描画するという方法があります。 - 描画を作成する際は、前に描画したものをキャッシュに保存する必要があります。データをキャッシュに保存する方法はいくつかあります。1 つはビットマップ(
extraBitmap
)で、もう 1 つは描画した座標を座標と指示として保存する方法です。 - Canvas Drawing API を使用してキャッシュ ビットマップ(
extraBitmap
)に描画するには、キャッシュ ビットマップ用のキャッシュ キャンバス(extraCanvas
)を作成します。 - 次に、キャッシュ キャンバス(
extraCanvas
)に描画し、キャッシュ ビットマップ(extraBitmap
)に描画します。 - 画面に描画されたすべてを表示するには、ビューのキャンバス(
canvas
)にキャッシュ ビットマップ(extraBitmap
)を描画するように指示します。
前提となる知識
- アクティビティと基本レイアウトを使用してアプリを作成し、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
という新しい Kotlin ファイル/クラスを作成します。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() をオーバーライドする
onSizeChanged()
メソッドは、ビューのサイズが変更されるたびに Android システムによって呼び出されます。ビューはサイズなしで開始されるため、アクティビティが最初に作成してインフレートした後に、ビューの 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
をリサイクルします。
if (::extraBitmap.isInitialized) extraBitmap.recycle()
ステップ 2:onDraw() をオーバーライドする
MyCanvasView
の描画作業はすべて onDraw()
で行われます。
まず、キャンバスを表示して、onSizeChanged()
で設定した背景色で画面を塗りつぶします。
onDraw()
をオーバーライドして、キャッシュされたextraBitmap
のコンテンツをビューに関連付けられたキャンバスに描画します。drawBitmap()
Canvas
メソッドにはいくつかのバージョンがあります。このコードでは、ビットマップ、左上隅の x 座標と y 座標(ピクセル単位)、Paint
には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 float
MyCanvasView
のクラスレベルで、描画する色を保持するための変数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
に設定すると、図形に影響を与えずに描画される角が滑らかになります。true
の場合、isDither
は、デバイスよりも高い精度のカラーをダウンサンプリングする方法に影響します。たとえば、ディザリングは、画像の色範囲を 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
}
- クラスレベルで、現在のタッチイベントの x 座標と y 座標(
MotionEvent
座標)をキャッシュするための欠落している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 = 0f
- 次のように
touchStart()
メソッドを実装します。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: Rect
onSizeChanged()
の最後にインセットを定義し、新しいディメンションとインセットを使用して、フレームに使用する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)
- アプリを実行します。このフレームに注目してください。
タスク(省略可): パスにデータを保存する
現在のアプリでは、描画情報はビットマップに格納されます。これは優れたソリューションですが、描画情報を保存する唯一の方法ではありません。描画履歴の保存方法は、アプリとさまざまな要件によって異なります。たとえば、図形を描画する場合は、図形のリストとそのサイズとサイズを保存できます。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
クラス- ベジェ曲線に関するウィキペディアのページ
- キャンバスとドローアブル
- グラフィック アーキテクチャ シリーズの記事(上級者向け)
- ドローアブル
- onDraw()
- onSizeChanged()
MotionEvent
ViewConfiguration.get(context).scaledTouchSlop
このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。
- 必要に応じて課題を割り当てます。
- 宿題の提出方法を生徒に伝える。
- 宿題を採点します。
教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。
この Codelab にご自分で取り組む場合は、これらの課題を使用して知識をテストしてください。
次の質問に答えてください。
問題 1
Canvas
の操作に必要なコンポーネントは次のうちどれですか。該当するものをすべて選択してください。
▢ Bitmap
▢ Paint
▢ Path
▢ View
質問 2
invalidate()
の呼び出しは何を行いますか。
▢ アプリを無効にし、再起動します。
▢ ビットマップの描画を消去します。
▢ 前のコードを実行するべきでないことを示します。
▢ 画面の再描画が必要であることをシステムに伝えます。
問題 3
Canvas
、Bitmap
、Paint
オブジェクトの関数は何ですか。
▢ 2D 描画サーフェス、画面上に表示されるビットマップ、描画用のスタイル情報。
▢ 3D 描画サーフェス、パスをキャッシュするためのビットマップ、描画用のスタイル情報。
▢ 2D 描画サーフェス、画面上に表示されるビットマップ、ビューのスタイル設定
▢ 描画情報用のキャッシュ、描画用ビットマップ、描画用スタイル情報。
このコースの他の Codelab へのリンクについては、Kotlin Codelab の高度な Codelab のランディング ページをご覧ください。