
Chào các bạn, hôm nay chúng ta sẽ cùng "mổ xẻ" một khái niệm nghe có vẻ hơi "hàn lâm" nhưng lại cực kỳ hữu ích trong Flutter: AnimatedModalBarrier. Hãy hình dung bạn đang điều khiển một dàn nhạc giao hưởng, và đôi khi bạn cần một nhạc công nào đó tạm thời "ngưng diễn" để tập trung vào một đoạn cao trào khác. AnimatedModalBarrier chính là cái tấm màn nhung huyền ảo mà bạn kéo lên, vừa để che đi, vừa để tạo hiệu ứng chuyển cảnh đầy kịch tính.
1. AnimatedModalBarrier là gì và để làm gì?
Trong thế giới UI/UX, đôi khi chúng ta cần "khóa" một phần màn hình lại, không cho người dùng tương tác với các widget bên dưới, trong khi một thứ gì đó quan trọng hơn (như một hộp thoại, một menu pop-up, hay một màn hình loading) đang hiển thị ở phía trên. Cái "khóa" này thường là một lớp phủ mờ, tối đi một chút, tạo cảm giác chiều sâu và tập trung.
ModalBarrier là một widget sinh ra để làm điều đó. Nó tạo ra một lớp phủ màu, chặn các sự kiện chạm và cử chỉ, không cho chúng lọt xuống các widget phía dưới. Nhưng đời không chỉ có "bật" và "tắt" cục cằn, đúng không? Chúng ta cần sự tinh tế, sự chuyển động mượt mà. Đó chính là lúc AnimatedModalBarrier bước lên sân khấu.
AnimatedModalBarrier về cơ bản là một ModalBarrier nhưng được tích hợp khả năng hoạt hình (animation). Thay vì xuất hiện "phập!" một cái hay biến mất "phụt!" một cái, nó sẽ từ từ mờ dần vào (fade in) hoặc mờ dần đi (fade out), hoặc thậm chí là thay đổi màu sắc hay hình dạng theo một cách uyển chuyển. Nó giống như tấm màn nhung trong nhà hát, không bao giờ được kéo lên hay hạ xuống một cách thô bạo, mà luôn nhẹ nhàng, từ tốn, tạo cảm giác sang trọng và chuyên nghiệp.
Nó dùng để làm gì?
- Tạo lớp phủ cho Dialogs/Pop-ups: Khi bạn hiển thị một
AlertDialoghayshowModalBottomSheet, chínhAnimatedModalBarrier(hoặc một biến thể của nó) đang hoạt động ngầm để làm mờ nền và chặn tương tác. - Màn hình Loading/Chờ: Khi ứng dụng đang xử lý một tác vụ nặng, bạn có thể dùng nó để hiện một lớp phủ mờ với biểu tượng loading, ngăn người dùng bấm lung tung gây lỗi.
- Chế độ Focus/Highlight: Đôi khi bạn muốn hướng sự chú ý của người dùng vào một phần tử cụ thể, bạn có thể dùng
AnimatedModalBarrierđể làm mờ toàn bộ phần còn lại của màn hình. - Custom Overlays: Để tạo ra các hiệu ứng overlay độc đáo của riêng bạn mà không bị giới hạn bởi các widget có sẵn.
Điểm mấu chốt là: bạn muốn chặn tương tác VÀ bạn muốn quá trình chặn/mở chặn đó diễn ra một cách mượt mà, có hiệu ứng.

2. Code Ví Dụ Minh Hoạ: "Chế Độ Tập Trung"
Hãy cùng tạo một ví dụ "ngầu" hơn một chút: một "Chế độ Tập Trung" (Focus Mode) tạm thời. Khi bạn kích hoạt, toàn bộ màn hình sẽ được làm mờ đi một cách nhẹ nhàng, và một thông báo "Đang tập trung..." sẽ hiện lên. Sau vài giây, màn hình sẽ trở lại bình thường.
Để làm được điều này, chúng ta sẽ cần một chút "phép thuật" của OverlayEntry để đặt AnimatedModalBarrier lên trên cùng của mọi thứ.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AnimatedModalBarrier Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
// Đối tượng OverlayEntry sẽ giữ widget overlay của chúng ta.
OverlayEntry? _focusModeOverlayEntry;
// AnimationController để điều khiển animation của barrier.
late AnimationController _animationController;
// Animation<Color> để thay đổi màu sắc của barrier.
late Animation<Color?> _barrierColorAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500), // Thời gian mờ dần
vsync: this, // Cần SingleTickerProviderStateMixin
);
// Tween để định nghĩa sự chuyển đổi màu sắc từ trong suốt đến đen mờ.
_barrierColorAnimation = ColorTween(
begin: Colors.transparent,
end: Colors.black.withOpacity(0.7), // Màu đen mờ 70%
).animate(_animationController);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
// Hàm để hiển thị lớp phủ "Chế độ Tập trung"
void _showFocusModeOverlay() {
// Đảm bảo không có overlay nào đang hiển thị
if (_focusModeOverlayEntry != null) return;
// Tạo một OverlayEntry mới
_focusModeOverlayEntry = OverlayEntry(
builder: (context) {
return Stack(
children: [
// AnimatedModalBarrier: Lớp phủ chặn tương tác và có animation
AnimatedModalBarrier(
color: _barrierColorAnimation, // Sử dụng animation màu sắc
dismissible: false, // Không cho phép đóng bằng cách chạm vào barrier
),
// Widget hiển thị thông báo "Đang tập trung..."
Center(
child: Material( // Cần Material để Text có theme và độ cao
color: Colors.transparent, // Không có màu nền
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
CircularProgressIndicator(color: Colors.white), // Biểu tượng loading
SizedBox(height: 16),
Text(
'Đang tập trung... Xin đừng làm phiền!',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
);
},
);
// Chèn OverlayEntry vào Overlay của context hiện tại
Overlay.of(context).insert(_focusModeOverlayEntry!);
// Bắt đầu animation barrier từ trong suốt đến màu đen mờ
_animationController.forward();
// Thiết lập một timer để tự động đóng overlay sau 3 giây
Future.delayed(const Duration(seconds: 3), () {
if (_focusModeOverlayEntry != null) {
// Bắt đầu animation barrier từ đen mờ trở lại trong suốt
_animationController.reverse().then((_) {
// Sau khi animation hoàn tất, loại bỏ OverlayEntry
_focusModeOverlayEntry?.remove();
_focusModeOverlayEntry = null;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chào mừng đến với Trạm Không Gian Flutter!'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Hãy thử kích hoạt chế độ tập trung:',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _showFocusModeOverlay, // Gắn hàm hiển thị overlay
icon: const Icon(Icons.psychology_alt),
label: const Text(
'Kích hoạt Focus Mode',
style: TextStyle(fontSize: 18),
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
),
const SizedBox(height: 40),
const Text(
'Bạn có thể bấm các nút khác ở đây, nhưng khi Focus Mode bật, chúng sẽ bị khóa!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 20),
OutlinedButton(
onPressed: () {
// Nút này sẽ bị chặn khi Focus Mode đang hoạt động
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bạn đã bấm nút này!')),
);
},
child: const Text('Nút Phụ'),
),
],
),
),
);
}
}
Giải thích code:
_HomePageStatevớiSingleTickerProviderStateMixin: Để sử dụngAnimationController,StatefulWidgetcủa chúng ta cần "mixin"SingleTickerProviderStateMixin. Đây là cơ chế cung cấp "nhịp đập" cho animation.AnimationController: Đây là "đạo diễn" của mọi animation. Nó quản lý thời gian, tốc độ, và trạng thái của animation (chạy tới, chạy lùi, dừng).ColorTweenvà_barrierColorAnimation:ColorTweenđịnh nghĩa sự chuyển đổi giữa hai màu (ở đây là từColors.transparentđếnColors.black.withOpacity(0.7))._barrierColorAnimationlà mộtAnimation<Color?>được tạo ra từColorTweenvà điều khiển bởi_animationController.AnimatedModalBarriernhận trực tiếp mộtAnimation<Color?>cho thuộc tínhcolorcủa nó, rất tiện lợi!_showFocusModeOverlay(): Đây là hàm "ma thuật" để hiển thị overlay.- Nó tạo một
OverlayEntry.OverlayEntrylà một "cánh cửa" cho phép bạn chèn widget vào lớpOverlaycủa ứng dụng, tức là nó sẽ nằm trên tất cả các widget khác trong cây widget thông thường, giống như một tấm kính trong suốt đặt lên trên bức tranh. - Bên trong
OverlayEntry, chúng ta dùngStackđể xếp chồngAnimatedModalBarriervà thông báo "Đang tập trung...".AnimatedModalBarriersẽ chiếm toàn bộ không gian củaOverlayEntry. AnimatedModalBarrierđược truyền_barrierColorAnimationđể nó tự động cập nhật màu sắc theo animation.dismissible: falsenghĩa là người dùng không thể chạm vào lớp phủ để đóng nó (chúng ta muốn nó tự đóng sau 3 giây).Overlay.of(context).insert(_focusModeOverlayEntry!)là dòng lệnh "thả"OverlayEntryvào màn hình._animationController.forward()bắt đầu animation, làm cho barrier mờ dần vào.Future.delayed(...)hẹn giờ 3 giây. Sau đó,_animationController.reverse()sẽ làm cho barrier mờ dần đi, và khi animation hoàn tất (.then((_) { ... })), chúng taremove()OverlayEntrykhỏi màn hình.
- Nó tạo một
dispose(): Đừng quên_animationController.dispose()! Đây là một quy tắc vàng.AnimationControllertiêu tốn tài nguyên và cần được giải phóng khiStatekhông còn được sử dụng nữa để tránh rò rỉ bộ nhớ.
Khi bạn chạy ứng dụng này và bấm nút "Kích hoạt Focus Mode", bạn sẽ thấy màn hình chính từ từ mờ đi một cách duyên dáng, một thông báo và biểu tượng loading hiện ra, và sau 3 giây, mọi thứ lại trở về bình thường một cách nhẹ nhàng. Trong thời gian màn hình mờ, bạn sẽ không thể bấm vào nút "Nút Phụ" hay bất kỳ thứ gì khác bên dưới.
3. Mẹo (Best Practices) Để Ghi Nhớ và Dùng Thực Tế
-
Hiểu rõ sự khác biệt
ModalBarriervs.AnimatedModalBarrier:ModalBarrier: Chỉ là một lớp phủ tĩnh, không animation. Nó hiện ra "cộp" một cái, biến mất "cộp" một cái. Thích hợp cho các trường hợp đơn giản, không cần hiệu ứng.AnimatedModalBarrier: Có animation. Dùng khi bạn muốn sự mượt mà, chuyên nghiệp trong trải nghiệm người dùng. Luôn ưu tiênAnimatedModalBarriernếu bạn có animation, vì nó được tối ưu hóa cho điều đó.
-
Quản lý
AnimationControllercẩn thận:- Luôn khởi tạo trong
initState()vàdispose()trongdispose(). Đây là việc làm bắt buộc để tránh rò rỉ bộ nhớ và các lỗi không mong muốn. vsync: Đừng quênvsync: thisvàSingleTickerProviderStateMixinchoStatefulWidgetcủa bạn.
- Luôn khởi tạo trong
-
Sử dụng
OverlayEntrycho các overlays "toàn cục":- Nếu bạn muốn
AnimatedModalBarrierche phủ toàn bộ màn hình, hoặc xuất hiện độc lập với cây widget hiện tại của bạn (như một dialog),OverlayEntrylà lựa chọn tuyệt vời. Nó cho phép bạn "chèn" widget vào lớp phủ trên cùng của ứng dụng. - Nếu
AnimatedModalBarrierchỉ cần che phủ một phần nhỏ trong mộtStackcụ thể, bạn có thể đặt nó trực tiếp vàoStackđó mà không cầnOverlayEntry.
- Nếu bạn muốn
-
Thuộc tính
dismissible:dismissible: true(mặc định): Cho phép người dùng chạm vào lớp phủ để đóng nó. Hữu ích cho các pop-up, menu có thể đóng dễ dàng.dismissible: false: Người dùng không thể chạm để đóng. Buộc họ phải tương tác với các widget khác trên lớp phủ (ví dụ: bấm nút "OK" trong dialog) hoặc chờ một hành động tự động (như ví dụ Focus Mode). Hãy cân nhắc UX khi dùngfalse.
-
Kết hợp với
Stackvà các widget khác:AnimatedModalBarrierthường được dùng trong mộtStack, với nó là lớp dưới cùng để chặn, và các widget nội dung của bạn (Text, CircularProgressIndicator, Dialog,...) là các lớp trên.- Đảm bảo các widget nội dung của bạn có màu nền hoặc
Materialđể chúng không bị "nuốt chửng" bởi màu của barrier.
-
Tối ưu hiệu suất:
AnimatedModalBarrierkhá nhẹ. Tuy nhiên, nếu bạn đặt các widget nội dung phức tạp bên trên nó và cũng animation chúng, hãy chú ý đến hiệu suất.- Tránh vẽ lại quá nhiều phần tử cùng lúc nếu không cần thiết.
AnimatedModalBarrier giống như một người quản lý sân khấu tài ba. Nó không chỉ kéo màn lên hay hạ màn xuống, mà còn làm điều đó với sự duyên dáng, tạo ra những khoảnh khắc chuyển tiếp mượt mà, giúp trải nghiệm của khán giả (người dùng) trở nên trọn vẹn và đáng nhớ hơn. Nắm vững nó, bạn sẽ có thêm một công cụ mạnh mẽ để tạo ra các giao diện Flutter không chỉ đẹp mà còn cực kỳ linh hoạt và chuyên nghiệp!
Thuộc Series: Flutter
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!