這個程式碼研究室是「Android Kotlin 進階功能」課程的一部分。如果您按部就班完成每一堂程式碼研究室課程,就能充分體驗到本課程的價值,但這不是強制要求。如要查看所有課程程式碼研究室,請前往 Android Kotlin 進階功能程式碼研究室登陸頁面。
簡介
在 Android 中,您可以使用多種技術,在檢視區塊中實作自訂 2D 圖像和動畫。
除了使用可繪項目,您也可以使用 Canvas 類別的繪圖方法建立 2D 繪圖。Canvas 是 2D 繪圖表面,提供繪圖方法。如果應用程式需要定期重新繪製自身內容 (因為使用者看到的內容會隨時間改變),這項功能就很有用。在本程式碼研究室中,您將瞭解如何建立畫布並在畫布上繪圖,然後在 View 中顯示畫布。
您可以在畫布上執行的作業類型包括:
- 將整個畫布填滿顏色。
- 繪製形狀,例如矩形、弧形和路徑,樣式則定義於
Paint物件中。Paint物件會保存幾何圖形 (例如線條、矩形、橢圓和路徑) 的繪製樣式和顏色資訊,或是文字的字體。 - 套用轉換,例如翻譯、縮放或自訂轉換。
- 裁剪:在畫布上套用形狀或路徑,定義可見部分。

Android 繪圖的簡化概念
在 Android 或任何其他新式系統中繪圖是複雜的程序,包括抽象層和硬體最佳化。Android 的繪製方式是個很有趣的主題,相關著作相當多,但詳細內容超出本程式碼研究室的範圍。
在本程式碼研究室的脈絡下,以及在畫布上繪製內容並以全螢幕檢視模式顯示的應用程式中,您可以將其視為下列項目。

- 您需要檢視畫面來顯示繪製內容。這可能是 Android 系統提供的其中一個檢視區塊。或者,在本程式碼研究室中,您會建立做為應用程式內容檢視區塊的自訂檢視區塊 (
MyCanvasView)。 - 這個檢視畫面與所有檢視畫面一樣,都有自己的畫布 (
canvas)。 - 如要在檢視區塊的畫布上繪圖,最基本的方法是覆寫
onDraw()方法,並在畫布上繪圖。 - 建構繪圖時,您需要快取先前繪製的內容。快取資料的方法有很多種,其中一種是使用點陣圖 (
extraBitmap),另一種則是將繪製內容的歷史記錄儲存為座標和指令。 - 如要使用畫布繪圖 API 繪製快取點陣圖 (
extraBitmap),請為快取點陣圖建立快取畫布 (extraCanvas)。 - 接著,您會在快取畫布 (
extraCanvas) 上繪圖,這會繪製到快取點陣圖 (extraBitmap) 上。 - 如要顯示在畫面上繪製的所有內容,請告知檢視區塊的畫布 (
canvas) 繪製快取點陣圖 (extraBitmap)。
必備知識
- 如何使用 Android Studio 建立含有活動和基本版面配置的應用程式,並執行該應用程式。
- 如何將事件處理常式與檢視區塊建立關聯。
- 如何建立自訂檢視畫面。
課程內容
- 如何建立
Canvas,並在使用者觸控時繪製內容。
學習內容
- 建立應用程式,讓使用者在觸控螢幕時,螢幕上會繪製線條。
- 擷取動作事件,並在畫布上繪製線條,這些線條會顯示在螢幕上的全螢幕自訂檢視區塊中。
MiniPaint 應用程式會使用自訂檢視區塊,根據使用者觸控操作顯示線條,如下方螢幕截圖所示。

步驟 1:建立 MiniPaint 專案
- 使用「Empty Activity」範本建立名為「MiniPaint」的新 Kotlin 專案。
- 開啟
app/res/values/colors.xml檔案,並新增下列兩種顏色。
<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() 方法。由於檢視區塊一開始沒有大小,因此在 Activity 首次建立並擴充檢視區塊後,系統也會呼叫檢視區塊的 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。第三個引數是點陣圖顏色設定。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 座標 (以像素為單位),以及null(因為您稍後會設定)。Paint
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)
}color是您先前定義的drawColor。paintisAntiAlias定義是否要套用邊緣平滑處理。將isAntiAlias設為true可平滑化繪製內容的邊緣,但不會影響形狀。isDither,當true時,會影響顏色向下取樣的精確度 (高於裝置)。舉例來說,最常見的手段是使用半色調技術,將圖片的色域縮減至 256 種 (或更少) 顏色。style會設定要對筆觸 (本質上是線條) 執行的繪製類型。Paint.Style指定要繪製的圖元是否要填滿、描邊或兩者皆是 (使用相同顏色)。根據預設,系統會填滿套用顏料的物件。(「填滿」會為形狀內部著色,「筆觸」則會沿著形狀輪廓著色)。strokeJoin的Paint.Join會指定線條和曲線線段在筆觸路徑上的接合方式。預設值為MITER。strokeCap會將線條的結尾形狀設為端點。Paint.Cap會指定筆觸線條和路徑的開頭和結尾。預設值為BUTT。strokeWidth可以像素為單位,指定筆劃的寬度。預設值為髮絲寬度,也就是非常細的線條,因此請將其設為您先前定義的STROKE_WIDTH常數。
步驟 2:初始化 Path 物件
Path 是使用者繪製內容的路徑。
- 在
MyCanvasView中新增path變數,並使用Path物件初始化,以儲存使用者在螢幕上觸控時繪製的路徑。匯入android.graphics.Path做為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
}- 在類別層級,新增缺少的
motionTouchEventX和motionTouchEventY變數,用於快取目前觸控事件的 x 和 y 座標 (MotionEvent座標)。將這些變數初始化為0f。
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- 為三個函式
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)、在兩點之間建立曲線並儲存在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)。 - 如果移動距離超過觸控容差,請在路徑中新增區隔。
- 將下一個片段的起點設為這個片段的終點。
- 使用
quadTo()而非lineTo()建立平滑的線條,沒有轉角。請參閱「貝茲曲線」。 - 呼叫
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中,新增名為frame的變數,該變數會保留Rect物件。
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()- 執行應用程式,您會發現兩者完全相同。
下載完成的程式碼研究室程式碼。
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
您也可以將存放區下載為 ZIP 檔案、將其解壓縮,並在 Android Studio 中開啟。
Udacity 課程:
Android 開發人員說明文件:
Canvas類別Bitmap類別View類別Paint類別Bitmap.config設定Path類別- 貝茲曲線維基百科頁面
- 畫布和可繪項目
- 圖像架構系列文章 (進階)
- 可繪項目
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
使用 Canvas 時,下列哪些元件為必要條件?請選取所有適用選項。
▢ Bitmap
▢ Paint
▢ Path
▢ View
第 2 題
呼叫 invalidate() 會執行什麼動作 (一般而言)?
▢ 使應用程式失效並重新啟動。
▢ 從點陣圖中清除繪圖。
▢ 表示不應執行先前的程式碼。
▢ 指示系統重新繪製畫面。
第 3 題
Canvas、Bitmap 和 Paint 物件的用途為何?
▢ 2D 繪圖表面、螢幕上顯示的點陣圖、繪圖的樣式資訊。
▢ 3D 繪圖表面、用於快取路徑的點陣圖,以及用於繪圖的樣式資訊。
▢ 2D 繪圖介面、螢幕上顯示的點陣圖、檢視區塊的樣式。
▢ 繪圖資訊的快取、要繪製的點陣圖、繪圖的樣式資訊。
如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 進階功能程式碼研究室登陸頁面。