ทำความเข้าใจกลไกภายในของแอนิเมชันแบบ implicit และควบคุม AnimationController ของคุณเองอย่างละเอียด
เปิดบทเรียนนี้ใน KodokonAnimatedContainer อยู่ในตระกูล แอนิเมชันแบบ implicit: คุณเปลี่ยนค่าเป้าหมาย แล้วเฟรมเวิร์กจะทำแอนิเมชันการเปลี่ยนผ่านให้ เบื้องหลังนั้น ImplicitlyAnimatedWidget ทุกตัวมี AnimationController ซ่อนอยู่ในสเตตของมัน เมื่อมีการตั้งค่าใหม่เข้ามา didUpdateWidget จะไล่ดูคุณสมบัติต่าง ๆ ผ่าน forEachTween และ ตั้งเป้าหมายใหม่ (re-target) ให้ tween แต่ละตัว: ขอบเขต begin จะกลายเป็นค่าที่ถูกแทรก (interpolate) อยู่ในปัจจุบัน ส่วนขอบเขต end จะเป็นเป้าหมายใหม่ จากนั้นคอนโทรลเลอร์จึงเริ่มใหม่จากศูนย์ ผลที่ตามมาอันมีค่าคือ การเปลี่ยนเป้าหมายกลางคันของแอนิเมชันจะ ไม่มีการกระโดดของภาพ แอนิเมชันจะทำงานต่อจากตรงที่ค้างไว้พอดี
import 'package:flutter/material.dart';
class SelectableCard extends StatefulWidget {
const SelectableCard({super.key});
@override
State<SelectableCard> createState() =>
_SelectableCardState();
}
class _SelectableCardState extends State<SelectableCard> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _selected ? 220 : 160,
height: 96,
decoration: BoxDecoration(
color: _selected
? Colors.indigo
: Colors.indigo.shade200,
borderRadius: BorderRadius.circular(
_selected ? 28 : 12,
),
),
),
);
}
}แบบ implicit ครอบคลุมคุณสมบัติที่เฟรมเวิร์กคาดการณ์ไว้ ส่วน TweenAnimationBuilder ขยายกระบวนการนี้ให้ใช้กับค่าใด ๆ ที่แทรกค่าได้ ทันทีที่คุณต้องการ ควบคุม แอนิเมชัน ไม่ว่าจะหยุดชั่วคราว ย้อนกลับ ทำซ้ำ หรือตอบสนองต่อสถานะ ให้เปลี่ยนไปใช้ AnimationController แบบ explicit พารามิเตอร์ vsync ต้องการ TickerProvider: mixin จะสร้าง Ticker ซึ่งเป็นคอลแบ็กที่ SchedulerBinding เรียกในทุกเฟรม รายละเอียดเล็ก ๆ ที่น้อยคนรู้: TickerMode จะ ปิดเสียง (mute) ticker ของ subtree ที่ถูกซ่อนอยู่ (เช่น อยู่ใต้เส้นทางที่ถูกบัง) เวลายังคงเดินต่อไป แต่ไม่มีคอลแบ็กใดทำงานอีกแล้ว ไม่มีการเปลือง CPU ไปกับแอนิเมชันที่มองไม่เห็นเลย
import 'package:flutter/material.dart';
class RevealPanel extends StatefulWidget {
const RevealPanel({super.key, required this.child});
final Widget child;
@override
State<RevealPanel> createState() => _RevealPanelState();
}
class _RevealPanelState extends State<RevealPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 200),
);
late final Animation<double> _opacity = _controller
.drive(CurveTween(curve: Curves.easeOutCubic));
void toggle() {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
_controller.reverse();
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggle,
child: FadeTransition(
opacity: _opacity,
child: widget.child,
),
);
}
}คอนโทรลเลอร์ก็เป็นเพียง Animation<double> ที่วิ่งจาก 0 ถึง 1: จงประกอบมันขึ้นมาแทนที่จะใช้ค่าดิบของมันตรง ๆ controller.drive(CurveTween(...)) สร้างแอนิเมชันที่แตกยอดออกมา โดยไม่มีสเตตเพิ่ม ในการนำค่าไปใช้กับ tree ให้เลือกใช้วิดเจ็ตตระกูล Transition (FadeTransition, RotationTransition) พวกมันฟังแอนิเมชันโดยตรงที่ระดับ render object โดยไม่ต้องสร้างวิดเจ็ตใหม่แม้แต่ตัวเดียวต่อเฟรม เมื่อการแปลงเป็นแบบใด ๆ ก็ตาม AnimatedBuilder จัดการงานนี้ได้ ขอเพียงคุณส่ง subtree ที่อยู่นิ่งผ่าน child: มันจะถูกสร้าง เพียงครั้งเดียว แล้วถูกฉีดกลับเข้าไปใน builder ในทุกเฟรม นั่นคือการปรับให้เหมาะสมที่ให้ผลตอบแทนสูงที่สุดสำหรับแอนิเมชันแบบ explicit
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Orbit extends StatefulWidget {
const Orbit({super.key});
@override
State<Orbit> createState() => _OrbitState();
}
class _OrbitState extends State<Orbit>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: const Icon(Icons.satellite_alt, size: 32),
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Transform.translate(
offset: Offset(
math.cos(angle) * 80,
math.sin(angle) * 80,
),
child: child,
);
},
);
}
}