การทำ Animation ใน Flutter ตอนที่ 2
สวัสดีผู้อ่านครับ บล็อกนี้เป็นตอนที่ 2 ของการทำ Animation ใน Flutter จากตอนที่แล้ว เราได้รู้จักกับ Animation controller และการทำ transition ด้วย AnimatedBuilder ไปแล้ว ซึ่งเป็น Widget ที่ทาง Flutter เตรียมไว้สำหรับ build Animation แบบ custom โดยเฉพาะ ในตอนที่ 2 นี้จะพามารู้จักกับ Widget Animated แบบอื่นๆกัน
ใครยังไม่ได้อ่าน ตอนที่ 1 อ่านได้ที่ลิงค์ด้านล่างนี้
Animated Widget
จากตอนที่แล้วได้ลองเล่น AnimatedBuilder ซึ่งเป็นหนึ่งใน widget ของกลุ่ม Animated โดย widget ประเภทนี้จะใช้ prefix ว่า animated และนอกจาก AnimatedBuilder แล้วยังมี widget สำหรับ animated อีกหลายตัว มาลองเล่นกัน
Animated Opacity
AnimatedOpacity จะช่วยให้เราทำ fade transition กับ widget ที่ต้องการได้สะดวกมาก
สร้างตัวแปรสำหรับเก็บค่า opacity
class _MyHomePageState extends State<MyHomePage> {
double currentOpacity = 0;
จากนั้น เอา widget ที่ต้องการมาเป็น child ของ AnimatedOpacity แล้วกำหนด duration กับ opacity ให้มัน จากนั้นเรียก setState เพื่อเปลี่ยนค่า opacity มันก็จะทำ animation ของ opacity ให้อัตโนมัติ ตามเวลา duration ที่กำหนด จะเห็นว่า เราไม่ต้องไปยุ่งกับ animation controller เลย
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animation"),
),
body: Center(
child: AnimatedOpacity( // เพิ่ม Animated Opacity
opacity: currentOpacity, //
duration: Duration(seconds: 1), //
child: Container( //
width: 200, //
height: 200, //
color: Colors.orange[400], //
))),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () { // เพิ่ม on pressed
setState(() {
currentOpacity = currentOpacity == 0 ? 1 : 0;
});
},
),
);
}
ลองรันดูผลลัพธ์
Animated Container
animated container ใช้งานคล้ายกับ animated opacity แต่จะทำ animation เกี่ยวกับคุณสมบัติของ container แทน เช่น ขนาด สี
เริ่มจาก สร้างตัวแปรไว้หนึ่งตัว สำหรับเก็บค่าขนาด container
class _MyHomePageState extends State<MyHomePage> {
double size = 200;
จากนั้นกำหนดตัวแปรให้กับ width , height ของ container แล้วลอง เรียก setState เปลี่ยนค่าของตัวแปร
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: AnimatedContainer( // เพิ่ม
width: size, //
height: size, //
color: Colors.orange[400], //
duration: Duration(milliseconds: 500), //
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
setState(() {
size = size + 50; // เพิ่ม
});
},
),
);
}
เท่านี้ container ก็จะทำ animation ตามตัวแปรที่เรากำหนดแล้ว ตัวอย่างนี้ผมกำหนดให้กดปุมแล้ว ขนาดจะใหญ่ขึ้น 50
Animated Align
Animated ยังมีอีกเยอะ มาลองเล่นอีกตัว AnimatedAlign ที่จะช่วยทำ animation ตามค่าของ align
กำหนดตัวแปร alignment มีค่าเริ่มต้นอยู่ ตรงกลาง
class _MyHomePageState extends State<MyHomePage> {
Alignment alignment = Alignment.center;
จากนั้นใช้ AnimatedAlign แล้วอัพเดทค่า align ใน setState ให้ alignment ไป บนซ้าย แทน
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: AnimatedAlign( // เพิ่ม
alignment: alignment, //
duration: Duration(seconds: 1), //
child: Container( //
width: 200, //
height: 200, //
color: Colors.orange[400], //
),
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
setState(() { // เพิ่ม
alignment = alignment == Alignment.center ? Alignment.topLeft : Alignment.center;
});
},
),
);
}
ลองรัน จะเห็นว่ามันทำ animation ตาม alignment แล้ว ง่ายมากๆ
Animated Positioned
ถ้าใช้ align แล้ว Animated positioned ก็ใช้งานไม่ต่างกันนัก โดยจะใช้งานเพื่อกำหนดตำแหน่งใน parent และทำ animation อัตโนมัติหาก เราอัพเดท position
สร้างตัวแปร กำหนดค่า position
class _MyHomePageState extends State<MyHomePage> {
double top = 99;
double right = 251;
จากนั้นนำค่าไปใช้กับ AnimatedPositioned แล้วลอง setState อัพเดทค่า position จากการกดปุ่ม
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Stack(
children: [
AnimatedPositioned( // เพิ่ม
top: top, //
right: right, //
duration: Duration(seconds: 1), //
child: Container( //
color: Colors.red[400], //
width: 100, //
height: 100, //
))
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () { // เพิ่ม
setState(() { //
top = top + 20; //
if(right >= 0){ //
right = right - 50; //
}else{ //
right = 250; //
} //
}); //
}, //
),
);
}
ลองรัน
ถึงตรงนี้ น่าจะเริ่มจับทางได้แล้วใช่ไหมครับ ว่า AnimatedOpacity , AnimatedContainer , AnimatedAlign พวกนี้ ใช้งานคล้ายกันมาก แค่นำมันมา wrap ให้กับ widget ที่ต้องการ กำหนด duration และค่าต่างๆ จากนั้นเมื่อเรา setState เพื่ออัพเดท มันก็จะทำ animation อัตโนมัติ สะดวกมากๆ
แต่ถึงอย่างนั้น widget ในกลุ่ม Animated ก็ยังมีอีกซึ่งมีอีกหลายตัวที่น่าสนใจ แต่การใช้งานจะซับซ้อนขึ้นอีกนิด
Animated Icon
Animated icon เป็น widget อีกตัวที่น่าสนใจ โดยมันจะ transition เปลี่ยนไอคอนนึง ไปอีกไอคอนนึง ซึ่งทาง Flutter ได้เตรียมไอคอนมาให้เราจำนวนนึงแล้ว
โดยเราจำเป็นต้องใช้ Animation controller
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController animationController;
@override
void initState() {
animationController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
super.initState();
}
จากนั้น Wrap widget ที่ต้องการด้วย AnimatedIcons โดยใช้ icon จาก AnimatedIcons เวลาใช้งานก็เรียกใช้ animationController.forward()
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_home,
progress: animationController,
color: Colors.red[400],
size: 100,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
if (animationController.isCompleted) {
animationController.reverse();
} else {
animationController.forward();
}
},
),
);
}
ลองรัน
มีไอคอนให้ใช้งานได้ประมาณสิบอัน สามารถดูได้ที่คลาส AnimatedIcons
Animated Cross Fade
ที่น่าสนใจอีกตัวคือ Cross Fade โดยจะทำงานง่ายๆคือ กำหนด widget 2 ตัว คือ first , second จากนั้นก็กำหนดว่าจะให้แสดงตัวไหน แล้วมันจะทำ fade animation ให้อัตโนมัติ
สร้าง bool มาตัวนึงว่าจะให้แสดง widget ตัวแรกมัย
class _MyHomePageState extends State<MyHomePage> {
bool _first = true;
จากนั้นก็กำหนด firstChild , secondChild ให้กับ AnimatedCrossFade ส่วนวิธีแสดง child ไหนจะใช้ CrossFadeState.showFirst หรือ CrossFadeState.showSecond
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: AnimatedCrossFade(
duration: Duration(seconds: 1),
firstChild: Container(width: 200,height: 200,color: Colors.blue[300]),
secondChild: FlutterLogo(style: FlutterLogoStyle.stacked, size: 150.0),
crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.red[300],
child: Icon(Icons.refresh),
onPressed: () {
setState(() {
_first = !_first;
});
},
),
);
}
จะได้แบบนี้
Animated List
Widget พระเอกอีกตัวที่ผมคิดว่าน่าจะใช้กันบ่อย คือ AnimatedList มันคือการทำ animation กับ item ใน ListView เช่น การเพิ่ม item หรือการลบ item
เริ่มจากลอง สร้างข้อมูลจำลองขึ้นมา สำหรับเป็น item ใน ListView
class _MyHomePageState extends State<MyHomePage> {
List<String> listData = List.generate(3, (index) {
return "New item";
});
AnimatedList การใช้งานแทบจเหมือน Listview.builder คือกำหนด itemCount และ implement ตัว itemBuilder ในตัวอย่างนี้ ผมจะแยก method สำหรับ build UI แต่ละ item เอาไว้
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Container(
color: Colors.grey[300],
child: AnimatedList(
initialItemCount: listData.length,
padding: EdgeInsets.all(16),
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
return buildRowData(listData[index]);
},
),
),
);
}
ลอง return item แบบธรรมดาๆไปก่อน แบบที่ยังไม่ใส่ animation
Widget buildRowData(String title) {
return Container(
margin: EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
width: double.infinity,
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontSize: 18),
),
Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut",
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
);
}
จะได้เหมือน ListView ปกติ
ใส่ floating action button สำหรับกดสร้างและลบ item
@override
Widget build(BuildContext context) {
return Scaffold(
...
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
backgroundColor: Colors.green[300],
child: Icon(
Icons.add,
),
onPressed: () {
// addData(); // รอ implement สำหรับกดเพิ่ม item
},
),
SizedBox(
width: 8,
),
FloatingActionButton(
backgroundColor: Colors.red[300],
child: Icon(Icons.remove),
onPressed: () {
// removeData(); // รอ implement สำหรับกดลบ item
},
),
],
),
);
}
ลองรัน จะได้แบบนี้
จากนั้นประกาศตัวแปร GlobalKey ของ AnimatedListState ใน State ของเรา ใช้สำหรับเข้าถึง State ของ AnimatedList
class _MyHomePageState extends State<MyHomePage> {
...
GlobalKey<AnimatedListState> keyAnimatedList = GlobalKey();
นำ GlobalKey ไปกำหนดให้กับ AnimatedList ผ่าน parameter ที่ชื่อว่า key
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Container(
color: Colors.grey[300],
child: AnimatedList(
key: keyAnimatedList, // เพิ่ม key
...
จากนั้นเขียน method สำหรับเพิ่ม item ลงไปใน List สิ่งที่เราต้องทำคือ เพิ่มข้อมูลลงไปใน list data จริงๆ และเรียก key current State เพื่อเข้าไปเรียก insertItem อันนี้ไม่ได้เป็นการ add ข้อมูลแต่จะเป็นการทำ animation ให้ item ที่เพิ่งถูกเพิ่มไป
void addData() {
listData.add("New item");
keyAnimatedList.currentState.insertItem(listData.length - 1, duration: Duration(seconds: 1));
}
ลองรันกด add data ได้แล้ว แต่ animation ยังไม่แสดง เพราะเรายังไม่ได้กำหนด Animation ให้แต่ละ item
กลับมาที่ itemBuilder ของ AnimatedList เพิ่ม FadeTransition ให้กับ buildRowData ของเรา จะเห็นหว่า itemBuilder ก็ส่งค่า animation มาให้แล้ว โดยค่านี้เกิดจากตอนเรากำหนดที่ insertItem นั่นเอง
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Container(
color: Colors.grey[300],
child: AnimatedList(
...
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
return FadeTransition(opacity: animation, child: buildRowData(listData[index])); // เพิ่ม Fade transition
},
),
),
ลองรัน ได้ animation ตอน add item แล้ว
ต่อมาทำ animation สำหรับลบ item ครับ วิธีการคล้ายกับ insert แต่เราจะทำ animation เพื่อลบก่อน จากนั้นค่อยลบข้อมูลจาก list จริง
void removeData() {
keyAnimatedList.currentState.removeItem(listData.length - 1, (context, animation) {
return FadeTransition(
opacity: animation.drive(Tween(begin: 0, end: 1)),
child: buildRowData(listData.last),
);
}, duration: Duration(seconds: 1));
listData.removeLast();
}
เสร็จแล้วเพิ่ม removeData() ให้กับปุ่มลบ
@override
Widget build(BuildContext context) {
return Scaffold(
...
floatingActionButton: Row(
...
children: <Widget>[
FloatingActionButton(
...
onPressed: () {
addData(); // เพิ่ม
},
),
...
FloatingActionButton(
...
onPressed: () {
removeData(); // เพิ่ม
},
),
],
),
);
}
ลองรัน จะได้ AnimatedList แล้ว
Source code ของ AnimatedList ครับ
https://gist.github.com/benznest/0a0f63758aeabd80e2c5e360d886df1e
สรุป
ในตอนที่ 2 เราได้รู้จักกับ widget ที่มี prefix Animated เช่น Animated Opacity , Animated Container ซึ่งเป็น Widget ที่ใช้สำหรับทำ animation กับ widget นั้นๆ และ AnimatedList ที่ช่วยให้ทำ Animation กับ item ใน ListView ได้ ในตอนถัดไปจะพาไปรู้จักกับ Animation ตัวไหนอีก ฝากติดตามด้วยครับ