Flutter | มาลองทำแอป Icon Showcase || Part II อนิเมชั่น

Suppachai Thanrukprasert
4 min readJan 6, 2020

--

เริ่ม

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

หรือใครอยากต่อยอดจากอันก่อนหน้า มาลองทำเฉพาะส่วนอนิเมชั่นเพิ่ม ก็สามารถไปโหลดจาก Github ได้ โดยคำอธิบายต่างๆในครั้งนี้เราก็จะเอาโค็ดจากครั้งที่แล้วมาทำต่อ

เป้าหมายสุดท้ายเรา จะทำให้ออกมาตามนี้

Icon Showcase(final)

เป้าหมายของเราคือ จะสร้างอนิเมชั่นสำหรับเวลาเปลี่ยนหน้า ให้มันดูลื่นไหล โดยเราจะทำ 2 จุด

  • ที่ Card แต่ละอันใน ListView
  • ที่ Icon Menu ที่อยู่ด้านซ้ายของ Appbar
สองจุดที่จะทำอนิเมชั่น

และสุดท้าย เราจะแก้ transition ระหว่างเปลี่ยนหน้าให้ดูกลมกลืนขึ้นด้วย

อนิเมชั่นสำหรับ Card

เราเริ่มจากง่ายๆก่อน เอา Widget Hero ไปหุ้ม Card ทั้งในหน้า main.dart และ detail_page.dart แล้วให้ตัง tag ของทั้งคู่ให้เหมือนกัน ซึ่งเราก็ตั้งให้เป็นชื่อ title ของไอคอนแต่ละอัน

main.dart
detail_page.dart

แล้วผลลัพธ์ที่ได้

ผลลัพธ์ก็ไม่ได้ออกมาดูแย่เลย แต่ถ้าเราอยากไปให้สุดกว่านี้ เราก็จะทำให้หัวข้อกับไอคอน ขยับตามไปด้วยจากตำแหน่งในหน้าก่อนหน้าไปหน้าถัดไป แต่ปัญหาคือ จู่ๆ เราจะครอบ Hero ให้ Text กับ Icon เลยไม่ได้ เพราะตอนนี้ทั้งคู่เป็น Widget ลูกหลาน ใน Hero ก่อนหน้าไปแล้ว เราไม่สามารถ ครอบ Hero ซ้อน Hero ได้ (อ้างอิง)

ทางแก้ของเราคือ แทนที่จะมอง Card ของเราเป็นชิ้นเดียว เราใช้ Stack แยกเป็น 3 ชั้นซ้อนกันแทน จากบนลงล่าง หัวข้อ ไอค่อน พื้นหลัง ซึ่งจะเพิ่มงานให้เราหน่อย แต่เพื่อความงาม ก็จัดไป

ที่ main.dart

เราจะแทนที่ Card ด้วย Stack หรือแก้โค๊ตกลายเป็นด้านล่างเลย

main.dart

ที่ detail_page.dart

เราก็ทำคล้ายๆ กัน

detail_page.dart

ผลลัพธ์เริ่มคล้ายที่เราตั้งเป้าแล้ว

แต่!!!! เห็นตัวอักษรแสดงแปลกๆ ไหม เพราะ อีกปัญหากับ Hero คือมันไม่ได้ถูกดีไซน์ให้ใช้กับ Text ตั้งแต่แรกเริ่ม แต่ก็ใช่ว่าจะแก้ไม่ได้ ทางออกก็มีแบบหยาบ กับทำให้เนียนเลย แบบหยาบก่อน เราจะเอา Text ทั้งในหน้า main.dart และ detail_page.dart ไปคลุมด้วย Widget Material และแทรกพารามีเตอร์ type: MaterialType.transparency เพิ่มเข้าไป ก็จะพอแก้ขัดไปคร่าวๆได้

แก้ Hero Text แบบหยาบ

ประมาณนี้ อันนี้ไปลองเอาเอง วิธีนี้จะพบเฉพาะปัญหา Text Overflow เวลา transition ทำงาน

ทางแก้ที่ดีกว่า แต่ก็ยาวกว่าเยอะคือ เราจะต้องไปแก้ flightShuttleBuilder ของ Hero ซึ่งเราก็ต้องมาทำอนิเมชั่นให้กับ Text ของเราเอง Hero จะได้เอาตัวนี้ไปใช้แทน

หลักการของเรา คือ เราจะสร้าง Widget ให้ Text ที่จะมี 4 สถานะ enlarge(กำลังขยายขนาด), enlarged(ค้างที่ขนาดใหญ่), shrink(กำลังย่อส่วน), shrunk(ค้างที่ขนาดเล็ก) เราจะใช้ชื่อเหล่านี้เรียกสถานะ ดังนั้น เราเลยสร้างไฟล์ใหม่ขึ้นมา จะประกอบไปด้วย enum พวกนี้ ชื่อไฟล์ว่า view_state.dart

view_state.dart

เรามาสร้างตัว Widget ให้ Text ต่อ สร้างไฟล์ใหม่ชื่อว่า title_hero_flight.dart

title_hero_flight.dart

เราค่อยๆมา อธิบายไปทีละส่วนกัน

  • class DestinationTitleContent จะทำหน้าที่รับผิดชอบในการสร้าง Widget Text ณ เวลาหนึ่งๆ โดยจะรับ ข้อความ ขนาดฟอร์ต จำนวนบรรทัด overflow isOverflow สี
  • class DestinationTitle จะทำหน้าที่เอาข้อความที่สร้างจาก class DestinationTitleContent สร้างข้อความนิ่ง หรืออนิเมชั่น ขึ้นกับ ViewState ที่ส่งมา คลาสนี้จึงจำเป็นต้องรับ ViewState ขนาดฟอร์ตทั้งเล็ก และใหญ่เข้ามาด้วย สำหรับ ViewState.enlarge กับ ViewState.shrink ที่มีอนิเมชั่น AnimationController จะเข้ามาจัดการส่วนนี้ ซึ่งขอละคำอธิบายในส่วนนี้ไปก่อน

ใครตามมาถึงจุดนี้แล้วงงเป็นพิเศษ ก็แนะนำให้อย่าพึ่งไปคิดไรมาก ก็อปทั้งไฟล์ title_hero_flight.dart นี้ไปใช้ก่อนก็ได้

เราจะกลับไปที่ main.dart กับ detail_page.dart กันต่อ เอาของใหม่ของเรามาใช้งาน

แก้ Hero ที่มี Text ให้เป็นตามนี้

main.dart
detail_page.dart

สังเกตว่าใน flightShuttleBuilder เราจะ return DestinationTitle ที่มี ViewState ไม่ enlarge ก็ shrink เพราะ flightShuttleBuilder จะถูกเรียกขณะที่มี transition เปลี่ยนหน้าเกิดขึ้น ส่วน child เราจะส่ง DestinationTitle ที่มี ViewState enlarged กับ shrank ตามหน้าที่ต้องแสดง

เสร็จเรียบร้อยในส่วนของ Card แล้ว ไปกันต่อ

อนิเมชั่นสำหรับ Icon Menu

ในส่วนนี้ก็ไม่ค่อยยุ่งยากมากแล้ว แต่ต้องใช้ AnimationController ดังนั้นมาเริ่มกันเลยที่ main.dart

main.dart

เราจะเพิ่มที่ขีดเส้นใต้ 2 อัน และกล่อง 3 กล่องลงไปใน class _MyHomePageState

ฟังก์ชั่น _initAnimationController() จะทำหน้าที่ประกาศค่าแรกเริ่มให้ _animationController ของเรา โดยเราตั้งเวลาเล่นไว้ที่ 500 ms เมื่อสร้างเสร็จก็เอาฟังก์ชั้นนี้ไปใส่ใน initState() และสุดท้ายอย่าลืม dispose _animationController ของเราที่ ฟังก์ชั่น dispose() ด้วย

หลังจากสร้าง _animationController เสร็จ เราก็เอามันมาใช้เวลาจะเปลี่ยนหน้าไป-กลับ แต่ก่อนหน้านั้น เราจำเป็นต้องเปลี่ยนไอคอน จากภาพนิ่งเฉยๆให้ มีอนิเมชั่นด้วย ซึ่ง Flutter ก็มีให้เราใช้ AnimatedIcon ดังนั้นเราก็เปลี่ยนเลย

เดิม
แก้ใหม่

แล้วสุดท้ายไปเพิ่ม _animationController.forward(from: 0.0); ตอนก่อนกำลังเปลี่ยนไปหน้า DetailPage และ _animationController.reverse(from: 1.0); เมื่อกลับมาจากหน้าก่อนหน้า แต่ก่อนจะรัน ก็ต้องมีหลักการเช็คเล็กน้อย

main.dart

และทำแบบเดียวกันในหน้า detail_page.dart ด้วย

detail_page.dart

มีความแตกต่างเล็กน้อยเมื่อ initState() เราจะ _animationController.forward() ไปเลย

แล้วอย่าลืมเปลี่ยนไอคอนเดิมให้กลายเป็น AnimatedIcon ด้วย

detail_page.dart

จากจุดนี้จะพบว่าอนิเมชั่นเราจะทำงานแล้ว แต่ตำแหน่งของมันยังไม่เคลื่อนตาม ไม่พอยังโดนทับอีก

Icon โดนทับเวลา transition เปลี่ยนหน้า

ทางแก้รอบนี้ง่ายเลย เอา Widget Hero หุ้ม AnimatedIcon ทั้งสองหน้า และอย่าลืมตั้ง tag ให้เหมือนกัน

main.dart(ซ้าย) และ detail_page.dart(ขวา)

แค่นี้ก็เรียบร้อย

(แถม) เพื่อความสมูทอีกนิด แก้ transition เปลี่ยนหน้า

เราจำเป็นต้องสร้าง transition ของเราเอง สร้างไฟล์ใหม่เรียก fade_page_route.dart แล้วก็อปแปะเลย

เราสามารถเปลี่ยนไปใช้ transition อื่นได้ แต่เราจะใช้ FadeTransition เพื่อความกลมกลืน นอกจากนี้ เราสามารถแก้ Duration กับ Curve ของ transition ได้ โดยเราจะตั้ง Duration ไว้ 550 millisecond และใช้ Curves.easeIn

พร้อมแล้ว เราก็จะใช้แทนที่ MaterialPageRoute ในหน้า main.dart import เข้ามาแล้วเปลี่ยน MaterialPageRoute เป็น FadePageRoute

main.dart

ตอนนี้ แอปเราก็ออกมาดูดีแล้ว

จบไปอีกแอปนึง หวังว่าการแนะนำ Widget เหล่านี้ และการเล่นกับ Hero และ AnimationController จะช่วยทำให้รู้จักการทำอนิเมชั่นมากขึ้นไม่มากก็น้อย อย่างไรก็ตามแอปนี้ยังมีอีกหลายจุดให้เก็บตกอีก อย่างเช่น Icon ที่อยู่ใน Card ไม่ได้สเกลขนาดตามเมื่อเปลี่ยนหน้า หรือสี Icon บน Appbar ไม่ได้ค่อยๆเปลี่ยนตาม ก็ลองไปทำเพิ่มเองได้นะ 55 หรือใครลองทำตามแล้วไม่เข้าใจส่วนไหนก็พิมพ์คำถามลงด้านล่างได้เลย

ถ้าเกิดพยายามแก้โค๊ดตามแล้วงง หรืออยากข้ามเอาโค๊ดเต็มมาเทียบเลย ก็โหลดจากลิงค์ด้านล่างได้เลย

--

--