CompositedTransformFollower: Ma Thuật Định Vị Đa Chiều trong Flutter
Chào các lập trình viên tương lai, hôm nay chúng ta sẽ cùng giải mã một trong những “phép thuật” định vị widget khá nâng cao trong Flutter: CompositedTransformFollower. Tưởng tượng thế này, bạn có một con tàu vũ trụ chính (widget A) đang lướt đi trong dải ngân hà UI của mình. Bây giờ, bạn muốn phóng một con tàu con (widget B) từ tàu chính đó, và con tàu con này phải luôn bay theo sát tàu mẹ, giữ một khoảng cách nhất định, dù tàu mẹ có di chuyển đến đâu, thậm chí là “lặn” xuống một tầng không gian khác. Nghe có vẻ phức tạp phải không? Đó chính là lúc CompositedTransformFollower xuất hiện! CompositedTransformFollower là gì và để làm gì? Trong thế giới phẳng của các widget Flutter, mọi thứ thường được sắp xếp theo một hệ thống phân cấp chặt chẽ – con nằm trong cha, cha nằm trong ông. Điều này tuyệt vời cho hầu hết các tác vụ bố cục. Tuy nhiên, đôi khi chúng ta cần một widget thoát ly khỏi sự ràng buộc của cha mẹ nó, nhưng vẫn phụ thuộc vào vị trí của một widget khác ở đâu đó rất xa trong cây widget, thậm chí là ở một tầng rendering khác. Ví dụ điển hình là các tooltip, dropdown menu, hoặc context menu – chúng cần xuất hiện ngay cạnh một nút bấm, nhưng lại phải nổi lên trên tất cả các nội dung khác. CompositedTransformFollower (tôi gọi nó là “vệ tinh theo dõi”) là một widget cho phép bạn định vị nó tương đối so với một CompositedTransformTarget (tôi gọi là “nguồn phát tín hiệu”) cụ thể. Điểm đặc biệt là, nó không bị giới hạn bởi ranh giới của cha mẹ nó, mà có thể “bay” tự do trên các lớp (layers) rendering khác, nhờ vào cơ chế compositing của Flutter. Để hai widget này “liên lạc” được với nhau, chúng cần một sợi dây liên kết ma thuật: LayerLink. Cả CompositedTransformTarget và CompositedTransformFollower đều phải chia sẻ cùng một instance LayerLink để biết mình đang theo dõi hoặc được theo dõi bởi ai. Code Ví Dụ Minh Họa: Tạo Tooltip Nổi Bật Hãy cùng xây dựng một ví dụ kinh điển: một nút bấm, khi được nhấn, sẽ hiển thị một tooltip nhỏ ngay bên cạnh nó. Để tooltip này thực sự “nổi” lên trên mọi thứ, chúng ta sẽ kết hợp CompositedTransformFollower với OverlayEntry. 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: 'Flutter CompositedTransformFollower Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const TooltipDemoPage(), ); } } class TooltipDemoPage extends StatefulWidget { const TooltipDemoPage({super.key}); @override State<TooltipDemoPage> createState() => _TooltipDemoPageState(); } class _TooltipDemoPageState extends State<TooltipDemoPage> { // 1. Khai báo LayerLink: sợi dây liên kết giữa target và follower final LayerLink _layerLink = LayerLink(); OverlayEntry? _overlayEntry; // Để quản lý tooltip nổi void _showOverlay(BuildContext context) { if (_overlayEntry != null) return; // Tránh tạo nhiều overlay _overlayEntry = OverlayEntry( builder: (context) => Positioned( // Sử dụng CompositedTransformFollower để định vị tooltip child: CompositedTransformFollower( link: _layerLink, // Cùng LayerLink với CompositedTransformTarget targetAnchor: Alignment.bottomLeft, // Vị trí neo của target (góc dưới bên trái của nút) followerAnchor: Alignment.topLeft, // Vị trí neo của follower (góc trên bên trái của tooltip) offset: const Offset(0, 8), // Dịch chuyển tooltip xuống 8 pixel từ vị trí neo child: Material( elevation: 4.0, child: Container( padding: const EdgeInsets.all(8.0), color: Colors.yellow[100], child: const Text('Đây là tooltip của bạn!'), ), ), ), ), ); // Thêm OverlayEntry vào Overlay của ứng dụng Overlay.of(context).insert(_overlayEntry!); } void _hideOverlay() { _overlayEntry?.remove(); // Gỡ bỏ tooltip khỏi Overlay _overlayEntry = null; } @override void dispose() { _hideOverlay(); // Đảm bảo tooltip được gỡ bỏ khi widget bị dispose super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('CompositedTransformFollower Demo')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 2. CompositedTransformTarget: Widget nguồn phát tín hiệu (nút bấm) CompositedTransformTarget( link: _layerLink, // Sợi dây liên kết child: ElevatedButton( onPressed: () { if (_overlayEntry == null) { _showOverlay(context); } else { _hideOverlay(); } }, child: const Text('Nhấn để xem Tooltip'), ), ), const SizedBox(height: 100), const Text('Nội dung khác trong trang...'), ], ), ), ); } } Giải thích code: _layerLink = LayerLink(): Đây là chìa khóa. Một instance LayerLink duy nhất được chia sẻ giữa CompositedTransformTarget và CompositedTransformFollower để chúng biết “đối tác” của mình là ai. CompositedTransformTarget: Bọc quanh ElevatedButton. Widget này đánh dấu vị trí mà CompositedTransformFollower sẽ theo dõi. Nó không làm thay đổi bố cục của ElevatedButton. _showOverlay(BuildContext context): Hàm này tạo và chèn một OverlayEntry vào Overlay của ứng dụng. OverlayEntry là cách để chúng ta “nổi” một widget lên trên tất cả các widget khác trong cây widget, như một lớp kính trong suốt. CompositedTransformFollower: Đây là trái tim của ví dụ. Nó được đặt bên trong OverlayEntry: link: _layerLink: Kết nối với CompositedTransformTarget thông qua _layerLink. targetAnchor: Alignment.bottomLeft: Chỉ định điểm neo trên CompositedTransformTarget. Ở đây là góc dưới bên trái của nút bấm. followerAnchor: Alignment.topLeft: Chỉ định điểm neo trên chính CompositedTransformFollower. Ở đây là góc trên bên trái của tooltip. offset: const Offset(0, 8): Dịch chuyển tooltip thêm 8 pixel xuống dưới từ vị trí neo, tạo ra một khoảng trống nhỏ giữa nút và tooltip. Khi bạn nhấn nút, _showOverlay được gọi, tạo ra một OverlayEntry chứa CompositedTransformFollower. Follower này sẽ tự động định vị tooltip ngay bên cạnh nút bấm, dù nút bấm có nằm ở đâu trên màn hình đi chăng nữa. Mẹo và Best Practices để làm chủ CompositedTransformFollower Luôn đi theo cặp: CompositedTransformFollower không có ý nghĩa gì nếu không có CompositedTransformTarget tương ứng. Chúng là một cặp bài trùng không thể tách rời. LayerLink là linh hồn: Đảm bảo cả Target và Follower đều sử dụng cùng một instance LayerLink. Nếu không, chúng sẽ không thể “nhận ra” nhau. Kết hợp với Overlay cho hiệu ứng nổi: Để widget của bạn thực sự “nổi” lên trên các nội dung khác mà không bị cắt xén bởi cha mẹ nó, hãy đặt CompositedTransformFollower vào trong một OverlayEntry và chèn nó vào Overlay.of(context). Tùy chỉnh vị trí với targetAnchor, followerAnchor và offset: Đây là bộ ba quyền lực giúp bạn định vị Follower một cách chính xác. Hãy hình dung targetAnchor là điểm bạn muốn “bắn” tia laze từ Target, và followerAnchor là điểm trên Follower mà tia laze đó sẽ “chạm tới”. offset là dịch chuyển thêm sau khi đã neo. Quản lý OverlayEntry cẩn thận: Khi không còn cần tooltip/dropdown, hãy gọi _overlayEntry?.remove() để giải phóng tài nguyên. Việc quên xóa OverlayEntry có thể dẫn đến rò rỉ bộ nhớ hoặc các lỗi UI khó chịu. showWhenUnlinked: Thuộc tính này (mặc định là true) cho phép Follower vẫn hiển thị ngay cả khi Target không còn tồn tại trong cây widget hoặc không còn liên kết. Trong hầu hết các trường hợp, bạn muốn nó là false để khi target biến mất thì follower cũng biến mất. Ứng dụng Thực Tế của CompositedTransformFollower CompositedTransformFollower là một công cụ cực kỳ mạnh mẽ, được sử dụng rộng rãi trong các ứng dụng Flutter để tạo ra trải nghiệm người dùng mượt mà và trực quan: Dropdown Menus: Các menu thả xuống (như menu chọn ngày, chọn danh mục) luôn xuất hiện ngay dưới hoặc bên cạnh nút kích hoạt của chúng. Tooltips & Popovers: Các hộp thoại nhỏ bật lên cung cấp thông tin chi tiết khi người dùng tương tác với một phần tử UI. Context Menus: Menu hiển thị khi người dùng nhấn giữ (long-press) hoặc click chuột phải vào một đối tượng, ví dụ như menu “Copy”, “Paste”, “Delete” trên một item trong danh sách. Autocomplete Suggestions: Khi bạn gõ vào một trường nhập liệu, danh sách gợi ý sẽ xuất hiện ngay bên dưới trường đó. Custom Modals/Dialogs: Đôi khi bạn cần một cửa sổ pop-up không phải là một dialog toàn màn hình mà được neo vào một phần tử cụ thể. Các ứng dụng/website lớn như Google Docs (menu ngữ cảnh), Figma (menu dropdown của các thuộc tính), hoặc thậm chí là các thành phần UI phức tạp trong giao diện người dùng game đều có thể sử dụng các nguyên lý tương tự để định vị các phần tử UI tương tác một cách linh hoạt. Kết luận CompositedTransformFollower có thể trông phức tạp lúc đầu, nhưng khi bạn hiểu được vai trò của nó như một “vệ tinh” được “buộc” vào một “nguồn phát tín hiệu” bằng sợi dây LayerLink và được phép bay tự do trên các lớp rendering, bạn sẽ thấy nó là một công cụ vô giá để tạo ra các giao diện người dùng Flutter tinh tế và tương tác cao. Hãy thực hành, thử nghiệm với các targetAnchor, followerAnchor, offset khác nhau, và bạn sẽ sớm làm chủ được phép thuật định vị đa chiều này! 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é!