Flutter - แอนิเมชั่น
ภาพเคลื่อนไหวเป็นขั้นตอนที่ซับซ้อนในแอปพลิเคชันมือถือใด ๆ แม้จะมีความซับซ้อน แต่แอนิเมชั่นก็ช่วยเพิ่มประสบการณ์ของผู้ใช้ไปอีกระดับและให้การโต้ตอบกับผู้ใช้ที่หลากหลาย เนื่องจากความมีชีวิตชีวาแอนิเมชั่นจึงกลายเป็นส่วนสำคัญของแอปพลิเคชันมือถือสมัยใหม่ เฟรมเวิร์ก Flutter ตระหนักถึงความสำคัญของแอนิเมชั่นและมอบเฟรมเวิร์กที่เรียบง่ายและใช้งานง่ายในการพัฒนาแอนิเมชั่นทุกประเภท
บทนำ
แอนิเมชั่นคือกระบวนการแสดงชุดภาพ / ภาพตามลำดับเฉพาะภายในช่วงเวลาที่กำหนดเพื่อให้เกิดภาพลวงตาของการเคลื่อนไหว สิ่งที่สำคัญที่สุดของแอนิเมชั่นมีดังนี้ -
ภาพเคลื่อนไหวมีค่าที่แตกต่างกัน 2 ค่า ได้แก่ ค่าเริ่มต้นและค่าสิ้นสุด ภาพเคลื่อนไหวเริ่มต้นจากค่าเริ่มต้นและผ่านชุดของค่ากลางและสุดท้ายจะสิ้นสุดที่ค่าสิ้นสุด ตัวอย่างเช่นหากต้องการทำให้วิดเจ็ตเคลื่อนไหวให้จางหายไปค่าเริ่มต้นจะเป็นค่าความทึบเต็มและค่าสุดท้ายจะเป็นค่าความทึบเป็นศูนย์
ค่ากลางอาจเป็นเชิงเส้นหรือไม่เป็นเชิงเส้น (เส้นโค้ง) และสามารถกำหนดค่าได้ ทำความเข้าใจว่าภาพเคลื่อนไหวทำงานตามที่กำหนดค่าไว้ การกำหนดค่าแต่ละแบบให้ความรู้สึกที่แตกต่างกันกับภาพเคลื่อนไหว ตัวอย่างเช่นการซีดจางวิดเจ็ตจะมีลักษณะเป็นเส้นตรงในขณะที่การกระดอนของลูกบอลจะไม่เป็นเส้นตรง
ระยะเวลาของกระบวนการแอนิเมชั่นมีผลต่อความเร็ว (ความช้าหรือความเร็ว) ของแอนิเมชั่น
ความสามารถในการควบคุมกระบวนการแอนิเมชั่นเช่นการเริ่มแอนิเมชั่นการหยุดแอนิเมชั่นการทำซ้ำแอนิเมชั่นเพื่อกำหนดจำนวนครั้งการย้อนกระบวนการของแอนิเมชั่นเป็นต้น
ใน Flutter ระบบแอนิเมชั่นไม่ได้ทำแอนิเมชั่นจริง แต่จะให้เฉพาะค่าที่จำเป็นในทุกเฟรมเพื่อแสดงภาพ
คลาสแอนิเมชั่น
ระบบภาพเคลื่อนไหวกระพือขึ้นอยู่กับวัตถุแอนิเมชั่น คลาสแอนิเมชั่นหลักและการใช้งานมีดังนี้ -
ภาพเคลื่อนไหว
สร้างค่าที่ถูกแก้ไขระหว่างตัวเลขสองตัวในช่วงเวลาหนึ่ง คลาสแอนิเมชั่นที่พบมากที่สุด ได้แก่ -
Animation<double> - แก้ไขค่าระหว่างทศนิยมสองจำนวน
Animation<Color> - สอดแทรกสีระหว่างสองสี
Animation<Size> - สอดแทรกขนาดระหว่างสองขนาด
AnimationController- ออบเจ็กต์แอนิเมชั่นพิเศษเพื่อควบคุมแอนิเมชั่นเอง สร้างค่าใหม่เมื่อใดก็ตามที่แอปพลิเคชันพร้อมสำหรับเฟรมใหม่ รองรับแอนิเมชั่นเชิงเส้นและค่าเริ่มตั้งแต่ 0.0 ถึง 1.0
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
ที่นี่ตัวควบคุมจะควบคุมตัวเลือกภาพเคลื่อนไหวและระยะเวลาควบคุมระยะเวลาของกระบวนการแอนิเมชั่น vsync เป็นตัวเลือกพิเศษที่ใช้เพื่อเพิ่มประสิทธิภาพทรัพยากรที่ใช้ในภาพเคลื่อนไหว
CurvedAnimation
คล้ายกับ AnimationController แต่รองรับภาพเคลื่อนไหวที่ไม่ใช่เชิงเส้น CurvedAnimation สามารถใช้ร่วมกับวัตถุ Animation ได้ดังนี้ -
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
ทวี <T>
ได้มาจาก Animatable <T> และใช้เพื่อสร้างตัวเลขระหว่างตัวเลขสองตัวใด ๆ ที่ไม่ใช่ 0 และ 1 สามารถใช้ร่วมกับออบเจ็กต์แอนิเมชั่นโดยใช้วิธีการเคลื่อนไหวและส่งผ่านวัตถุแอนิเมชั่นจริง
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this); Animation<int> customTween = IntTween(
begin: 0, end: 255).animate(controller);
Tween ยังสามารถใช้ร่วมกับ CurvedAnimation ได้ดังนี้ -
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> customTween = IntTween(begin: 0, end: 255).animate(curve);
ที่นี่ตัวควบคุมคือตัวควบคุมภาพเคลื่อนไหวที่แท้จริง เส้นโค้งให้ประเภทของความไม่เป็นเชิงเส้นและ customTween ให้ช่วงที่กำหนดเองตั้งแต่ 0 ถึง 255
ขั้นตอนการทำงานของ Flutter Animation
ขั้นตอนการทำงานของภาพเคลื่อนไหวมีดังนี้ -
กำหนดและเริ่มตัวควบคุมแอนิเมชันใน initState ของ StatefulWidget
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
เพิ่มแอนิเมชั่นตามฟัง addListener เพื่อเปลี่ยนสถานะของวิดเจ็ต
animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() {
setState(() {
// The state that has changed here is the animation object’s value.
});
});
สามารถใช้วิดเจ็ต Build-in, AnimatedWidget และ AnimatedBuilder เพื่อข้ามขั้นตอนนี้ วิดเจ็ตทั้งสองยอมรับออบเจ็กต์ Animation และรับค่าปัจจุบันที่จำเป็นสำหรับภาพเคลื่อนไหว
รับค่าแอนิเมชั่นระหว่างขั้นตอนการสร้างวิดเจ็ตจากนั้นนำไปใช้กับความกว้างความสูงหรือคุณสมบัติที่เกี่ยวข้องแทนค่าเดิม
child: Container(
height: animation.value,
width: animation.value,
child: <Widget>,
)
แอปพลิเคชันการทำงาน
ให้เราเขียนแอปพลิเคชั่นที่ใช้แอนิเมชั่นอย่างง่ายเพื่อทำความเข้าใจแนวคิดของแอนิเมชั่นใน Flutter framework
สร้างแอปพลิเคชันFlutterใหม่ใน Android studio, product_animation_app
คัดลอกโฟลเดอร์ assets จาก product_nav_app ไปยัง product_animation_app และเพิ่มเนื้อหาภายในไฟล์ pubspec.yaml
flutter:
assets:
- assets/appimages/floppy.png
- assets/appimages/iphone.png
- assets/appimages/laptop.png
- assets/appimages/pendrive.png
- assets/appimages/pixel.png
- assets/appimages/tablet.png
ลบรหัสเริ่มต้นเริ่มต้น (main.dart)
เพิ่มการนำเข้าและฟังก์ชันหลักพื้นฐาน
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
สร้างวิดเจ็ต MyApp ที่ได้มาจาก StatefulWidgtet
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
สร้างวิดเจ็ต _MyAppState และใช้งาน initState และกำจัดนอกเหนือจากวิธีการสร้างเริ่มต้น
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 10), vsync: this
);
animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
controller.forward();
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
controller.forward();
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Product layout demo home page', animation: animation,)
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
ที่นี่
ในวิธีการ initState เราได้สร้างออบเจ็กต์ตัวควบคุมแอนิเมชั่น (ตัวควบคุม) วัตถุแอนิเมชั่น (ภาพเคลื่อนไหว) และเริ่มการเคลื่อนไหวโดยใช้ controller.forward
ในวิธีการกำจัดเราได้กำจัดออบเจ็กต์ตัวควบคุมแอนิเมชั่น (คอนโทรลเลอร์)
ในวิธีการสร้างส่งภาพเคลื่อนไหวไปยังวิดเจ็ต MyHomePage ผ่านตัวสร้าง ตอนนี้วิดเจ็ต MyHomePage สามารถใช้ออบเจ็กต์แอนิเมชั่นเพื่อทำให้เนื้อหาเคลื่อนไหวได้
ตอนนี้เพิ่มวิดเจ็ต ProductBox
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image})
: super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/appimages/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.name, style:
TextStyle(fontWeight: FontWeight.bold)),
Text(this.description),
Text("Price: " + this.price.toString()),
],
)
)
)
]
)
)
);
}
}
สร้างวิดเจ็ตใหม่ MyAnimateWidget เพื่อสร้างแอนิเมชั่นจาง ๆ ง่ายๆโดยใช้ความทึบ
class MyAnimatedWidget extends StatelessWidget {
MyAnimatedWidget({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
child: Opacity(opacity: animation.value, child: child),
),
child: child),
);
}
ที่นี่เราได้ใช้ AniatedBuilder เพื่อสร้างแอนิเมชั่นของเรา AnimatedBuilder เป็นวิดเจ็ตที่สร้างเนื้อหาในขณะที่ทำแอนิเมชั่นในเวลาเดียวกัน ยอมรับออบเจ็กต์แอนิเมชั่นเพื่อรับค่าแอนิเมชันปัจจุบัน เราได้ใช้ค่าภาพเคลื่อนไหว animation.value เพื่อตั้งค่าความทึบของวิดเจ็ตลูก ผลกระทบวิดเจ็ตจะทำให้วิดเจ็ตลูกเคลื่อนไหวโดยใช้แนวคิดความทึบ
สุดท้ายสร้างวิดเจ็ต MyHomePage และใช้ออบเจ็กต์แอนิเมชั่นเพื่อทำให้เนื้อหาใด ๆ ของมันเคลื่อนไหว
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title, this.animation}) : super(key: key);
final String title;
final Animation<double>
animation;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Listing")),body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
FadeTransition(
child: ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.png"
), opacity: animation
),
MyAnimatedWidget(child: ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.png"
), animation: animation),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.png"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.png"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.png"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppy.png"
),
],
)
);
}
}
ที่นี่เราได้ใช้ FadeAnimation และ MyAnimationWidget เพื่อทำให้สองรายการแรกในรายการเคลื่อนไหว FadeAnimation เป็นคลาสแอนิเมชั่นในตัวซึ่งเราใช้ในการทำให้เด็กเคลื่อนไหวโดยใช้แนวคิดทึบแสง
รหัสที่สมบูรณ์มีดังนี้ -
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 10), vsync: this);
animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
controller.forward();
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
controller.forward();
return MaterialApp(
title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Product layout demo home page', animation: animation,)
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title, this.animation}): super(key: key);
final String title;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Listing")),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
FadeTransition(
child: ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.png"
),
opacity: animation
),
MyAnimatedWidget(
child: ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.png"
),
animation: animation
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.png"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.png"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.png"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppy.png"
),
],
)
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image}) :
super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/appimages/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.description), Text(
"Price: " + this.price.toString()
),
],
)
)
)
]
)
)
);
}
}
class MyAnimatedWidget extends StatelessWidget {
MyAnimatedWidget({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
child: Opacity(opacity: animation.value, child: child),
),
child: child
),
);
}
รวบรวมและเรียกใช้แอปพลิเคชันเพื่อดูผลลัพธ์ เวอร์ชันเริ่มต้นและเวอร์ชันสุดท้ายของแอปพลิเคชันมีดังนี้ -