Trong phát triển ứng dụng Flutter, hoạt ảnh là một công cụ quan trọng để nâng cao trải nghiệm người dùng. Hôm nay, chúng ta sẽ đi sâu vào một thành phần hoạt ảnh mạnh mẽ và tinh tế – SizeTransition, cho phép các phần tử giao diện người dùng (UI) của bạn có hiệu ứng chuyển đổi mượt mà khi thay đổi kích thước.
SizeTransition là gì
SizeTransition là một thành phần hoạt ảnh có sẵn trong Flutter, cho phép thành phần con của nó tạo ra hiệu ứng chuyển đổi mượt mà khi thay đổi kích thước. Nói một cách đơn giản, nó có thể điều khiển chiều rộng hoặc chiều cao của thành phần con thay đổi theo một đường cong hoạt ảnh được chỉ định, từ đó tạo ra các hiệu ứng thị giác như mở rộng, co lại, v.v.
SizeTransition kế thừa từ AnimatedWidget, điều này có nghĩa là nó sẽ tự động lắng nghe các thay đổi của đối tượng Animation và xây dựng lại UI. Vai trò cốt lõi của nó là điều chỉnh kích thước của thành phần con trong quá trình hoạt ảnh, biến việc hiển thị/ẩn đi vốn dĩ thô cứng trở nên tự nhiên và mượt mà.
Cách sử dụng cơ bản của SizeTransition
Hãy bắt đầu với một ví dụ đơn giản:
class HienThiSizeTransition extends StatefulWidget {
@override
_TrangThaiHienThiSizeTransition createState() => _TrangThaiHienThiSizeTransition();
}
class _TrangThaiHienThiSizeTransition extends State<HienThiSizeTransition>
with SingleTickerProviderStateMixin {
late AnimationController _khoiDieuKhien;
late Animation<double> _giaTriAnHien;
@override
void initState() {
super.initState();
_khoiDieuKhien = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_giaTriAnHien = CurvedAnimation(
parent: _khoiDieuKhien,
curve: Curves.easeInOut,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Demo SizeTransition')),
body: Column(
children: [
ElevatedButton(
onPressed: () {
if (_khoiDieuKhien.isCompleted) {
_khoiDieuKhien.reverse();
} else {
_khoiDieuKhien.forward();
}
},
child: Text('Bật/Tắt Hoạt ảnh'),
),
SizeTransition(
sizeFactor: _giaTriAnHien,
child: Container(
width: 200,
height: 100,
color: Colors.blue,
child: Center(
child: Text(
'Xin chào Flutter!',
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
);
}
@override
void dispose() {
_khoiDieuKhien.dispose();
super.dispose();
}
}
Trong ví dụ này, chúng ta đã tạo một AnimationController và một CurvedAnimation, sau đó truyền chúng vào thuộc tính sizeFactor của SizeTransition. Khi giá trị hoạt ảnh thay đổi từ 0 đến 1, thành phần con sẽ từ từ mở rộng từ trạng thái ẩn hoàn toàn đến kích thước đầy đủ.
Điều khiển hướng hoạt ảnh
SizeTransition cũng cung cấp các thuộc tính axis và axisAlignment để kiểm soát hướng và cách căn chỉnh của hoạt ảnh:
SizeTransition(
sizeFactor: _giaTriAnHien,
axis: Axis.horizontal, // Hoạt ảnh theo chiều ngang
axisAlignment: -1.0, // Mở rộng từ bên trái
child: YourWidget(),
)
Ưu điểm của SizeTransition
Ưu điểm về hiệu suất
So với việc tự triển khai hoạt ảnh kích thước, SizeTransition có những ưu điểm về hiệu suất đáng kể. Nó hoạt động trực tiếp trên thuộc tính kích thước của RenderBox, tránh các phép tính bố cục không cần thiết. Khi bạn sử dụng AnimatedContainer hoặc các giải pháp khác, có thể sẽ kích hoạt việc bố cục lại toàn bộ cây con, trong khi SizeTransition chỉ ảnh hưởng đến các phần cần thiết.
So sánh với các giải pháp truyền thống
Hãy xem xét việc triển khai truyền thống mà không sử dụng SizeTransition:
// Giải pháp truyền thống: Sử dụng AnimatedContainer
AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isExpanded ? 100 : 0,
child: YourWidget(),
)
Vấn đề của giải pháp này là:
- Khi chiều cao bằng 0, thành phần con vẫn tồn tại trong cây widget, có thể gây ra lỗi tràn
- Quá trình hoạt ảnh có thể xuất hiện các hiệu ứng cắt xén không tự nhiên
- Chi phí hiệu suất tương đối cao
Trong khi đó, ưu điểm của SizeTransition là:
- Tự động xử lý việc cắt xén và tràn của thành phần con
- Hiệu ứng hoạt ảnh mượt mà hơn
- Hiệu suất tốt hơn
- Kiểm soát hoạt ảnh chính xác hơn
Cách sử dụng nâng cao của SizeTransition
Kết hợp hoạt ảnh phức tạp
Bạn có thể kết hợp SizeTransition với các thành phần hoạt ảnh khác để tạo ra các hiệu ứng phức tạp hơn:
class SizeTransitionNangCao extends StatefulWidget {
@override
_TrangThaiSizeTransitionNangCao createState() => _TrangThaiSizeTransitionNangCao();
}
class _TrangThaiSizeTransitionNangCao extends State<SizeTransitionNangCao>
with TickerProviderStateMixin {
late AnimationController _khoiDieuKhienKichThuoc;
late AnimationController _khoiDieuKhienDoManHinh;
late Animation<double> _giaTriKichThuoc;
late Animation<double> _giaTriDoManHinh;
@override
void initState() {
super.initState();
_khoiDieuKhienKichThuoc = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
_khoiDieuKhienDoManHinh = AnimationController(
duration: Duration(milliseconds: 400),
vsync: this,
);
_giaTriKichThuoc = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _khoiDieuKhienKichThuoc,
curve: Curves.elasticOut,
));
_giaTriDoManHinh = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _khoiDieuKhienDoManHinh,
curve: Curves.easeIn,
));
}
void _batDauHieuUng() async {
await _khoiDieuKhienKichThuoc.forward();
_khoiDieuKhienDoManHinh.forward();
}
void _daoHuongHieuUng() async {
await _khoiDieuKhienDoManHinh.reverse();
_khoiDieuKhienKichThuoc.reverse();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: _giaTriKichThuoc,
child: FadeTransition(
opacity: _giaTriDoManHinh,
child: Container(
width: 300,
height: 150,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'Hoạt ảnh Nâng cao',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}
Tùy chỉnh đường cong hoạt ảnh
Bạn có thể tạo các đường cong hoạt ảnh tùy chỉnh để đạt được các hiệu ứng độc đáo:
class DuongCongTuyChon extends Curve {
@override
double chuyenDoi(double t) {
// Tạo hiệu ứng nảy
if (t < 0.5) {
return 2 * t * t;
} else {
return 1 - 2 * (1 - t) * (1 - t);
}
}
}
// Sử dụng đường cong tùy chỉnh
_giaTriAnHien = CurvedAnimation(
parent: _khoiDieuKhien,
curve: DuongCongTuyChon(),
);
Hoạt ảnh kích thước đáp ứng
Kết hợp với MediaQuery để thực hiện hoạt ảnh kích thước đáp ứng:
class SizeTransitionPhanHoi extends StatelessWidget {
final Animation<double> _giaTriAnHien;
final Widget _widgetCon;
SizeTransitionPhanHoi({
required this._giaTriAnHien,
required this._widgetCon,
});
@override
Widget build(BuildContext context) {
final chieuRongManHinh = MediaQuery.of(context).size.width;
final laMayTinhBang = chieuRongManHinh > 600;
return SizeTransition(
sizeFactor: _giaTriAnHien,
axis: laMayTinhBang ? Axis.horizontal : Axis.vertical,
axisAlignment: laMayTinhBang ? -1.0 : 0.0,
child: _widgetCon,
);
}
}
Lưu ý và thực hành tốt nhất
Quản lý bộ nhớ
Luôn nhớ giải phóng AnimationController trong phương thức dispose:
@override
void dispose() {
_khoiDieuKhien.dispose();
super.dispose();
}
Tránh hoạt ảnh quá mức
Mặc dù hoạt ảnh có thể nâng cao trải nghiệm người dùng, nhưng quá nhiều hoạt ảnh sẽ làm ứng dụng trông rườm rà. Sử dụng SizeTransition một cách hợp lý, chỉ thêm hiệu ứng hoạt ảnh ở những nơi thực sự cần thiết.
Cân nhắc về hiệu suất
Khi xử lý nhiều SizeTransition, hãy cân nhắc sử dụng mô hình singleton cho AnimationController hoặc một "hồ" hoạt ảnh để tối ưu hóa hiệu suất:
class QuanLyHieuUng {
static final QuanLyHieuUng _thayThe = QuanLyHieuUng._noiBo();
factory QuanLyHieuUng() => _thayThe;
QuanLyHieuUng._noiBo();
final Map<String, AnimationController> _cacKhoiDieuKhien = {};
AnimationController layKhoiDieuKhien(String khoa, TickerProvider vsync) {
return _cacKhoiDieuKhien.putIfAbsent(
khoa,
() => AnimationController(
duration: Duration(milliseconds: 300),
vsync: vsync,
),
);
}
void giaHuyKhoiDieuKhien(String khoa) {
_cacKhoiDieuKhien[khoa]?.dispose();
_cacKhoiDieuKhien.remove(khoa);
}
}
Xử lý các trường hợp biên
Trong một số trường hợp, bạn có thể cần xử lý các trường hợp biên của hoạt ảnh:
class SizeTransitionAnToan extends StatelessWidget {
final Animation _giaTriAnHien;
final Widget _widgetCon;
final double _kichThuocToiThieu;
SizeTransitionAnToan({
required this._giaTriAnHien,
required this._widgetCon,
this._kichThuocToiThieu = 0.01, // Tránh trường hợp hoàn toàn bằng 0
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _giaTriAnHien,
builder: (context, child) {
final heSo = math.max(_giaTriAnHien.value, _kichThuocToiThieu);
return SizeTransition(
sizeFactor: AlwaysStoppedAnimation(heSo),
child: this._widgetCon,
);
},
);
}
}