มุมมองแบบกำหนดเองเป็นพื้นฐานของอินเทอร์เฟซที่ไม่ซ้ำใครของแอปพลิเคชันมือถือใดๆ มุมมองแบบกำหนดเองในแอปพลิเคชันมือถือช่วยให้คุณสร้างกราฟิก แอนิเมชัน และองค์ประกอบของคุณเองที่ไม่มีในคอมโพเนนต์มาตรฐาน ตาม Apple Developer, 2025 drawRect ยังคงเป็นเมธอดหลักของการวาดแบบกำหนดเองในการพัฒนามือถือ iOS บน Android onDraw กับ Canvas ทำหน้าที่เดียวกัน และใน Flutter — CustomPainter
ประเด็นสำคัญ
มุมมองแบบกำหนดเองคือคอมโพเนนต์อินเทอร์เฟซที่ผู้ใช้กำหนดซึ่งแทนที่เมธอดการวาดและการวัดมาตรฐานเพื่อสร้างกราฟิกที่ไม่ซ้ำใคร กราฟ แอนิเมชัน แผนภาพ องค์ประกอบแชทแบบกำหนดเอง และหน้ากาก AR — นี่คือสถานการณ์การใช้งานทั่วไปของมุมมองแบบกำหนดเองในแอปพลิเคชันมือถือ คอมโพเนนต์ UI มาตรฐาน (Button, TextView, Label) ครอบคลุม 80% ของงาน แต่ 20% ที่เหลือต้องการการควบคุมการวาดอย่างสมบูรณ์ในการพัฒนามือถือ การวาดในการพัฒนามือถือเป็นทักษะที่แยกนักพัฒนา junior ออกจาก senior
มุมมองแบบกำหนดเองในแอปพลิเคชันมือถือจำเป็นสำหรับ กราฟิก 2D ที่ไม่ซ้ำใคร แอนิเมชันที่ซับซ้อน แผนภูมิ และการแสดงภาพข้อมูล หาก UIKit มาตรฐานไม่มีองค์ประกอบที่ต้องการ — จะสร้างมุมมองแบบกำหนดเอง ในแอปพลิเคชันมือถือ มุมมองแบบกำหนดเองใช้สำหรับแถบความคืบหน้า ปุ่มแบบกำหนดเอง การเติมสีแบบเกรเดียนต์ ไอคอนเคลื่อนไหว และแคนวาสสำหรับวาดด้วยนิ้ว สิ่งสำคัญคือไม่ใช้มากเกินไป — สำหรับสไตล์ง่ายๆ เพียงกำหนดค่าคอมโพเนนต์มาตรฐานผ่าน Appearance API ก็เพียงพอ
บน iOS มุมมองแบบกำหนดเองถูกสร้างขึ้นโดยการสืบทอด UIView และแทนที่เมธอด drawRect drawRect ถูกเรียกในการเรนเดอร์ครั้งแรกและหลังจากการเรียก setNeedsDisplay() แต่ละครั้ง ภายใน drawRect นักพัฒนาได้รับ CGContext ผ่าน UIGraphicsGetCurrentContext() — แคนวาสสำหรับเส้น รูปร่าง เกรเดียนต์ และข้อความ Core Graphics (Quartz 2D) เป็นเอนจินเรนเดอร์ระดับต่ำบน CPU ที่ให้การควบคุมทุกพิกเซลอย่างสมบูรณ์ UIBezierPath เป็นตัวห่อหุ้มที่สะดวกสำหรับการสร้างเส้นทางด้วยไวยากรณ์ที่กระชับ
class CircleView: UIView {
private var fillColor: UIColor = .systemBlue
override func drawRect(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setFillColor(fillColor.cgColor)
ctx.addEllipse(in: bounds.insetBy(dx: 4, dy: 4))
ctx.drawPath(using: .fill)
}
func updateColor(_ newColor: UIColor) {
fillColor = newColor
setNeedsDisplay()
}
}
Core Graphics ให้ CGContext — แคนวาสสำหรับการดำเนินการวาดทั้งหมด CGGradient สร้างเกรเดียนต์เชิงเส้นและรัศมี ด้วยการสนับสนุน CGColorSpace (sRGB, Display P3) การแปลงดำเนินการผ่าน CGAffineTransform — เมทริกซ์ 3x3 สำหรับการหมุน การปรับขนาด และการเลื่อน CGContextTranslateCTM, CGContextRotateCTM และ CGContextScaleCTM เปลี่ยนแปลงระบบพิกัด Core Graphics ทำงานบน CPU ดังนั้นกราฟิกที่ซับซ้อนสามารถโหลดโปรเซสเซอร์ — ใช้ Metal สำหรับการเร่ง GPU
UIBezierPath เป็นตัวห่อหุ้มเชิงวัตถุเหนือ CGPath เมธอด addArc(withCenter:radius:startAngle:endAngle:clockwise:) สร้างส่วนโค้งและวงกลม addCurve สร้างเส้นโค้งเบซิเอร์ เส้นทางสนับสนุนการดำเนินการรวม การตัด และการลบผ่าน addClip UIBezierPath สะดวกสำหรับมาสก์แบบกำหนดเอง (maskView) เส้นทางตัด และรูปร่างที่ซับซ้อน — ดาว หัวใจ ไอคอนแบบกำหนดเอง
บน Android มุมมองแบบกำหนดเองขยาย View ด้วยการแทนที่ onDraw(Canvas) Canvas มีเมธอดการวาดทั้งหมดในการพัฒนามือถือ: drawCircle, drawRect, drawLine, drawText, drawBitmap, drawPath และ drawRoundRect Paint กำหนดค่าพารามิเตอร์ — สี ความหนาของเส้น สไตล์ (FILL, STROKE, FILL_AND_STROKE) การลดรอยหยัก (isAntiAlias) และขนาดข้อความ onDraw ถูกเรียกทุกครั้งที่ invalidate() และไม่ควรมีการดำเนินการหนัก — การคำนวณทั้งหมดถูกแคช
class CustomCircleView(context: Context, attrs: AttributeSet?)
: View(context, attrs) {
private val paint = Paint().apply {
style = Paint.Style.FILL
color = Color.BLUE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 4f
canvas.drawCircle(cx, cy, radius, paint)
}
}
Path บน Android เทียบเท่ากับ UIBezierPath: addCircle, addRect, addOval, lineTo, cubicTo และ quadTo PathMeasure คำนวณความยาวเส้นทางและพิกัดที่จุดใดๆ — มีประโยชน์สำหรับการเคลื่อนไหวของวัตถุตามเส้นทาง สำหรับแอนิเมชัน ใช้ ValueAnimator กับ invalidate() ใน callback ตัวปรับแต่งสามารถเปลี่ยนสี รัศมี ตำแหน่ง — คุณสมบัติใดๆ ที่ส่งผลต่อการวาด มุมมองแบบกำหนดเองบน Android ยังสนับสนุน StateListDrawable และ animated-vector สำหรับแอนิเมชันผ่าน XML
เกรเดียนต์บน Android ถูกตั้งค่าผ่าน Shader — LinearGradient, RadialGradient และ SweepGradient Shader ถูกกำหนดให้กับ Paint ผ่าน paint.setShader(shader) LinearGradient สร้างเกรเดียนต์เชิงเส้นระหว่างสองจุดพร้อมตำแหน่งสีที่ไม่บังคับ RadialGradient — รัศมีจากศูนย์กลางถึงขอบ SweepGradient — รูปกรวยรอบจุดศูนย์กลาง Shader.TileMode (CLAMP, REPEAT, MIRROR) กำหนดพฤติกรรมนอกขอบเขตพื้นที่
Flutter ใช้ CustomPainter — คลาสที่มีเมธอด paint(Canvas canvas, Size size) และ shouldRepaint Canvas ใน Flutter ให้เมธอดเดียวกันกับใน Android: drawCircle, drawRect, drawLine, drawPath, drawParagraph และ drawImage Paint ใน Flutter กำหนดค่าสี ความหนา สไตล์ และการลดรอยหยัก Flutter ไม่ใช้ drawRect หรือ onDraw — การวาดทั้งหมดผ่าน CustomPainter ซึ่งฝังอยู่ในวิดเจ็ต CustomPaint
class CirclePainter extends CustomPainter {
final Color color;
CirclePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final radius = size.shortestSide / 2 - 4;
canvas.drawCircle(size.center(Offset.zero), radius, paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
oldDelegate.color != color;
}
shouldRepaint กำหนดว่า CustomPainter จำเป็นต้องวาดใหม่หรือไม่ คืนค่า true เฉพาะเมื่อข้อมูลที่ส่งผลต่อการเรนเดอร์เปลี่ยนแปลง — สี ขนาด ข้อมูลแผนภูมิ RepaintBoundary แยกการวาดใหม่เป็นเลเยอร์แยก — ใช้สำหรับวิดเจ็ตที่เปลี่ยนแปลงน้อยครั้ง Flutter ไม่มี setNeedsDisplay หรือ invalidate — การวาดใหม่ถูกเรียกผ่าน setState หรือ ValueNotifier ที่ฟังการเปลี่ยนแปลง
Flutter สนับสนุน LinearGradient, RadialGradient และ SweepGradient — ชนิดเดียวกับใน Android Gradient สร้าง Shader สำหรับ Paint ผ่าน createShader(Rect bounds) การแปลงดำเนินการผ่าน canvas.rotate, canvas.scale, canvas.translate และ canvas.skew Flutter ใช้เมทริกซ์การแปลง 4x4 (Matrix4) สำหรับวิดเจ็ต Transform แอนิเมชันการวาดใน Flutter ทำด้วย AnimationController ร่วมกับ setState เพื่อวาด CustomPainter ใหม่
การวัดมุมมองแบบกำหนดเองที่ถูกต้องเป็นพื้นฐานของพฤติกรรมอินเทอร์เฟซที่คาดเดาได้ บน iOS ขนาดของมุมมองถูกกำหนดผ่าน sizeThatFits และ intrinsicContentSize sizeThatFits คืนค่าขนาดที่เหมาะสมที่สุดสำหรับข้อจำกัดที่กำหนด intrinsicContentSize — ขนาดธรรมชาติของเนื้อหาโดยไม่มีข้อจำกัดภายนอก บน Android การวัดถูกนำไปใช้ใน onMeasure ด้วยพารามิเตอร์ MeasureSpec (UNSPECIFIED, EXACTLY, AT_MOST) ใน Flutter ขนาดถูกส่งไปยัง paint(Canvas, Size) และสามารถคำนวณผ่าน computeDryLayout หรือ LayoutBuilder
intrinsicContentSize คืนค่า CGSize — ขนาดธรรมชาติของมุมมองตามเนื้อหา setContentHuggingPriority และ setContentCompressionResistancePriority ควบคุมพฤติกรรมระหว่างการยืดและการบีบ sizeThatFits ถูกเรียกโดยระบบเพื่อคำนวณขนาดใน Auto Layout สำหรับมุมมองแบบกำหนดเอง ให้แทนที่ intrinsicContentSize หากคอมโพเนนต์ของคุณมีขนาดธรรมชาติ — ตัวอย่างเช่น วงกลมควรเป็น 100x100 เสมอ
onMeasure รับ widthMeasureSpec และ heightMeasureSpec MeasureSpec ประกอบด้วยโหมดและขนาด: UNSPECIFIED (ไม่มีข้อจำกัด มุมมองเลือกขนาดของตัวเอง), EXACTLY (ขนาดที่แน่นอนจากพาเรนต์), AT_MOST (ขนาดสูงสุด) setMeasuredDimension(width, height) บันทึกผลการวัด เมื่อข้อมูลเปลี่ยนแปลง ให้เรียก requestLayout() เพื่อวัดใหม่ ViewGroup ที่ซ้อนกันต้องการการแทนที่ onLayout เพื่อจัดเรียงองค์ประกอบย่อย
| แพลตฟอร์ม | เมธอดการวัด | การเรียกเมื่อเปลี่ยนแปลง | ค่าที่คืน |
|---|---|---|---|
| iOS | sizeThatFits / intrinsicContentSize | setNeedsLayout / invalidateIntrinsicContentSize | CGSize |
| Android | onMeasure / setMeasuredDimension | requestLayout / invalidate | void (บันทึกขนาด) |
| Flutter | computeDryLayout | setState / LayoutBuilder | Size |
computeDryLayout เป็นเมธอด RenderObject สำหรับคำนวณขนาดโดยไม่ต้องมีรอบเลย์เอาต์เต็ม LayoutBuilder.getSize — ทางเลือกสำหรับการรับพื้นที่ว่างในเมธอด build ใน Flutter ขนาดแคนวาสถูกส่งไปยัง paint เป็น Size และไม่ต้องการการวัดแยก — CustomPainter วาดภายในขอบเขตที่ส่ง อย่างไรก็ตาม สำหรับ RenderObject แบบกำหนดเอง คุณต้องแทนที่ performLayout และ computeDryLayout
คำถามที่พบบ่อย
drawRect เป็นเมธอด UIView สำหรับการวาดแบบกำหนดเองผ่าน Core Graphics มันถูกเรียกในการเรนเดอร์ครั้งแรกของมุมมองและหลังจากการเรียก setNeedsDisplay() แต่ละครั้ง ใช้ CGContext ภายใน drawRect สำหรับเส้น รูปร่าง และข้อความ
สร้างคลาสที่ขยาย View แทนที่ onDraw(Canvas canvas) และใช้ Canvas API: drawCircle, drawRect, drawPath Paint กำหนดค่าสี ความหนา และสไตล์ สำหรับการอัปเดต ให้เรียก invalidate()
CustomPainter เป็นคลาส Flutter สำหรับการวาดแบบกำหนดเองบน Canvas ใช้เมธอด paint(Canvas, Size) และ shouldRepaint วางผ่านวิดเจ็ต CustomPaint Flutter ไม่มี drawRect — การวาดทั้งหมดผ่าน CustomPainter
บน iOS ใช้ sizeThatFits และ intrinsicContentSize บน Android — onMeasure กับ MeasureSpec ใน Flutter ขนาดถูกส่งไปยัง paint(Canvas, Size) หรือคำนวณผ่าน computeDryLayout แต่ละแพลตฟอร์มใช้เมธอดการวัดของตัวเอง
มุมมองแบบกำหนดเอง จำเป็นสำหรับกราฟิกที่ไม่ซ้ำใคร แอนิเมชัน แผนภูมิ และองค์ประกอบที่ไม่สามารถนำไปใช้กับ UIKit หรือ View มาตรฐาน สำหรับสไตล์ง่ายๆ เพียงกำหนดค่าคอมโพเนนต์มาตรฐาน — อย่าทำให้สถาปัตยกรรมซับซ้อนโดยไม่จำเป็น
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ