web analytics

รู้จักกับ Custom Painting ใน Flutter ตอนที่ 1

สวัสดีครับ พบกันอีกครั้งกับผม benznest ครับ บล็อกนี้ผมจะพามาลองเล่นสิ่งที่เรียกว่า Painting ใน Flutter ครับ มันคือการวาดรูปทรงที่เราต้องการ สามารถนำไปประยุกต์ใช้ในแอปของเราได้หลายอย่าง มาลองเล่นกันว่ามันทำอะไรได้บ้างนะ

เริ่มต้น

เราจะมาดูการทำรูปทรงแบบพื้นฐานกันก่อน ซึ่ง Flutter ได้เตรียม Widget ที่สำเร็จรูปมาให้เราใช้งานแล้ว widget นั้นก็คือ Container

เริ่มต้นที่รูปสี่เหลี่ยม container สามารถจบงานได้เลย เพราะมันสี่เหลี่ยมอยู่แล้ว

ถ้าเป็นรูปวงกลมล่ะ Container ก็สามารถทำได้ โดยการใช้ ShapeDecoration และกำหนด shape เป็น CircleBorder ก็ได้วงกลมแล้ว

นอกจากนั้น ShapeDecoration ก็มีลูกเล่นอื่นๆอีก เช่น RoundedRectangleBorder , ContinuousRectangleBorder สำหรับการทำสี่เหลี่ยมขอบมน

อีกอันก็คือ BeveledRectangleBorder คือการตัดมุมออกไปเลย

หรือแม้แต่การหมุนรูปทรงให้ได้ตามต้องการเราก็สามารถใช้ Widget ที่ชื่อว่า Transform.rotate มาช่วยได้ เพียงกำหนดค่ามุมให้มัน

จะเห็นว่า Widget พื้นฐานอย่าง Container และใช้ร่วมกับ Transform widget จะสามารถทำรูปทรงแบบง่ายๆได้พอสมควร เช่น สี่เหลี่ยม วงกลม แต่ที่สิ่งที่ container ทำได้ก็เป็นเพียงรูปทรงแบบพื้นฐาน เป็นรุปเดี่ยวเท่านั้น แน่นอนว่ามันไม่สามารถทำรูปทรงที่ซับซ้อนได้ และการวาดรูปทรงหลายๆอันทับซ้อนก็ดูจะเป็นเรื่องยากลำบาก

Canvas

หากเราต้องการสร้างรูปทรงที่ซับซ้อนขึ้น เราจำเป็นต้องใช้สิ่งที่เรียกว่า Custom Painter ซึ่งมันก็เหมือนกับชุดเครื่องมือสำหรับการเขียนหรือวาดรูปทรงต่างๆลงในกระดาษ แต่ก่อนอื่นมาดูเจ้ากระดาษกันก่อนว่ามันทำงานอย่างไร

กระดาษในที่นี้ก็คือ Canvas นั่นเอง เป็นพื้นที่สำหรับวาด โดยมันจะเป็นสี่เหลี่ยม การกำหนดพิกัดบน canvas นี้ เป็น x,y โดยมุมบนซ้ายคือ x=0,y=0 และ x ขยับไปทางขวาตามขนาดความกว้าง ในขณะเดียวกัน y ก็ขยับลงด้านล่างตามความสูง ดังนั้น จุดกึ่งกลาง canvas นี้ ก็คือ (x = ความกว้าง/2 , y = ความสูง/2) และจุดมุมขวาล่างของ canvas คือ (x = ความกว้าง, y = ความสูง)

การวาดรูปทรงลงบน canvas เราจะต้องกำหนดพิกัดลงไปก่อน เช่น เราต้องการวาดวงกลมรัศมี 30 ตรงกลาง เราก็ต้องรู้พิกัดก่อนว่าตรงกลางของ canvas คือพิกัดอะไร แล้วจึงสั่งวาดวงกลมขนาดตามต้องการลงไปที่พิกัดนั้น เป็นเหตุผลว่าทำไมจึงควรรู้วิธีคำนวณพิกัดใน canvas จากขนาดของมัน

Custom Painter

มาลองวาดรูปโดยใช้ Custom Painter กัน มันก็คือ class สำหรับนิยามการวาด ว่าต้องวาดอะไรบ้าง สีใช้อะไร ขนาดเท่าไหร่ ในที่นี้ผมจะลองวาดรูปวงกลมแบบง่ายๆก่อน วิธีการคือสร้าง class ของเราขึ้นมาแล้ว extends CustomPainter ผมขอใช้ชื่อ class ของผมนี้ว่า MyCirclePainter

สิ่งที่เราต้อง implement มี 2 ส่วนคือ
1. method paint จะมี argument 2 ตัวคือ canvas หรือก็คือกระดาษของเรา และ size ขนาดของกระดาษ
2. shouldRepaint เป็น method ที่ให้ return ว่าควรจะเรียก paint ใหม่หรือไม่ เช่น มีการเปลี่ยนสีก็สั่งวาดใหม่ซะ

จากนั้นผมเพิ่ม ตัวแปรค่าสี Color มา 1 อัน เพื่อให้รับค่ามาจากภายนอก ว่าจะให้วาดวงกลมสีอะไร และที่ shouldRepaint ผมก็ตรวจสอบว่า ค่าสีมีการเปลี่ยนแปลงหรือไม่ ถ้าเปลี่ยนก็จะ return true เพื่อสั่งวาดใหม่

Paint method

ทีนี้ เราจะสนใจที่พระเอกของงานนี้ คือ method paint() มันคือการกำหนดว่าจะให้วาดอะไรลงไปบ้าง เราจะเริ่มจากวาดวงกลมก่อน คำสั่งคือ canvas.drawCircle แล้วกำหนดพิกัดและรัศมีให้มัน ส่วนสีเราจะกำหนดผ่าน class ที่ชื่อว่า Paint ที่เป็นเสมือนปากกา กล่าวคือ canvas สั่งวาดรูปทรง ส่วนพวกสี style จะกำหนดที่ Paint แล้วค่อยเอามาใช้ร่วมกันตอนสั่ง draw นั่นเอง

จากนั้นที่หน้า UI ก็ใช้ widget CustomPaint (คนละอันกับ CustomPainter นะ) กำหนด size ในที่นี้คือขนาดของกระดาษนั่นเอง และกำหนด painter เป็น Custom Painter ของเราเอง ในที่นี้ของผมคือ MyCirclePainter พร้อมกำหนดสี parameter เป็นสีเขียว

ได้วงกลมแล้ว

หรือเราจะใช้ style เป็น PaintingStyle.stroke แล้วกำหนด strokeWidth ก็ได้เช่นกัน

Drawing

จะเห็นว่าไอเดียของการทำ CustomPainter มี 2 ส่วน ก็คือ การ implement method paint() ใน class ที่ extends CustomPainter เป็นการนิยามว่าให้วาดอะไร และส่วนที่สอง คือการเรียกใช้ widget CustomPainter ในหน้า UI โดยผมจะไม่เน้นการเรียกใช้ widget CustomPaint ที่ UI นะ เพราะมันก็เหมือนเดิม เปลี่ยนแค่ size กับ painter เท่านั้นเอง ผมจะเน้นที่ method paint ที่ CustomPainter เป็นหลักมากกว่า

กลับมาที่ canvas คำสั่งพื้นฐานหลักๆก็คือการสั่ง draw ลง canvas ซึ่งก็มีคำสั่งอยู่หลายตัวมาก เมื่อกี้เราลองเล่น drawCircle ไปแล้ว ลองมาเล่นตัวอื่นๆกัน

Rect

การวาดรูปสี่เหลี่ยม ใช้คำสั่ง drawRect ซึ่งจะรับ parameter คือ class ที่ชื่อว่า Rect (ย่อมาจาก Rectangle) โดย Rect มีหลักการทำงานที่ง่ายมาก มันจะเก็บค่าพิกัดบนซ้าย และพิกัดล่างขวาเอาไว้ (Left-Top , Right-Bottom) เพื่อจะได้รู้ว่า สี่เหลี่ยมนี้อยู่ตรงไหนใน canvas และวิธีการกำหนดพิกัดมีอยู่ 4 วิธีตาม constructor ของมัน

ในที่นี้ผมจะกำหนดพิกัดให้กับ Rect โดยใช้วิธี fromLTRB ก็คือ กำหนดค่าพิกัดมุมให้มันโดยตรง โดยผมกำหนดให้มีขนาดเท่ากับขนาด canvas เลย คือ (0,0,w,h) แล้วเรียก canvas.drawRect เพื่อวาด

เท่านี้ก็ได้สี่เหลี่ยมแล้ว ซึ่งขนาดก็ตามที่เรากำหนดในหน้า UI ที่ widget CustomPaint

Oval

ลอง drawOval หรือวาดวงรี ก็ใช้ Rect เหมือนกัน เหมือนกับการวาดสี่เหลี่ยม

RRect

การวาดสี่เหลี่ยมแบบมุมมน จะใช้ class RRect (ย่อมาจาก Rounded Rectangle) ก็คือการเอา Rect มาเพิ่มเติมเรื่อง radius นั่นเอง ว่าต้องการให้มุมมันโค้งมนแค่ไหน

DRRect

อีกอันที่น่าสนใจ คือ DRRect ย่อมาจาก Difference Rounded Rectangle มันคือการเอา RRect สองตัว เรียกว่า inner กับ outer มาซ้อนตัดกัน (intersection)

Line

ไม่ใช่แอปแชทนะ มันคือการลากเส้นเนี้ยแหละ หรือ drawLine วิธีการคือกำหนด พิกัด (Offset) 2 จุด ว่าจะให้ลากจากจุดไหนไปจุดไหน โดยสามารถกำหนดขนาดของเส้นได้ที่ stokeWidth ของ Paint

วาดรูปเล่นกัน

พอจะเข้าใจไอเดียการใช้งาน canvas คร่าวๆกันแล้วใช่ไหมครับ ว่าการวาดรูปทรงแต่ละอันต้องใช้อะไรบ้าง เพื่อความเข้าใจที่มากขึ้นสำหรับเรื่องนี้ เรามาลองวาดรูปเล่นกันเถอะ

แถ่น แถ๊น !

และนี่ คือรูปที่ผมใช้โปรแกรม Paint วาดแบบร่างง่ายๆออกมา โดยผมกำหนดอัตราส่วน ความกว้าง 2 : ความสูง 3 กล่าวคือ ขนาด canvas จะเท่าไหร่ก็ได้ ขออัตราส่วน 2:3 ก็พอ ส่วนอัตราส่วนรูปเดี๋ยวอธิบายอีกทีนะ มันจะมั่วๆหน่อย คือทำยังไงก็ได้ให้วาดออกมาเป็นเจ้านี่อ่ะ

MyHumanPainter

ก่อนอื่นเลยผมสร้าง class MyHumanPainter สำหรับแสดง Custom Painter รูปคนของผม ขอกำหนดไม่มี parameter อะไรเลย จะแสดงอย่างเดียว ดังนั้น shouldRepaint ผมจะ return false ไปเลย ส่วน paint() เดี๋ยวเราไปคำนวณพิกัดก่อนแล้วค่อยมาเขียนอีกที

ขอไปที่ส่วนหน้า UI ก่อน เพื่อกำหนด widget CustomPaint ผมกำหนด size ขนาด 200 x 300 และใส่พื้นหลังเทาหน่อยๆ จะได้เห็นขนาด canvas ชัดๆ

วาดส่วนหัว

โดยโจทย์รูปนี้ ผมขอแบ่งแบบหยาบๆ เป็น 3 ส่วน คือ หัว,ตัว,ขา ดังนั้นแต่ละส่วน คือ ความสูง ส่วนละ 100 เพราะผมกำหนด canvas ไปว่าความสูง 300

เราจะเริ่มจากส่วนหัวก่อนนะ ส่วนหัวผมแบ่งออกเป็น 8 ส่วน คือ ตามความกว้าง 4 ส่วน และตามความสูง 2 ส่วน

โดยส่วนหัวจะเริ่มตรงกลางของส่วนที่ 1 นี้และรัศมีเท่ากับ 1/4 ของความกว้างของ canvas หรือ 1/6 ของความสูงก็ได้ ดังนั้นพิกัดกึ่งกลางวงกลมสำหรับวาดหัว ก็คือ (W/2 , H/6)

เริ่มจากขอกำหนดตัวแปร W = ความกว้าง , H = ความสูง
เราคำนวณตำแหน่งพิกัดกึ่งกลางวงกลม และรัศมีแล้วก็สั่งวาดได้เลย

ได้ส่วนหัวมาแล้ว

วาดส่วนลำตัว

ส่วนลำตัว ผมแบ่งตามความกว้างออกเป็น 8 ส่วน โดยลำตัวจะใช้ 2/8 ของความกว้าง อยู่กึ่งกลาง ส่วนความสูงก็คือใช้ 1/3 ของความสูง canvas

ลำตัวคือสี่เหลี่ยม การวาดสี่เหลี่ยมจะใช้ class Rect ใช่ไหม ดังนั้นเราต้องรู้พิกัด 2 จุดของสี่เหลี่ยม คือ มุมบนซ้าย และ มุมล่างขวา หรืออีกวิธีคือรูปมุมบนซ้ายและขนาดความกว้างความสูงก็ได้เช่นกัน

ผมคำนวณแบบง่ายๆ
พิกัดบนซ้าย คือ 3 * w / 8 , h / 3
พิกัดล่างขวา คือ 5 * w / 8 , 2 * h / 3

จากนั้นก็วาดลง canvas ต่อจากส่วนหัวได้เลย

ได้ส่วนลำตัวมาแล้ว

วาดส่วนแขน

สำหรับส่วนแขน ส่วนนี้จะเป็นการลากเส้น โดยผมใช้วิธีแบ่งตามแนวความกว้าง เป็น 10 ส่วน ดังนั้น แต่ละส่วนคือ 1/10 ของความกว้าง และหัวใหล่ของแขนซ้ายคือ มุมบนซ้ายของส่วนที่ 5 จากซ้าย หรือคือพิกัด 4 * w / 10 และปลายแขนซ้ายคือ มุมล่างขวาของส่วนที่ 2 พิกัดคือ 2 * w / 10 อธิบายแล้วอาจจะงง ลองดูรูปนะ แขนขวาก็ใช้หลักการเดียวกันแค่ Flip ด้านนั่นเอง

คำนวณพิกัดได้แล้ว สามารถ drawLine ออกมาเลย ทั้งแขนซ้ายและแขนขวา สำหรับขนาดเส้นจะอิงขนาดเส้นตามความกว้างของ canvas ไม่อย่างนั้นหากเรา hard code ค่าไว้ แล้ว canvas ใหญ่แขนจะเล็กนิดเดียว ดังนั้นผมจะใช้ขนาดเส้นประมาณ 1/30 ของความกว้าง

ได้ส่วนแขนแล้ว

วาดส่วนขา

ผมคิดว่าถึงตรงนี้ น่าจะพอเห็นไอเดีของการวาดแล้วใช่ครับ เพียงคำนวณพิกัดตามขนาด canvas นั่นเอง สำหรับส่วนขาผมใช้หลักการคล้ายลำตัว เพียงแต่เพิ่มให้ขาซ้ายขยับมาทางขวานิดนึง W/16 และขาซ้ายขยับมาทางซ้าย W/16 ขนาดเส้นใช้ W/25 ก็จะใหญ่กว่าแขนเล็กน้อย ส่วนเท้าก็ขนาด W/16 จากขาไปทางซ้ายและขวา

คำนวณพิกัดทั้งขาซ้าย เท้าซ้าย ขาขวา เท้าขวา แล้ววาดเส้นขนาด W/25

ได้ส่วนขาแล้ว

วาดส่วนตา

ส่วนสุดท้าย คือรายละเอียดใบหน้า มาวาดส่วนตากันก่อน ผมแบ่งคร่าวๆ จากส่วนหัว จะเห็นว่ามี 4 ส่วนใหญ่ๆ ขนาดกล่องละ W/4 * H/6 แล้วก็เอากล่องนี้มาแบ่งเป็น 4 ส่วนเท่าๆกัน และแต่ละส่วนก็แบ่งเป็น 9 ส่วนย่อยๆอีกตามรูป ดวงตาจะมีขนาด W/24

วาดดวงตาตามพิกัด ทั้งตาซ้าย และตาขวา โดยดวงตาผมขอใช้เป็นสีขาวแทนนะ

ได้ส่วนตาแล้ว

วาดส่วนปาก

ส่วนปาก ผมใช้ความกว้างของปากตามขนาดของจุดศูนย์กลางตาซ้ายกับตาขวา ส่วนตำแหน่งตามความสูง ผมแบ่งโดยใช้ H/48 ขยับจากกลางใบหน้าลงด้านล่าง 3 * H / 48

ได้พิกัดแล้ว ก็ drawLine ของปากได้เลย

ได้แล้วเจ้ามนุษย์

ลองใช้งาน

ลองนำ MyHumanPainter ไปใช้งานในขนาดต่างๆ ขอแค่อัตราส่วน 2:3 ก็พอ

ได้แล้ว เย้

สรุป

จบแล้วครับ ขอบคุณที่ติดตามอ่านจนจบนะครับ สำหรับในบล็อกเรื่อง Custom Painting ตอนที่ 1 นี้ เราได้รู้จักกับการวาดรูปทรงจาก widget สำเร็จรูปอย่าง container จนถึงการทำ class Custom Painter ของเราเอง เพื่อวาดรูปทรงลงใน canvas และเราก็ได้รู้จักกับ drawing ทั้งวาดเส้น วงกลม การใช้ Rect RRect ต่างๆ จนถึงตอนท้าย ได้ลองวาดรูปคนเล่นๆกัน น่าจะทำให้เห็นหลักการทำงาน การใช้งาน Custom Pianting นะครับ

สำหรับตอนถัดไป เราจะลองเล่น Custom Painter กันต่อครับ ยังไม่หมดเท่านี้แน่นอน

Source code สำหรับโปรเจควาดรูปคน สามารถดู code ได้ที่ DartPad ครับ
https://dartpad.dev/0556195d4c830c1ff627867e63a54eff

Credit
https://www.raywenderlich.com/7560981-drawing-custom-shapes-with-custompainter-in-flutter

อ่านต่อ ตอนที่ 2