
Hãy tưởng tượng bạn đang cầm trên tay một chiếc kính lúp vạn năng, có thể phóng to, thu nhỏ, kéo qua kéo lại bất kỳ tấm ảnh hay bản đồ nào. Trong Flutter, cái "kính lúp" đó chính là InteractiveViewer – một widget siêu tiện lợi giúp bạn làm điều đó một cách dễ dàng. Nó nhận một child (ví dụ: một Image, một Container chứa nội dung phức tạp) và biến nó thành một khu vực có thể tương tác: zoom, pan (kéo), thậm chí là rotate (xoay).
InteractiveViewerState là gì? Để làm gì?
Vậy còn InteractiveViewerState? À ha, đây chính là cái bảng điều khiển trung tâm, hay nói cách khác là trái tim của chiếc kính lúp thần kỳ đó. InteractiveViewerState là đối tượng quản lý toàn bộ trạng thái hiện tại của InteractiveViewer. Nó biết được:
- Bạn đang phóng to đến mức nào (scale factor)?
- Bạn đang kéo nội dung dịch chuyển bao nhiêu (pan offset)?
- Và tất cả những thông tin về ma trận biến đổi (transformation matrix) đang được áp dụng lên
childcủa bạn.
Nói một cách đơn giản, nếu InteractiveViewer là cái xe bus cho phép người dùng tự do lái (kéo, zoom), thì InteractiveViewerState chính là cái bảng đồng hồ hiển thị tốc độ, vị trí, và tất cả thông số hiện hành của chuyến đi đó.
Vậy tại sao chúng ta cần "đụng chạm" vào nó? Thường thì người dùng tự do tương tác là đủ rồi. Nhưng đôi khi, bạn muốn trở thành "người điều khiển từ xa", muốn lập trình để:
- Reset lại chế độ xem về trạng thái ban đầu (ví dụ: nút "Đặt lại").
- Tự động phóng to vào một điểm cụ thể trên bản đồ khi người dùng nhấn vào.
- Hoặc đơn giản là muốn biết hiện tại người dùng đang xem ở mức độ phóng to nào để điều chỉnh UI khác.
Đây chính là lúc InteractiveViewerState phát huy tác dụng. Mặc dù cách tốt nhất để kiểm soát InteractiveViewer từ bên ngoài là thông qua TransformationController, nhưng InteractiveViewerState vẫn là nơi chứa và phản ánh trạng thái đó, và cung cấp một số phương thức tiện ích.

Code Ví Dụ Minh Hoạ: "Người Lái Xe Bus" và "Bảng Điều Khiển"
Để điều khiển chiếc kính lúp này một cách có chủ đích, chúng ta sẽ cần một "người lái xe bus" riêng, đó chính là TransformationController. Và chiếc TransformationController này sẽ làm việc chặt chẽ với InteractiveViewerState.
Hãy cùng xem ví dụ đơn giản sau: Chúng ta có một tấm ảnh, và một nút bấm để "Đặt lại" chế độ xem về ban đầu.
import 'package:flutter/material.dart';
class InteractiveViewerDemo extends StatefulWidget {
const InteractiveViewerDemo({super.key});
@override
State<InteractiveViewerDemo> createState() => _InteractiveViewerDemoState();
}
class _InteractiveViewerDemoState extends State<InteractiveViewerDemo> {
// Đây là "người lái xe bus" của chúng ta.
// Nó sẽ điều khiển trạng thái phóng to/kéo của InteractiveViewer.
final TransformationController _transformationController = TransformationController();
@override
void dispose() {
_transformationController.dispose(); // Nhớ dọn dẹp "người lái xe bus" khi không dùng nữa!
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Kính Lúp Thần Kỳ của Creyt'),
),
body: Center(
child: Column(
children: [
Expanded(
child: InteractiveViewer(
// Giao "người lái xe bus" cho InteractiveViewer.
transformationController: _transformationController,
boundaryMargin: const EdgeInsets.all(20.0),
minScale: 0.1,
maxScale: 4.0,
child: Image.network(
'https://picsum.photos/seed/flutter/800/600', // Một bức ảnh ngẫu nhiên
fit: BoxFit.contain,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
// Này "người lái xe bus", đưa tôi về điểm xuất phát đi!
_transformationController.value = Matrix4.identity();
// Hoặc bạn có thể dùng Animation để reset mượt mà hơn:
// _transformationController.animateTo(
// Matrix4.identity(),
// duration: const Duration(milliseconds: 300),
// curve: Curves.easeOut,
// );
// Để đọc trạng thái hiện tại từ InteractiveViewerState (nếu bạn cần):
// Nếu bạn muốn truy cập InteractiveViewerState trực tiếp mà không dùng controller,
// bạn sẽ cần một GlobalKey cho InteractiveViewer và dùng key.currentState.
// Nhưng với việc điều khiển, TransformationController là cách chuẩn mực hơn.
// Ví dụ: Để lấy scale hiện tại từ controller:
// final currentScale = _transformationController.value.getMaxScaleOnAxis();
// print('Current Scale: $currentScale');
},
child: const Text('Đặt Lại Chế Độ Xem'),
),
),
],
),
),
);
}
}
Trong ví dụ trên:
- Chúng ta tạo một
TransformationController(_transformationController). Đây là công cụ chính để điều khiểnInteractiveViewermột cách lập trình. - Khi bạn gán
_transformationControllervàoInteractiveViewer, mọi tương tác của người dùng (zoom, pan) sẽ được phản ánh vào_transformationController.value. Ngược lại, khi bạn thay đổi_transformationController.value(như khi nhấn nút "Đặt Lại"),InteractiveViewersẽ tự động cập nhật hiển thị của nó. InteractiveViewerStatechính là nơi lưu trữ cáivaluenày và các trạng thái nội bộ khác. Mặc dù chúng ta không trực tiếp gọi_interactiveViewerKey.currentStatetrong ví dụ này để reset, nhưngTransformationControllerchính là "cầu nối" hiệu quả nhất để tương tác với trạng thái đó. Nếu bạn muốn truy cập các phương thức củaInteractiveViewerStatemà không dùngTransformationController, bạn sẽ cần mộtGlobalKeygắn vàoInteractiveViewer.
Mẹo Nhỏ và Best Practices từ Giảng viên Creyt
- Dùng
TransformationControllernhư bạn thân: Khi bạn muốn điều khiểnInteractiveViewerbằng code (reset, pan đến điểm cụ thể, zoom tự động), hãy nghĩ ngay đếnTransformationController. Nó sinh ra là để làm việc này! Nhớdispose()nó khiStatekhông còn được sử dụng để tránh rò rỉ bộ nhớ. - Hiệu năng là vàng:
InteractiveViewerrất mạnh mẽ, nhưng nếu bạn nhét vào đó một widget con quá phức tạp hoặc một tấm ảnh siêu to khổng lồ, việc phóng to/thu nhỏ có thể không mượt mà. Hãy tối ưu widget con nếu có thể, hoặc cân nhắc việc hiển thị phiên bản độ phân giải thấp hơn khi zoom out và tải phiên bản chất lượng cao khi zoom in. buildervschild: Nếu nội dung của bạn thay đổi động hoặc cần được xây dựng dựa trên trạng thái phóng to/kéo, hãy cân nhắc dùngInteractiveViewer.builderthay vìInteractiveViewervớichildthông thường.buildercung cấpBuildContextvàMatrix4hiện tại, giúp bạn xây dựng widget con linh hoạt hơn.boundaryMarginvàminScale/maxScale: Luôn định nghĩa rõ ràng các thuộc tính này để kiểm soát giới hạn tương tác của người dùng, tránh việc nội dung bị kéo ra khỏi màn hình hoàn toàn hoặc zoom quá lố.
Ứng dụng Thực tế: "Kính Lúp" ở khắp mọi nơi
Bạn có thể thấy InteractiveViewer (và gián tiếp là InteractiveViewerState) được sử dụng ở rất nhiều nơi trong các ứng dụng hàng ngày:
- Ứng dụng bản đồ: Google Maps, Apple Maps, hay bất kỳ ứng dụng bản đồ nào bạn từng dùng đều cần khả năng phóng to, kéo bản đồ mượt mà.
- Ứng dụng xem ảnh/PDF: Khi bạn mở một bức ảnh độ phân giải cao hoặc một tài liệu PDF, bạn thường muốn zoom vào chi tiết, kéo để xem các phần khác nhau.
- Ứng dụng thiết kế/CAD: Các phần mềm xem bản vẽ kỹ thuật, sơ đồ mạch điện thường cho phép người dùng phóng to các chi tiết nhỏ, kéo để di chuyển giữa các khu vực.
- Trình duyệt web: Một số website có tính năng zoom vào nội dung ảnh hoặc biểu đồ lớn.
Tóm lại, InteractiveViewerState là "bộ não" giữ thông tin về trạng thái tương tác của InteractiveViewer. Và TransformationController là "người lái xe" giúp bạn điều khiển bộ não đó một cách có chủ đích. Nắm vững bộ đôi này, bạn sẽ có trong tay sức mạnh để tạo ra những trải nghiệm người dùng thực sự sống động và linh hoạt trong ứng dụng Flutter của mình. Chúc mừng bạn đã lên thêm một level nữa trong hành trình trở thành "phù thủy" code!
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é!