本程式碼研究室是 Kotlin 進階課程的一部分。只要您按部就班完成程式碼研究室,就能發揮本課程的最大效益。不過,您不一定要這麼做。所有課程程式碼研究室清單均列於進階 Android 版的 Kotlin 程式碼研究室到達網頁中。
引言
在 Android 中,您可以透過幾種技術在資料檢視中導入自訂 2D 圖形和動畫。
除了使用 drawables,您還可以使用 Canvas
類別的繪圖方法建立 2D 繪圖。Canvas
是 2D 繪圖介面,可提供繪圖方法。如果應用程式需要定期自行重新整理,這個方法就很實用,因為使用者看到的內容會隨著時間改變。在這個程式碼研究室中,您將瞭解如何在 View
上建立及繪製畫布。
可在畫布上執行的作業類型包括:
- 讓整個畫布充滿色彩。
- 繪製形狀,例如矩形、弧形,以及依照
Paint
物件定義的路徑樣式。Paint
物件會保存有關繪製幾何圖形 (例如線條、矩形、橢圓形和路徑) 的樣式和顏色資訊,例如文字的字體。 - 套用轉換,例如翻譯、縮放或自訂轉換。
- 剪輯是指對畫布套用形狀或路徑,定義其可見部分。
您對於 Android 繪圖的運用方式 (超級簡化!)
在 Android 或任何其他現代化系統上繪圖時,這個過程相當複雜,其中包含層次抽象層和最佳化的硬體。「Android 繪圖」是令人驚嘆的主題,用來說明撰寫的內容數量,且細節不在本程式碼研究室的涵蓋範圍內。
在這個程式碼研究室和其以畫布上呈現的應用程式上,以全螢幕檢視的方式可以採用以下方法。
- 您需要一個用來顯示繪圖的視圖。這可能是 Android 系統提供的其中一種檢視模式。或者,在這個程式碼研究室中,您可以建立自訂檢視,做為應用程式 (
MyCanvasView
) 的內容檢視。 - 這個檢視畫面和所有資料檢視一樣都具備專屬畫布 (
canvas
)。 - 如要在視圖上繪製最基本的方法,您可以覆寫其
onDraw()
方法,並在其畫布上繪圖。 - 建立繪圖時,您必須快取先前繪圖的內容。快取資料的方法有很多種,一個是位在點陣圖 (
extraBitmap
) 中,另一種方式則是將您所繪製的繪圖記錄儲存為座標和指示。 - 如要使用 CanvasDraw API 繪圖到快取點陣圖 (
extraBitmap
),請為您的快取點陣圖建立快取畫布 (extraCanvas
)。 - 然後,您就可以透過快取畫布 (
extraCanvas
) 繪圖,進而繪製到快取點陣圖 (extraBitmap
) 上。 - 如要在螢幕上顯示所有項目,請告知檢視面板 (
canvas
) 繪製快取點陣圖 (extraBitmap
)。
須知事項
- 如何使用「活動」和基本版面配置建立應用程式,並透過 Android Studio 執行應用程式。
- 如何將事件處理常式與檢視表建立關聯。
- 如何建立自訂檢視模式。
課程內容
- 如何建立
Canvas
,並配合使用者觸控操作繪製圖表。
執行步驟
- 建立應用程式,以便根據使用者輕觸螢幕來繪製線條。
- 擷取動作事件及做出回應,在畫布上以全螢幕自訂畫面的形式顯示線條。
MiniPaint 應用程式會使用自訂檢視模式,根據使用者的觸控狀態顯示線條,如以下螢幕截圖所示。
步驟 1:建立 MiniPaint 專案
- 建立名為 MiniPaint 的新 Kotlin 專案,該專案使用 Empty 活動範本。
- 開啟
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
套件中,建立名為 New > Kotlin File/Class 並命名為MyCanvasView
。 - 讓
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
。第三個引數是點陣圖色彩配置。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:初始化繪製物件
- 在 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
可使繪製的邊緣平滑,而不會影響形狀。isDither
會在true
會影響精確度小於裝置螢幕色彩的色彩品質。例如,將圖片的色彩範圍縮小到 256 (或更低) 是最常見的方法。style
會將繪畫類型設為筆劃,基本上就是線條。Paint.Style
指定繪製的原狀是填充、筆觸,還是兩者皆有 (相同顏色)。預設會填滿套用繪製的物件。(「填充」表示形狀內部色彩,「筆觸」則在外框的外框內填滿)。Paint.Join
的strokeJoin
會指定線條和曲線區隔在筆觸路徑上的合併方式。預設值為MITER
。strokeCap
會將線條末端的形狀設定為大寫。Paint.Cap
指定筆觸線條和路徑的起點和結尾方式。預設值為BUTT
。strokeWidth
指定筆劃的寬度 (以像素為單位)。預設值為寬度線,這個長度真的很精簡,因此會設為您稍早定義的STROKE_WIDTH
常數。
步驟 2. 初始化路徑物件
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
}
- 在類別層級新增缺少的
motionTouchEventX
和motionTouchEventY
變數,以便快取目前觸控事件 (MotionEvent
座標) 的 X 和 Y 座標。請將其初始化為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
,移至觸控事件 (motionTouchEventX
和motionTouchEventY
) 的 x-y 座標,並將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()
可輕鬆建立沒有圓角的平滑線條。請參閱 Bezier Curves。 - 呼叫
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
類別- Bezier 曲線 維基百科頁面
- 畫布和可繪項目
- Graphics Architecture 系列文章 (高級)
- 可繪項目
- onDraw()
- onSizeChanged()
MotionEvent
ViewConfiguration.get(context).scaledTouchSlop
這個部分會列出在代碼研究室中,受老師主導的課程作業的可能學生作業。由老師自行決定要執行下列動作:
- 視需要指派家庭作業。
- 告知學生如何提交家庭作業。
- 批改家庭作業。
老師可視需要使用這些建議,並視情況指派其他合適的家庭作業。
如果您是自行操作本程式碼研究室,歡迎透過這些家庭作業來測試自己的知識。
回答這些問題
第 1 題
使用 Canvas
時必須執行下列哪些元件?請選取所有適用選項。
▢ Bitmap
▢ Paint
▢ Path
▢ View
第 2 題
invalidate()
的呼叫有什麼作用 (一般來說)?
▢ 撤銷應用程式並重新啟動。
▢ 從點陣圖中清除繪圖。
▢ 不得執行先前的程式碼。
▢ 告知系統必須重新整理畫面。
第 3 題
Canvas
、Bitmap
和 Paint
物件的函式是什麼?
▢ 2D 繪圖介面、螢幕上的點陣圖、繪圖樣式資訊。
▢ 3D 繪圖表面、快取路徑點陣圖、繪圖樣式資訊。
▢ 2D 繪圖介面、螢幕上的點陣圖、視圖樣式。
▢ 用於繪圖資訊的快取、可繪製的點陣圖、繪圖的樣式資訊。
如要瞭解本課程中其他程式碼研究室的連結,請參閱 Kotlin 的進階 Android 程式碼研究室到達網頁。