รอบนี้กลับมาดีไซน์กันอีกรอบใน Flutter และเนื่องจากเราจะสร้างเองหมดเลย ดังนั้นเตรียมใจว่าเราจะหนีเลขไม่พ้นอย่างแน่นอน หลักๆ มี 2 เรื่อง เมทริกซ์ กับ ตรีโกณ ลองเดากันก่อนได้ว่าส่วนไหนใช้อะไรบ้าง
เราจะมาทำนาฬิกา(Clock) ที่ให้อารมณ์กึ่งๆระหว่าง Analog(mechanic) กับ Digital กัน ตัวเลขที่โชว์ จะเป็น 7-Segment Display ที่เราสร้างขึ้นเอง เวลามีการอัพเดท เราจะทำอนิเมชั่น Flip หรือพลิกนั่นเอง ซึ่งจะให้อารมณ์ 2.5D แล้วเนื่องจาก Flutter ไม่ใช่ 3D engine เราก็ต้องมีการพลิกแพลงกันหน่อย
ด้านบนขวา จะเป็นสวิทช์(Switch) ใช้เลือกระหว่าง Light กับ Dark Mode
ด้านล่างจะเป็นพื้นหลัง(Background) รันคลื่น เคลื่อนตัวไปเรื่อยๆ 3 อันซ้อนกัน
มาเริ่มกัน
สร้างโปรเจค Flutter หลังจากนั้นเตรียมสร้างไฟล์ดังนี้
- Switch — อันนี้ เราจะใช้ของ Material design ที่ Flutter มีให้อยู่แล้ว
- Clock — เราจะสร้างจากการสร้าง dash ก่อน เอามารวมกัน 7 อันได้ digit แล้วเอา digit มาใช้แสดงนาฬิกาอีกที
- Wave — เราจะมาวาดคลื่นกันใช้ CustomPainter
เนื่องจากแอปเรามีการใช้อนิเมชั่น ดังนั้นเราจำเป็นต้องมี ValueNotifier เพื่อส่งสัญญาณให้อนิเมชั่น นั้นๆ รับทราบได้
ภาพรวมคือ หลังเราสร้าง 3 ส่วนเสร็จ เราจะเอามา Stack / ซ้อนกัน แล้วเนื่องจากไม่อยากใช้ AppBar ก็จะแนะนำให้ครอบ body ใน Scaffold ด้วย SafeArea
การเรียงลำดับ Stack ตามนี้จะเป็นการเรียง Switch, Clock, Wave จากด้านหน้าสุดไปหลังสุด
หมายเหตุ ในบทความนี้ เราจะเน้นไปส่วนการสร้าง Graphics มากกว่า Logic ดังนั้นจะเน้นคำอธิบายเฉพาะส่วน Design and Animation
แนะนำ ให้ clone source code มาไถตามไปด้วย ที่ด้านล่างสุด
เริ่มที่ Dash กันก่อน
เปิดไฟล์
widgets/dash.dart
มันจะมีทั้งเส้นแนวตั้ง DashVertical และแนวนอน DashHorizontal เรามาอธิบายที่ DashVertical กัน
CustomPainter เป็นกระดานไว้ให้เราวาดรูป เมื่อเราต้องการวาดเราจะทำใน paint()
ใน paint เราจะมี canvas เราสามารถใชตัวนี้วาดรูปพื้นฐานได้เลย เช่นสี่เหลี่ยม วงกลม แต่ถ้าเราอยากวาดของเราเอง เราก็จะใช้
canvas.drawPath
ในนี้ เราจะส่ง
Path() กับ
Paint()
Path จะก็จะสั่งให้วาดรูปตามที่เราคำนวนไว้แล้ว
Paint จะเป็นข้อมูลการลงสี เราจะวาดแค่เส้นรอบรูป หรือลงสีทั้งรูปก็ได้ สามารถ blend colorFilter … ได้ แนะนำให้ลองไปแก้เพิ่มดูได้ แต่สำหรับครั้งนี้เราเอาแค่นี้ก่อน
ตอนเรียกใช้ก็ให้เรียก CustomPaint แล้วใส่ขนาด กับ DashVerticalPainter ไป
ต่อมา
เราจะเอา CustomPaint อันนี้ไปทำอนิเมชั่นต่อใน DashVertical Widget
ในส่วน constructor เราก็จะรับตัว ขนาด สี และตัวบอกสถานะ ซึ่งมี 2 ตัว status กับ isDark
ในส่วน state เราก็จะรับตัวแปรจาก constructor มา มีการกำหนดค่า Default ให้ถ้าไม่ได้ส่งค่ามา เมื่อรัน
initColorValue()
เท่ากับว่าเรามีค่าให้ตัวแปรเกือบหมดแล้ว ขาดแค่
animationController กับ
animationValue
เราจะเอาฟังก์ชั่นเมื่อกี้มารันใน initState นอกจากนี้ก็จะมากำหนดค่า animationController ด้วย ให้เวลา 800 ms เวลานี้เป็นเวลาที่ dash จะใช้พลิก (ไม่ควรเกิน 1000 ms ไม่งั้นจะเสร็จไม่ทันก่อนการอัพเดทครั้งถัดไป)
ต่อมาเราก็จะนำมาใช้กำหนด animationValue ต่อ ใส่ curve ให้ได้ความรู้สึกของการพลิกแบบมีฟิสิกส์
สุดท้ายเราก็จะฟังว่า ถ้าตัวเลขเปลี่ยน status หรือ โหมดสีเปลียน isDark เราก็จะรันอนิเมชั่น ด้วยฟังก์ชั่น updateDash
ใน
updateDash()
เราก็จะเช็คว่า สีต้นทาง ปลายทางควรเป็นสีไหน เมื่อเช็คเสร็จก็สั่งรันอนิเมชั่น
ท้ายสุดอย่าลืม dispose animationController หลังใช้งานเสร็จ
มาดูในส่วนสร้างอนิเมชั่นต่อ การพลิก เราจะเอา Transform เข้ามาช่วย แล้วด้วยความ Custom ของเรา เราจึงต้องสร้าง Matrix transform ของเราเอง
ดังนั้นที่ transform เราจะเอา Matrix identity เป็นตัวตั้งต้น แล้วเราหมุนรอบแกน Y จึงมี rotateY
ส่วน setEntry ที่ row 3 col 2 เป็นการเพิ่มความลึกตื้น ขึ้นกับแกนตำแหน่ง Y ที่จุดนั้นๆ ให้ความเหมือนจริงมากขึ้น
origin เราจะตั้งไว้ตรงกลาง หาจากความกว้าง และความยาวมาหาร 2 (หรือจะใช้
alignment: Alignment.center
แทน origin ก็ได้)
child เราจะสลับสีเมื่อค่า
animationValue.value > 0.5
หรือว่าเป็นจุดที่กำลังตั้งฉากกับสายตาเรา ทำให้เหมือนว่า
Dash เรามีด้านหน้า ด้านหลัง
ตอนนี้ส่วนอนิเมชั่นก็หมดแล้ว เหลือให้เราเอามาประกอบกันให้เป็น 7-segment
ขอข้ามส่วน DashHorizontal เพราะมีความคล้ายกัน ต่างกันที่ขณะวาดเราจะคำนวนสำหรับแนวนอน และเวลา transform จะ rotateX แทน และ setEntry ที่ row 3 col 1
Digit
เราจะสร้าง class DigitDash
ไว้ใช้สำหรับความเป็นระเบียบใน class Digit เวลา init ค่า ValueNotifier ของแต่ละ Dash
ใน state เราก็จะ init และให้ฟังถ้าตัวเลขต้องอัพเดท เราจะเอาตัวเลขมาแปลงเป็น 7-Segment ด้วยมือเรา
เราจะใช้ตารางเปรียบเทียบ แล้วให้ loop ไล่เช็คและเปลี่ยนให้เรา
digit.dash\[i\].value = digitTo7Seg\[widget.numberNotifier.value\]\[i\];
หมายเหตุ ในตารางเปรียบเทียบ false หมายถึงตอนโชว์ฝั่ง on ส่วน true หมายถึงตอนโชว์ฝั่ง off
สุดท้าย เวลา build เราก็เอา Vertical กับ Horizontal Dash มาเรียงใน Row กับ Column
แล้วเราก็เสร็จ Digit แล้ว
มารวม Digit ให้กลายเป็น Clock
เราก็จะนำ Digit มาใส่ใน
_buildClock()
จากจุดนี้ น่าจะเริ่มสงสัยว่าตัวแปร dashHeight minute0 isDark … มาจากไหน
เราจะประกาศตัวแปรไว้ด้านบน จะเห็นได้ว่าเราจะสร้าง ValueNotifier ของแต่ละ Digit ไว้ที่นี่ แล้วส่งตัวแปรต่ออีกที
isDark ก็จะประกาศค่าตั้งต้นเป็น false ไว้ก่อน แล้วส่งต่ออีกทีเช่นกัน
ตรงนี้จะเป็นตำแหน่งที่เราสร้างตัวเรียกอัพเดทเวลา ต้องขอบคุณที่ Dart รองรับ Stream ในตัว เราเลยใช้มันสร้างข้อมูลเป็นช่วงๆได้ เราเก็บไว้ที่ตัวแปร timer
ต่อมาเราก็ฟังค่าของมัน ด้วย
timer.listen()
แล้วอัพเดทตัวเลขแต่ละหลักของนาฬิกา
ส่วนนี้จะเป็นการคำนวนความกว้างยาวของ Dash เพราะต้องการให้มัน Responsive
ค่าตรงนี้มาจากการวัดอัตราส่วนของ Dash โดยในครั้งนี้ กำหนดให้ ความกว้าง:ความยาว เท่ากับ 5:1 ที่เหลือก็ต้องลองนับดูแล้วว่ามี Dash ในแนวตั้งกี่อัน แนวนอนกี่อัน แล้วบวกเลขดู จะได้ที่มาของ 143/2 😀
ส่วนยากสุดเสร็จแล้ว กระโดดมาทำอันง่ายสุดบ้าง
Switch
เราสามารถเรียก
Switch()
จาก material ที่ Flutter มีให้ได้เลย
ค่าของมันจะฟังจาก
isDark และเวลากด เราก็จะอัพเดทค่าตัวนี้ นอกจากนี้เพื่อให้มีการอัพเดท UI ที่ไม่ได้มี Listener เช่น background color เราเลยเรียก
setState()
ตำแหน่งของ
Switch เราก็วางไว้บนขวา ห่างมา 16 px ซึ่งจุดนี้ก็ไม่ต้องกลัวว่าจะโดน Notification Bar ทับ เพราะเราได้หุ้มด้วย
SafeArea()
ก่อนหน้าแล้ว
Wave
มาดูที่ไฟล์
widgets/wave.dart
กัน
คลื่นสร้างขึ้นมายังไง จุดเริ่มต้นก็มาจากตรีโกณ แบบพื้นฐานสุดก็
Y = sin(X)
แต่ในเมื่อเราอยากได้คลื่นที่พลิ้วไหวและมีความแปรปวน แสดงว่าเราก็ต้องเอาคลื่น หลายๆอันมายำรวมกัน
สำหรับที่ใช้ในนี้ เราจะมีคลื่น 2 อัน
- อันแรก(สีเขียว) จะเป็นคลื่นที่นิ่งๆ ไม่ได้มี แอมพลิจูด(ความสูงคลื่น)มาก เคลื่อนตัวช้า
- ส่วนอีกอัน(สีแดง) ตัวนี้จะมี แอมพลิจูดที่กว้างกว่า และความยาวคลื่น(ความกว้างคลื่น)ที่สั้นกว่า มีความแปรปวนสูง ถ้าสังเกตในโค๊ด เราจะใส่เวลาเข้ามาเป็นปัจจัยส่วนนี้ด้วย เพื่อให้เฟสคลื่นไม่ตรงกัน
รวมกันได้(สีฟ้า) คลื่นที่ค่อยๆเคลื่อนตัว มีความแปรปวนกลางๆ ดูเป็นธรรมชาติ(มั้ง😅)
คลื่นจะเคลื่อนตัว หรือ Offset ตามค่า waveOffset ที่จะรับมาจาก AnimationController อีกที
เพื่อความ Random เวลาเรียก Wave Widget แต่ละครั้ง เราจะมี RandomOffset รับมาด้วย
ทั้งคู่เราจะมาอธิบายต่อใน Wave Widget
width กับ height เราก็จะรับมาจาก parent widget อีกที ดังนั้นเพื่อไม่ให้คลื่นล้นออกมาจากความสูงที่กำหนด เราเลยมี highestWaveHeight ที่คำนวนจากกรณีของความสูงที่สูงสุดของคลื่นเรา (สังเกตว่าเราเอาสมการมาแก้ค่าใน sin() ให้เป็น pi/2 ให้หมด) แล้วเอาค่านี้ไป offset ในแกน y
ที่
initState()
เรามีการประกาศค่าตั้งต้นดังนี้
randomOffset เราใช้ฟังชั่น
Random()
ให้สุ่มค่าที่อยู่ช่วงคลื่น คือ 0–2pi หรือ 0–6.28 เราเลยสุ่มเลข 0–63 แล้วหาร 10 ค่าตรงนี้ก็ประมาณๆเอา ใครอยากให้สุ่มกว้างกว่านี้ก็ 0–628 หาร 100 ก็ได้ หรือสุ่มมั่วเลยก็ได้ ตามความชอบ
time เก็บค่า AnimationController ตรงนี้เราจะกำหนดให้คลื่นเคลื่อนตัวช้าเร็ว แก้ Duration ตรงนี้ก็ได้ แค่ลองกลับไปดูฟังก์ชั่นที่ใช้สร้างคลื่นแล้วคำนวนดูให้ดี เพราะมีตัวแปรอื่นที่ส่งผลด้วย
waveOffset นำค่าจาก time มา Tween ให้เป็นช่วง 0 ถึง 2pi แล้วดึงค่ามาเลย
แล้วเนื่องจากเรารันคลื่นเรื่อยๆใน background เลยสั่ง
time.repeat()
ส่วน build เราก็สร้าง CustomPaint แล้วรับ WavePainter หรือ CustomPainter ที่เราพึ่งสร้างมาได้เลย
นำมาใส่ใน main.dart กัน
ที่
_buildBackgroundWave()
เรานำ Wave Widget มาหุ้มด้วย SizedBox เพื่อให้ CustomPainter รู้ขนาดได้ แล้วให้อยู่ด้านล่างเลยใช้ Align
ทำแบบนี้ 3 ครั้ง ซ้อนกันโดยใช้ Stack ก็เสร็จเรียบร้อยแล้ว
ครบแล้ว 3 ส่วน หน้าตาสุดท้ายก็ควรจะออกมาได้เท่ๆตามนี้
จบอีกหนึ่งบทความ เป้าหมายหลักๆครั้งนี้ก็เพื่อเพิ่มความคุ้นเคยกับเครื่องมือที่ใช้วาดรูปแบบ Custom และการทำอนิเมชั่นให้พิเศษขึ้น เพราะโดยทั่วไป เราก็จะใช้เพียง scale translate rotate แต่เวลาเราอยากสร้าง 2.5D หรือ 3D เราก็ต้องกลับไปพึ่งเลขอีกครั้ง matrix ก็เข้ามาช่วยชีวิตเราตรงนี้ อยากได้อะไรมากกว่านี้ ก็ขึ้นกับจิตนาการจะไปถึงแล้ว หวังว่าทั้งหมดนี้จะเป็นประโยชน์ไม่น้อย