onDraw() เป็นเมธอดสำคัญของคลาส View ใน Android SDK ที่รับผิดชอบในการวาดเนื้อหาของมุมมอง ระบบจะเรียก onDraw ทุกครั้งที่รูปลักษณ์ของ View มีการอัพเดต — ระหว่างการเรนเดอร์เบรื้องต้นใช้งาน หลังจาก invalidate() หรือเมื่อขนาดเปลี่ยนแปลง ตามเอกสาร Android Developers (2026) การเปิดเพิ่ม onDraw โดยใช้ Canvas API เป็นวิธีหลักในการสร้างกราฟิกส์ที่กำหนดเองในแอปพลิเคชัน Android
หัวข้อสำคัญ
onDraw(Canvas canvas) เป็นเมธอด protected ของคลาส android.view.View ซึ่งระบบ Android เรียกเมื่อ View ต้องการถูกวาด นักพัฒนาเปิดเพิ่มเมธอดนี้เพื่อเพิ่มการวาดรูปที่กำหนดเอง: รูปร่าง, ข้อความ, รูปภาพ, กราเดียนต์, แอนิเมชัน พารามิเตอร์ Canvas เป็นพื้นผิวกราฟิกที่ใช้ดำเนินการวาดรูปทั้งหมด
ลายเซ็นเมธอดง่ายๆ: override fun onDraw(canvas: Canvas) ภายในเมธอดนี้ ไม่ควรสร้างออบเจกต์ใหม่ (เพื่อหลีกเลี่ยง GC overload), ดำเนินการที่ใช้เวลานาน หรือทำงานกับเครือข่าย View ยังมีเมธอด onDraw ในวงจรการทำงานของมันระหว่าง onLayout (การคำนวณตำแหน่ง) และ dispatchDraw (การวาดองค์ประกอบย่อย)
เมื่อสืบทอดจาก View, การเปิดเพิ่ม onDraw เป็นสิ่งจำเป็นสำหรับการแสดงเนื้อหาใดๆ — มิฉะนั้น View จะว่างเปล่า เมื่อสืบทอดจากซับคลาสที่มีอยู่แล้ว (TextView, ImageView, Button), การเปิดเพิ่ม onDraw เป็นตัวเลือก และใช้เพื่อเพิ่มกราฟิกเพิ่มเติมเทืองเนื้อหามาตรฐาน
Android ดำเนินการวาดรูป View ในลำดับที่กำหนดอย่างเข้มงวด ซึ่งเรียกจากเมนไธร์สรีด (UI thread) วงจรเต็มประกอบด้วยสามเรียง: measure (การวัดขนาด), layout (การคำนวณตำแหน่ง) และ draw (การเรนเดอร์) onDraw เป็นส่วนหนึ่งของเรียง draw และถูกเรียกหลังจากการกำหนดขนาดและตำแหน่งของ View แล้ว
draw(Canvas canvas) เป็นเมธอดสาธารณะของ View ที่เรียก onDraw และ dispatchDraw เมธอด draw ไม่ได้ออกแบบให้เปิดเพิ่ม — แทนที่จะเปิดเพิ่ม onDraw หลังจาก onDraw ทำงานสำเร็จสำหรับ View ปัจจุบัน, dispatchDraw จะถูกเรียก ซึ่งเรียก draw แบบทอดซ้ำสำหรับ View ย่อยทั้งหมด (ถ้า View ปัจจุบันเป็นคอนเทนเนอร์ ViewGroup)
onDraw ถูกเรียกภายใต้เงื่อนไขต่อไปนี้: 1) View แสดงครั้งแรก (หลัง setVisibility(VISIBLE)); 2) แอพเรียก invalidate() บน View นี้หรือแม่ของมัน; 3) ขนาด View เปลี่ยนไป (layout ใหม่); 4) แอนิเมชันที่เกี่ยวข้องกับ View เปลี่ยนไป (ผ่าน ValueAnimator หรือ ObjectAnimator); 5) สถานะ View เปลี่ยนไป (pressed, focused, enabled)
ตั้งแต่ Android 3.0 (API 11), การดำเนินงาน Canvas ส่วนใหญ่ถูกเร่งด้วยฮาร์ดแวร์ผ่าน GPU นักพัฒนาสามารถตรวจสอบได้ว่าเปิดใช้การเร่งหรือไม่ผ่าน canvas.isHardwareAccelerated() ด้วยการเร่งฮาร์ดแวร์, บางการดำเนินงาน Canvas ไม่ได้รับการสนับสนุน (ตัวอย่าง, clipPath กับบริเวณที่ไม่ใช่สี่เหลี่ยมสมผล) และอาจถูกละเลยหรือทำให้เกิดข้อผิดพลาด
Canvas มีชุดเมธอดการวาดรูปที่หลากหลาย เมธอดทั้งหมดทำงานในระบบพิกัดปัจจุบัน, ซึ่งสามารถเปลี่ยนแปลงโดยการแปลงแปลง (translate, rotate, scale, skew) แต่ละเมธอดรับออบเจกต์ Paint ที่กำหนดสี, รูปแบบ, ความหนาเส้น, เอฟเฟกต์ และการลบรอบหยาว
| เมธอด Canvas | วัตถุประสงค์ | พารามิเตอร์สำคัญของ Paint |
|---|---|---|
| drawCircle | วาดวงกลม | centerX, centerY, radius, paint |
| drawRect | วาดสี่เหลี่ยมมุมฉาก | left, top, right, bottom, paint |
| drawLine | วาดเส้น | startX, startY, endX, endY, paint |
| drawPath | วาดเส้นทางตามอำเอง | path, paint |
| drawBitmap | วาดรูบิตแมพ | bitmap, srcRect, dstRect, paint |
| drawText | วาดข้อความ | text, x, y, paint |
| drawArc | วาดส่วนโค้งหรือเซกเตอร์ | ovalRect, startAngle, sweepAngle, useCenter, paint |
| drawOval | วาดรูปไข่ | ovalRect, paint |
Paint กำหนดรูปแบบการวาด: สี (setColor), ความหนาเส้น (setStrokeWidth), รูปแบบการเติม (setStyle — FILL, STROKE, FILL_AND_STROKE), การลบรอบหยาว (setAntiAlias), เอฟเฟกต์ (setShadowLayer, setMaskFilter) และกราเดียนต์ (setShader) ควรสร้างและกำหนดค่า Paint หนึ่งครั้งในคอนสตรักเตอร์ของ View, ไม่ใช่ใน onDraw, เพื่อหลีกเลี่ยงการสร้างออบเจกต์ในวงจรการวาดรูป
Canvas ใช้ระบบพิกัดที่แกน X ชี้ไปทางขวา และแกน Y ชี้ไปทางล่าง จุดกำเนิด (0, 0) คือมุมบนซ้ายของ View ก่อนเรียกเมธอด draw, นักพัฒนาสามารถเปลี่ยนระบบพิกัดได้ผ่าน canvas.translate(dx, dy), canvas.rotate(degrees, px, py), canvas.scale(sx, sy, px, py) การวาดรูปทั้งหมดต่อไปจะดำเนินการในระบบที่แปลงแปลงแล้ว
onDraw ทำงานบนเมนไธร์สรีด UI, และระยะเวลาของมันส่งผลโดยตรงต่ออัตราเฟรม (fps) เพื่อรักษา 60 fps, เมธอด onDraw ต้องสมบูรณ์ภายใน 16 มิลลิวินาที ความช้าใดๆ อาจนำไปสู่การตกเฟรม (jank), ซึ่งมองเห็นได้เป็นการกระตุกของแอนิเมชันหรือความล่าในการเลื่อนไฟล์
กฎหลัก สำหรับการเพิ่มประสิทธิภาพ onDraw ได้แก่: 1) อย่าสร้างออบเจกต์ภายใน onDraw — ออบเจกต์ Paint, Path, Rect ทั้งหมดควรเป็นฟิลด์ของคลาส; 2) อย่าจัดสรรหหน่วยความจำ (new) — สิ่งนี้ทำให้เกิดการเก็บขยะ (GC) และการตกเฟรม; 3) แคชเอฟเฟกต์ที่ซับซ้อนใน Bitmap ผ่าน Bitmap.createBitmap และวาด Bitmap ที่เตรียมแล้ว; 4) อย่าทำการ I/O หรือโหลดทรัพยากร
clipRect จำกัดพื้นที่การวาดรูปให้อยู่ภายในสี่เหลี่ยมมุมฉากที่ระบุ ใช้เพื่อป้องกันไม่ให้ Canvas เสียเวลากับการวาดพิกเซลนอกพื้นที่ที่มองเห็น มีประสิทธิภาพโดยเฉพาะเมื่อเลื่อนรายการขนาดใหญ่หรือซูม: ตั้ง clipRect เป็นพื้นที่ที่มองเห็น (dirty rect), และ Canvas จะวาดเฉพาะสิ่งที่มองเห็น
canvas.save() บันทึกสถานะปัจจุบันของ Canvas (การแปลงแปลงและ clip) ไว้ในสแต็ก canvas.restore() คืนค่าสถานะที่บันทึกไว้ล่าสุด จำเป็นสำหรับการแปลงแปลงชั่วคราว: บันทึกสถานะ, ใช้ translate/rotate, วาด, แล้วคืนค่า save และ restore เบาและไม่มีผลต่อประสิทธิภาพเมื่อใช้อย่างสมเหตุสมผล (น้อยกว่า 10–15 ระดับการซ้อนกัน)
invalidate() เป็นเมธอดของ View ที่แจ้งให้ระบบทราบว่าเนื้อหาของ View ล้าสมัยและต้องการวาดใหม่ การเรียก invalidate จะเริ่มวงจร draw ใหม่ซึ่ง onDraw จะถูกเรียก invalidate ต้องเรียกจาก UI เมนไธร์สรีด (เมนไธร์สรีดหลัก) เท่านั้น สำหรับการเรียกจากเมนไธร์สรีดพื้นหลัง ให้ใช้ postInvalidate
postInvalidate() เป็นเมธอดที่ปลอดภัยจากเมนไธร์สรีดของ invalidate ที่ส่งคำขอวาดใหม่ไปยังคิวของเมนไธร์สรีดหลักผ่าน Handler ใช้ในงานพื้นหลังเมื่อ View ต้องการอัพเดตหลังจากการโหลดข้อมูลหรือการคำนวณสำเร็จ สามารถใช้ร่วมกับ runOnUiThread สำหรับการซังโครไนซ์กับ UI
สำหรับ การวาดใหม่บางส่วน, ใช้ invalidate(Rect dirty) หรือ invalidate(int l, int t, int r, int b) สิ่งนี้บอกระบบว่าเฉพาะพื้นที่ที่ระบุเท่านั้นที่เปลี่ยนแปลง, ไม่ใช่ View ทั้งหมด Android ปรับปรุงการวาดรูปโดยการวาดเฉพาะ dirty rect, ซึ่งช่วยเพิ่มประสิทธิภาพสำหรับแอนิเมชันที่มีการอัพเดตบางส่วน
มาดูตัวอย่างที่ใช้ได้จริงของการเปิดเพิ่ม onDraw เพื่อสร้าง View ที่กำหนดเองใน Android โดยใช้ Kotlin
ตัวอย่างนี้สร้างตัวชี้ความคืบหน้าแบบวงกลมพร้อมการเติมแบบแอนิเมชัน onDraw วาดวงกลมสองวง: วงกลมพื้นหลังสีเทาและวงกลมเติมสีที่มีส่วนโค้งกำหนดโดยความคืบหน้าปัจจุบัน การเรียก invalidate ใน setProgress ช่วยให้มั่นใจได้ว่าจะมีการวาดใหม่เมื่อค่าเปลี่ยนไป
class CircularProgressView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val bgPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.parseColor("#E0E0E0")
isAntiAlias = true
}
private val progressPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.BLUE
isAntiAlias = true
strokeCap = Paint.Cap.ROUND
}
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = minOf(width, height) - 24f
val oval = RectF(12f, 12f,
12f + size, 12f + size)
canvas.drawArc(oval, 270f, 360f, false, bgPaint)
canvas.drawArc(oval, 270f,
progress * 360f,
false, progressPaint)
}
}
ตัวอย่างนี้วาดสี่เหลี่ยมมุมฉากที่มีกราเดียนต์และข้อความอยู่ด้านบน กราเดียนต์ถูกสร้างผ่าน LinearGradient (Shader), และข้อความผ่าน drawText ออบเจกต์ Paint ทั้งสองถูกสร้างขึ้นหนึ่งครั้งในคอนสตรักเตอร์, ในขณะที่ onDraw เพียงแค่อัพเดตขนาดของสี่เหลี่ยมมุมฉากตามความกว้างปัจจุบันของ View
class GradientTextView(context: Context)
: View(context) {
private val shaderPaint = Paint().apply {
isAntiAlias = true
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 48f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val gradient = LinearGradient(
0f, 0f, width.toFloat(), 0f,
Color.parseColor("#FF6B35"),
Color.parseColor("#FF0040"),
Shader.TileMode.CLAMP
)
shaderPaint.shader = gradient
canvas.drawRect(0f, 0f,
width.toFloat(),
height.toFloat(), shaderPaint)
val xCenter = width / 2f
val yCenter = height / 2f
val yOffset = (textPaint.descent() +
textPaint.ascent()) / 2f
canvas.drawText("Hello from Canvas",
xCenter, yCenter - yOffset,
textPaint)
}
}
ตัวอย่างนี้สร้างนาฬิกาแอนิเมชันที่อัพเดตการวาดทุก 1000 มิลลิวินาที postDelayed ภายใน onDraw เรียก invalidate หลังจากหนึ่งวินาที, สร้างวงจรการวาดใหม่ไม่สิ้นสุด เข็มนาฬิกาถูกวาดผ่าน drawLine โดยมีความยาวและความหนาต่างกัน
class AnalogClockView(context: Context)
: View(context) {
private val handPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 20f
val now = Calendar.getInstance()
val hours = now.get(Calendar.HOUR) % 12
val minutes = now.get(Calendar.MINUTE)
val seconds = now.get(Calendar.SECOND)
val secondAngle = Math.toRadians(
seconds * 6f - 90f)
handPaint.strokeWidth = 2f
handPaint.color = Color.RED
canvas.drawLine(cx, cy,
cx + cos(secondAngle) * radius * 0.8f,
cy + sin(secondAngle) * radius * 0.8f,
handPaint)
postDelayed({ invalidate() }, 1000)
}
}
คำถามที่พบบ่อย
ไม่, ไม่ควรเรียก onDraw โดยตรง หากต้องการเรียกวาดใหม่, ให้ใช้ invalidate() (จาก UI thread) หรือ postInvalidate() (จากเมนไธร์สรีดพื้นหลัง) ระบบจะตัดสินใจเองว่าเมื่อไหร่ที่จะเรียก onDraw และจะปรับปรุงการวาดรูปโดยการรวมการเรียก invalidate หลายครั้งไว้ในการผ่านครั้งเดียว
สาเหตุ: View ถูกซ่อน (setVisibility(GONE) หรือ INVISIBLE); View ไม่ได้ถูกเพิ่มในลำดับชั้นของ Window; ขนาด View เท่ากับ 0 (ความกว้างหรือความสูง = 0); มีการเรียก setWillNotDraw(true) (ค่าเริ่มต้นเป็น true สำหรับ ViewGroup) ตรวจสอบแต่ละข้อเหล่านี้เมื่อแก้ไขปัญหา
ที่ 60 fps, onDraw สามารถถูกเรียกได้มากถึง 60 ครั้งต่อวินาที ที่ 120 fps (บนอุปกรณ์ที่มีหน้าจอ 120 Hz) — มากถึง 120 ครั้ง แต่ละครั้งต้องสมบูรณ์ภายในประมาณ 8–16 มิลลิวินาที ระหว่างการแอนิเมชันผ่าน ObjectAnimator หรือ ValueAnimator, ความถี่จะถูกกำหนดโดยจำนวนเฟรมแอนิเมชัน
onDraw มีหน้าที่วาดเนื้อหาของ View เอง dispatchDraw มีหน้าที่วาด View ย่อย (ใน ViewGroup) dispatchDraw ถูกเรียกหลังจาก onDraw และเดินทางแบบทอดซ้ำผ่านองค์ประกอบย่อยทั้งหมด สำหรับ View ทั่วไป (ไม่ใช่ ViewGroup), dispatchDraw จะว่างเปล่า
ได้, สามารถสร้าง Canvas สำหรับ Bitmap ผ่าน Canvas(bitmap) และวาดบนมันในเมนไธร์สรีดใดๆ ใช้สำหรับการเรนเดอร์กราฟิกส์ที่ซับซ้อนล่วงหน้าในพื้นหลัง อย่างไรก็ตาม, Canvas ที่ส่งต่อไปยัง onDraw เป็นของระบบ และสามารถใช้งานได้เฉพาะบน UI thread ภายในเมธอด onDraw
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม