キャンバス オブジェクトを描画する

この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。

はじめに

Android では、ビューにカスタムの 2D グラフィックとアニメーションを実装するための手法がいくつか用意されています。

ドローアブルを使用するだけでなく、Canvas クラスの描画メソッドを使用して 2D 描画を作成することもできます。Canvas は、描画用のメソッドを提供する 2D 描画サーフェスです。これは、ユーザーが目にするものが時間とともに変化するため、アプリが定期的に再描画する必要がある場合に便利です。この Codelab では、View に表示されるキャンバスを作成して描画する方法を学びます。

キャンバスで実行できる操作の種類は次のとおりです。

  • キャンバス全体を色で塗りつぶします。
  • Paint オブジェクトで定義されたスタイルで、長方形、円弧、パスなどのシェイプを描画します。Paint オブジェクトには、ジオメトリ(線、長方形、楕円、パスなど)の描画方法に関するスタイルと色の情報や、テキストの書体などの情報が保持されます。
  • 変換(変換、スケーリング、カスタム変換など)を適用します。
  • クリップ。つまり、キャンバスにシェイプまたはパスを適用して、表示する部分を定義します。

Android の描画の考え方(超簡略化)

Android やその他の最新のシステムでの描画は、抽象化のレイヤやハードウェアまでの最適化を含む複雑なプロセスです。Android の描画は、多くの文献で取り上げられている興味深いトピックですが、その詳細はこの Codelab の範囲外です。

この Codelab のコンテキストでは、全画面ビューで表示するためにキャンバスに描画するアプリについて、次のように考えることができます。

  1. 描画内容を表示するためのビューが必要です。これは、Android システムが提供するビューの 1 つである可能性があります。または、この Codelab では、アプリのコンテンツ ビュー(MyCanvasView)として機能するカスタムビューを作成します。
  2. このビューには、他のすべてのビューと同様に、独自のキャンバス(canvas)が付属しています。
  3. ビューのキャンバスに描画する最も基本的な方法は、onDraw() メソッドをオーバーライドして、そのキャンバスに描画することです。
  4. 描画をビルドするときは、以前に描画したものをキャッシュに保存する必要があります。データをキャッシュに保存する方法はいくつかあります。1 つはビットマップ(extraBitmap)に保存する方法です。もう 1 つは、描画した内容の履歴を座標と指示として保存する方法です。
  5. キャンバス描画 API を使用してキャッシュ保存ビットマップ(extraBitmap)に描画するには、キャッシュ保存ビットマップ用のキャッシュ保存キャンバス(extraCanvas)を作成します。
  6. 次に、キャッシュ キャンバス(extraCanvas)に描画します。この描画はキャッシュ ビットマップ(extraBitmap)に描画されます。
  7. 画面に描画されたものをすべて表示するには、ビューのキャンバス(canvas)にキャッシュ保存されたビットマップ(extraBitmap)を描画するよう指示します。

前提となる知識

  • Activity と基本的なレイアウトを含むアプリを作成し、Android Studio を使用して実行する方法。
  • イベント ハンドラをビューに関連付ける方法。
  • カスタムビューの作成方法。

学習内容

  • Canvas を作成し、ユーザーのタッチに応じて描画する方法。

演習内容

  • ユーザーが画面に触れると、画面に線を描画するアプリを作成します。
  • モーション イベントをキャプチャし、その応答として、画面の全画面カスタムビューに表示されるキャンバスに線を描画します。

MiniPaint アプリでは、次のスクリーンショットに示すように、ユーザーのタッチに応じて線を表示するためにカスタムビューを使用しています。

ステップ 1. MiniPaint プロジェクトを作成する

  1. Empty Activity テンプレートを使用して、MiniPaint という新しい Kotlin プロジェクトを作成します。
  2. app/res/values/colors.xml ファイルを開き、次の 2 つの色を追加します。
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
  1. styles.xml を開く
  2. 指定された AppTheme スタイルの親で、DarkActionBarNoActionBar に置き換えます。これによりアクションバーが削除され、全画面表示で描画できるようになります。
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

ステップ 2. MyCanvasView クラスを作成する

このステップでは、描画用のカスタムビュー MyCanvasView を作成します。

  1. app/java/com.example.android.minipaint パッケージで、MyCanvasView という [New] > [Kotlin File/Class] を作成します。
  2. MyCanvasView クラスが View クラスを拡張し、context: Context を渡すようにします。提案されたインポートを承認します。
import android.content.Context
import android.view.View

class MyCanvasView(context: Context) : View(context) {
}

ステップ 3. MyCanvasView をコンテンツ ビューとして設定する

MyCanvasView で描画する内容を表示するには、MainActivity のコンテンツ ビューとして設定する必要があります。

  1. strings.xml を開き、ビューのコンテンツの説明に使用する文字列を定義します。
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
   Drag your fingers to draw. Rotate the phone to clear.</string>
  1. MainActivity.kt を開く
  2. onCreate() で、setContentView(R.layout.activity_main) を削除します。
  3. MyCanvasView のインスタンスを作成します。
val myCanvasView = MyCanvasView(this)
  1. その下に、myCanvasView のレイアウトの全画面表示をリクエストします。そのためには、myCanvasViewSYSTEM_UI_FLAG_FULLSCREEN フラグを設定します。このようにして、ビューが画面全体を埋めます。
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
  1. コンテンツの説明を追加します。
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
  1. その下に、コンテンツ ビューを myCanvasView に設定します。
setContentView(myCanvasView)
  1. アプリを実行します。キャンバスのサイズが設定されておらず、まだ何も描画していないため、完全に白い画面が表示されます。

ステップ 1. onSizeChanged() をオーバーライドする

ビューのサイズが変更されるたびに、Android システムによって onSizeChanged() メソッドが呼び出されます。ビューはサイズなしで開始されるため、アクティビティが最初にビューを作成して拡張した後にも、ビューの onSizeChanged() メソッドが呼び出されます。したがって、この onSizeChanged() メソッドは、ビューのキャンバスを作成して設定するのに最適な場所です。

  1. MyCanvasView で、クラスレベルでキャンバスとビットマップの変数を定義します。これらを extraCanvasextraBitmap と呼びます。これらは、以前に描画されたものをキャッシュに保存するためのビットマップとキャンバスです。
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
  1. キャンバスの背景色を表すクラスレベルの変数 backgroundColor を定義し、前に定義した colorBackground に初期化します。
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. MyCanvasView で、onSizeChanged() メソッドをオーバーライドします。このコールバック メソッドは、変更された画面の寸法(新しい幅と高さ(変更先)と古い幅と高さ(変更元))とともに Android システムによって呼び出されます。
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   super.onSizeChanged(width, height, oldWidth, oldHeight)
}
  1. onSizeChanged() 内で、画面サイズである新しい幅と高さで Bitmap のインスタンスを作成し、extraBitmap に割り当てます。3 番目の引数はビットマップの色構成です。ARGB_8888 は各色を 4 バイトで保存するため、推奨されます。
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  1. extraBitmap から Canvas インスタンスを作成し、extraCanvas に割り当てます。
 extraCanvas = Canvas(extraBitmap)
  1. extraCanvas を塗りつぶす背景色を指定します。
extraCanvas.drawColor(backgroundColor)
  1. onSizeChanged() を見ると、関数が実行されるたびに新しいビットマップとキャンバスが作成されています。サイズが変更されたため、新しいビットマップが必要です。ただし、これはメモリリークであり、古いビットマップが残ります。これを修正するには、super の呼び出しの直後に次のコードを追加して、次の extraBitmap を作成する前に extraBitmap をリサイクルします。
if (::extraBitmap.isInitialized) extraBitmap.recycle()

ステップ 2. onDraw() をオーバーライドする

MyCanvasView の描画作業はすべて onDraw() で行われます。

まず、onSizeChanged() で設定した背景色で画面を塗りつぶして、キャンバスを表示します。

  1. onDraw() をオーバーライドし、キャッシュに保存された extraBitmap の内容をビューに関連付けられたキャンバスに描画します。drawBitmap() Canvas メソッドにはいくつかのバージョンがあります。このコードでは、ビットマップ、左上隅の x 座標と y 座標(ピクセル単位)、Paintnull を指定します。null は後で設定します。
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}


onDraw() に渡され、システムがビットマップを表示するために使用するキャンバスは、onSizeChanged() メソッドで作成し、ビットマップに描画するために使用したキャンバスとは異なります。

  1. アプリを実行します。画面全体が指定した背景色で塗りつぶされているはずです。

描画するには、描画時のスタイルの指定方法を記述する Paint オブジェクトと、描画対象を指定する Path が必要です。

ステップ 1. Paint オブジェクトを初期化する

  1. MyCanvasView.kt のファイルレベルの先頭で、ストロークの幅の定数を定義します。
private const val STROKE_WIDTH = 12f // has to be float
  1. MyCanvasView のクラスレベルで、描画に使用する色を保持する変数 drawColor を定義し、以前に定義した colorPaint リソースで初期化します。
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
  1. クラスレベルで、以下のように 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)
}
  • paintcolor は、先ほど定義した drawColor です。
  • isAntiAlias は、エッジ スムージングを適用するかどうかを定義します。isAntiAliastrue に設定すると、形状に影響を与えることなく、描画されたもののエッジが滑らかになります。
  • isDithertrue の場合)は、デバイスよりも高精度の色をダウンサンプリングする方法に影響します。たとえば、ディザリングは、画像の色の範囲を 256 色以下に減らす最も一般的な方法です。
  • style は、ストローク(基本的には線)に対して行うペイントのタイプを設定します。Paint.Style は、描画されるプリミティブが塗りつぶされるか、ストロークされるか、またはその両方(同じ色で)であるかを指定します。デフォルトでは、ペイントが適用されるオブジェクトが塗りつぶされます。(「塗りつぶし」はシェイプの内側を塗り、「ストローク」はアウトラインに沿って描画します)。
  • Paint.JoinstrokeJoin は、ストロークされたパスで線と曲線セグメントが結合する方法を指定します。デフォルトは MITER です。
  • strokeCap は、線の端の形状をキャップに設定します。Paint.Cap は、ストロークされた線とパスの始点と終点の形状を指定します。デフォルトは BUTT です。
  • strokeWidth には、ストロークの幅をピクセル単位で指定します。デフォルトはヘアライン幅で非常に細いため、先ほど定義した STROKE_WIDTH 定数に設定します。

ステップ 2. Path オブジェクトを初期化する

Path は、ユーザーが描画しているパスです。

  1. MyCanvasView で、変数 path を追加し、Path オブジェクトで初期化して、ユーザーが画面に触れたときに描画されるパスを保存します。Pathandroid.graphics.Path をインポートします。
private var path = Path()

ステップ 1. ディスプレイ上の動きに反応する

ユーザーがディスプレイにタッチするたびに、ビューの onTouchEvent() メソッドが呼び出されます。

  1. MyCanvasView で、onTouchEvent() メソッドをオーバーライドして、渡された eventx 座標と 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
}
  1. クラスレベルで、現在のタッチイベント(MotionEvent 座標)の x 座標と y 座標をキャッシュに保存するための motionTouchEventX 変数と motionTouchEventY 変数を追加します。0f に初期化します。
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
  1. 3 つの関数 touchStart()touchMove()touchUp() のスタブを作成します。
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. コードはビルドして実行できますが、まだ色付きの背景と何も変わりません。

ステップ 2. touchStart() を実装する

このメソッドは、ユーザーが最初に画面にタッチしたときに呼び出されます。

  1. クラスレベルで、最新の x 値と y 値をキャッシュに保存する変数を追加します。ユーザーが移動を停止してタッチを離すと、これらは次のパス(描画する線の次のセグメント)の開始点になります。
private var currentX = 0f
private var currentY = 0f
  1. touchStart() メソッドを次のように実装します。path をリセットし、タップイベントの x-y 座標(motionTouchEventXmotionTouchEventY)に移動して、その値に currentXcurrentY を割り当てます。
private fun touchStart() {
   path.reset()
   path.moveTo(motionTouchEventX, motionTouchEventY)
   currentX = motionTouchEventX
   currentY = motionTouchEventY
}

ステップ 3. touchMove() を実装する

  1. クラスレベルで、touchTolerance 変数を追加し、ViewConfiguration.get(context).scaledTouchSlop に設定します。
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

パスを使用すると、すべてのピクセルを描画したり、ディスプレイの更新をリクエストしたりする必要がなくなります。代わりに、ポイント間のパスを補間すると、パフォーマンスが大幅に向上します。

  • 指がほとんど動いていない場合は、描画する必要はありません。
  • 指の移動距離が touchTolerance より小さい場合は、描画しません。
  • scaledTouchSlop は、ユーザーがスクロールしているとシステムが判断するまでにタップが動ける距離をピクセル単位で返します。
  1. touchMove() メソッドを定義します。移動距離(dxdy)を計算し、2 点間の曲線を作成して path に保存し、実行中の currentXcurrentY の合計を更新して、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()
}

この方法の詳細:

  1. 移動した距離(dx, dy)を計算します。
  2. 移動量がタッチ許容値を超えた場合は、パスにセグメントを追加します。
  3. 次のセグメントの開始点をこのセグメントのエンドポイントに設定します。
  4. lineTo() の代わりに quadTo() を使用すると、角のない滑らかな線が描画されます。ベジエ曲線をご覧ください。
  5. invalidate() を呼び出して(最終的に onDraw() を呼び出して)ビューを再描画します。

ステップ 4: touchUp() を実装する

ユーザーがタッチを離したら、パスをリセットして再度描画されないようにするだけで済みます。何も描画されないため、無効化は不要です。

  1. touchUp() メソッドを実装します。
private fun touchUp() {
   // Reset the path so it doesn't get drawn again.
   path.reset()
}
  1. コードを実行し、指で画面に描画します。デバイスを回転させると、描画状態が保存されないため、画面がクリアされます。このサンプルアプリでは、ユーザーが画面を簡単にクリアできるように、このようになっています。

ステップ 5: スケッチの周りにフレームを描く

ユーザーが画面に描画すると、アプリはパスを構築し、ビットマップ extraBitmap に保存します。onDraw() メソッドは、ビューのキャンバスに余分なビットマップを表示します。onDraw() でさらに描画できます。たとえば、ビットマップを描画した後に図形を描画できます。

このステップでは、写真の端にフレームを描画します。

  1. MyCanvasView で、Rect オブジェクトを保持する frame という変数を追加します。
private lateinit var frame: Rect
  1. onSizeChanged() の最後にインセットを定義し、新しいディメンションとインセットを使用してフレームに使用する Rect を作成するコードを追加します。
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
  1. onDraw() で、ビットマップを描画した後、長方形を描画します。
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
  1. アプリを実行します。フレームが表示されます。

タスク(省略可): Path にデータを保存する

現在のアプリでは、描画情報はビットマップに保存されます。これは優れた解決策ですが、描画情報を保存する方法はこれだけではありません。描画履歴の保存方法は、アプリやさまざまな要件によって異なります。たとえば、図形を描画している場合、図形のリストとその位置と寸法を保存できます。MiniPaint アプリの場合、パスを Path として保存できます。お試しになる場合は、以下の概要をご覧ください。

  1. MyCanvasView で、extraCanvasextraBitmap のコードをすべて削除します。
  2. これまでのパスと現在描画中のパスの変数を追加します。
// Path representing the drawing so far
private val drawing = Path()

// Path representing what's currently being drawn
private val curPath = Path()
  1. 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)
  1. touchUp() で、現在のパスを前のパスに追加し、現在のパスをリセットします。
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()
  1. アプリを実行します。違いはまったくありません。

完成した Codelab のコードをダウンロードします。

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas


または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

ZIP をダウンロード

  • Canvas は、描画用のメソッドを提供する 2D 描画サーフェスです。
  • Canvas は、それを表示する View インスタンスに関連付けることができます。
  • Paint オブジェクトには、ジオメトリ(線、長方形、楕円、パスなど)とテキストの描画方法に関するスタイルと色の情報が保持されます。
  • キャンバスを操作する一般的なパターンは、カスタムビューを作成し、onDraw() メソッドと onSizeChanged() メソッドをオーバーライドすることです。
  • onTouchEvent() メソッドをオーバーライドして、ユーザーのタップをキャプチャし、描画によって応答します。
  • 追加のビットマップを使用して、経時的に変化する描画の情報をキャッシュに保存できます。または、シェイプやパスを保存することもできます。

Udacity コース:

Android デベロッパー ドキュメント:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

Canvas を使用するために必要なコンポーネントは次のうちどれですか。該当するものをすべて選択してください。

Bitmap

Paint

Path

View

問題 2

invalidate() の呼び出しは(一般的に)何を行いますか?

▢ アプリを無効にして再起動します。

▢ ビットマップから描画を消去します。

▢ は、前のコードを実行しないことを示します。

▢ 画面を再描画する必要があることをシステムに伝えます。

問題 3

CanvasBitmapPaint オブジェクトの機能は何ですか?

▢ 2D 描画サーフェス、画面に表示されるビットマップ、描画のスタイル設定情報。

▢ 3D 描画サーフェス、パスをキャッシュに保存するためのビットマップ、描画のスタイル設定情報。

▢ 2D 描画サーフェイス、画面に表示されるビットマップ、ビューのスタイル設定。

▢ 描画情報、描画するビットマップ、描画のスタイル設定情報のキャッシュ。

このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。