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

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

はじめに

Android には、ビューにカスタム 2D グラフィックスやアニメーションを実装するいくつかの手法があります。

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

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

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

Android での描画についての考え方(大幅に簡略化)

Android やその他の最新システムでの描画は、抽象化レイヤとハードウェアまでの最適化を含む複雑なプロセスです。「Android の描画方法」は、どれほど書かれているかという点で興味深いトピックであり、その詳細はこの Codelab の範囲外です。

この Codelab のコンテキストと、キャンバスに表示して全画面表示されるアプリは、以下のように考えることができます。

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

前提となる知識

  • アクティビティと基本レイアウトを使用してアプリを作成し、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 という新しい Kotlin ファイル/クラスを作成します。
  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() をオーバーライドする

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

  1. MyCanvasView で、クラスレベルで、キャンバスとビットマップの変数を定義します。extraCanvas および extraBitmap という名前を付けます。これらは、以前に描画したものをキャッシュに保存するためのビットマップとキャンバスです。
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
  1. キャンバスの背景色用にクラスレベルの変数 backgroundColor を定義し、前に定義した colorBackground に初期化します。
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. MyCanvasViewonSizeChanged() メソッドをオーバーライドします。このコールバック メソッドは、画面の寸法を変更して、新しい幅と高さ(変更するもの)と変更前の幅と高さ(変更前のもの)を指定して、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 をリサイクルします。
if (::extraBitmap.isInitialized) extraBitmap.recycle()

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

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

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

  1. onDraw() をオーバーライドして、キャッシュされた extraBitmap のコンテンツをビューに関連付けられたキャンバスに描画します。drawBitmap() Canvas メソッドにはいくつかのバージョンがあります。このコードでは、ビットマップ、左上隅の x 座標と y 座標(ピクセル単位)、Paint には 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 に設定すると、図形に影響を与えずに描画される角が滑らかになります。
  • true の場合、isDither は、デバイスよりも高い精度のカラーをダウンサンプリングする方法に影響します。たとえば、ディザリングは、画像の色範囲を 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() メソッドをオーバーライドして、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
}
  1. クラスレベルで、現在のタッチイベントの x 座標と y 座標(MotionEvent 座標)をキャッシュするための欠落している 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. アプリを実行します。このフレームに注目してください。

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

現在のアプリでは、描画情報はビットマップに格納されます。これは優れたソリューションですが、描画情報を保存する唯一の方法ではありません。描画履歴の保存方法は、アプリとさまざまな要件によって異なります。たとえば、図形を描画する場合は、図形のリストとそのサイズとサイズを保存できます。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 Codelab の高度な Codelab のランディング ページをご覧ください。