Hướng dẫn "AnimatedPositionedDirectional" - Flutter
Flutter

Hướng dẫn "AnimatedPositionedDirectional" - Flutter

Author

Admin System

@root

Ngày xuất bản

18 Mar, 2026

Lượt xem

2 Lượt

"AnimatedPositionedDirectional"

Chào mừng các bạn đến với buổi học hôm nay, nơi chúng ta sẽ cùng mổ xẻ một viên ngọc ẩn của Flutter, thứ mà nhiều bạn lập trình viên thường bỏ qua cho đến khi "đụng chuyện" làm ứng dụng đa ngôn ngữ. Hôm nay, chúng ta sẽ "giải phẫu" AnimatedPositionedDirectional.

1. AnimatedPositionedDirectional là gì và để làm gì?

Hãy hình dung thế này: Bạn là một đạo diễn sân khấu tài ba, và bạn muốn di chuyển một diễn viên (chính là widget của bạn) trên sân khấu (một Stack trong Flutter) một cách mượt mà, uyển chuyển.

  • AnimatedPositioned giống như bạn ra lệnh "Di chuyển diễn viên đến vị trí 10 bước từ mép trái, 20 bước từ mép trên." Rõ ràng, cụ thể, không thể nhầm lẫn. Mép trái là mép trái, dù bạn có đang đọc kịch bản từ trái sang phải hay phải sang trái.
  • Nhưng AnimatedPositionedDirectional thì lại "cao cấp" hơn một chút. Nó giống như bạn ra lệnh "Di chuyển diễn viên đến vị trí 10 bước từ điểm BẮT ĐẦU của dòng chữ trên kịch bản, 20 bước từ mép trên."

Cái "điểm BẮT ĐẦU của dòng chữ" này chính là mấu chốt.

  • Nếu kịch bản viết từ trái sang phải (như tiếng Việt, tiếng Anh - gọi là LTR: Left-To-Right), thì "điểm bắt đầu" là mép trái.
  • Nhưng nếu kịch bản viết từ phải sang trái (như tiếng Ả Rập, tiếng Hebrew - gọi là RTL: Right-To-Left), thì "điểm bắt đầu" lại là mép phải!

Vậy nên, AnimatedPositionedDirectional là phiên bản "nhạy cảm với hướng văn bản" của AnimatedPositioned. Nó sử dụng các thuộc tính startend thay vì leftright. Điều này giúp ứng dụng của bạn tự động thích nghi một cách duyên dáng khi người dùng thay đổi cài đặt ngôn ngữ hoặc hướng đọc trên thiết bị của họ. Đây là một chi tiết nhỏ nhưng cực kỳ quan trọng để ứng dụng của bạn "quốc tế hóa" một cách chuyên nghiệp, tránh những lỗi layout ngớ ngẩn khi chuyển sang ngôn ngữ RTL.

Nói cách khác, nó là một widget implicit animation (hoạt ảnh ngầm định), nghĩa là bạn chỉ cần thay đổi các thuộc tính vị trí của nó (như start, end, top, bottom, width, height), và Flutter sẽ tự động lo phần chuyển động mượt mà giữa các trạng thái, với một duration mà bạn định nghĩa.

Illustration

2. Code Ví Dụ Minh Họa Rõ Ràng, Ngầu

Để thấy rõ sự "ngầu" của nó, chúng ta sẽ tạo một ví dụ đơn giản với một hộp màu di chuyển qua lại, và nó sẽ tự động đảo chiều di chuyển nếu bạn thay đổi Directionality của ứng dụng.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // Biến để kiểm soát hướng văn bản của ứng dụng
  TextDirection _textDirection = TextDirection.ltr;

  void _toggleTextDirection() {
    setState(() {
      _textDirection = _textDirection == TextDirection.ltr
          ? TextDirection.rtl
          : TextDirection.ltr;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AnimatedPositionedDirectional Demo',
      // Widget Directionality bao bọc toàn bộ ứng dụng để thay đổi hướng văn bản
      builder: (context, child) {
        return Directionality(
          textDirection: _textDirection,
          child: child!,
        );
      },
      home: Scaffold(
        appBar: AppBar(
          title: const Text('AnimatedPositionedDirectional Demo'),
          actions: [
            IconButton(
              icon: const Icon(Icons.swap_horiz),
              onPressed: _toggleTextDirection,
              tooltip: 'Toggle Text Direction (LTR/RTL)',
            ),
          ],
        ),
        body: const Center(
          child: AnimatedBoxMover(),
        ),
      ),
    );
  }
}

class AnimatedBoxMover extends StatefulWidget {
  const AnimatedBoxMover({super.key});

  @override
  State<AnimatedBoxMover> createState() => _AnimatedBoxMoverState();
}

class _AnimatedBoxMoverState extends State<AnimatedBoxMover> {
  bool _isAtStart = true; // Biến kiểm soát vị trí của hộp

  void _togglePosition() {
    setState(() {
      _isAtStart = !_isAtStart;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        // Một Container lớn làm nền để dễ hình dung không gian
        Container(
          width: 300,
          height: 100,
          decoration: BoxDecoration(
            color: Colors.grey[200],
            borderRadius: BorderRadius.circular(10),
            border: Border.all(color: Colors.grey, width: 2),
          ),
        ),
        // Đây là ngôi sao của chúng ta: AnimatedPositionedDirectional
        AnimatedPositionedDirectional(
          // Thời gian chuyển động
          duration: const Duration(milliseconds: 500),
          curve: Curves.easeInOut, // Kiểu đường cong chuyển động

          // Vị trí "start" (từ điểm bắt đầu của dòng chữ)
          // Nếu _isAtStart là true, hộp sẽ ở cách điểm bắt đầu 10.0
          // Nếu _isAtStart là false, hộp sẽ không có giá trị start,
          // mà sẽ được đẩy về phía "end" (điểm kết thúc của dòng chữ)
          start: _isAtStart ? 10.0 : null,

          // Vị trí "end" (từ điểm kết thúc của dòng chữ)
          // Nếu _isAtStart là true, hộp sẽ không có giá trị end
          // Nếu _isAtStart là false, hộp sẽ ở cách điểm kết thúc 10.0
          end: _isAtStart ? null : 10.0,

          // Vị trí từ trên xuống (giữ nguyên)
          top: 25.0,
          // Chiều rộng và chiều cao của hộp
          width: 50.0,
          height: 50.0,
          child: Container(
            decoration: BoxDecoration(
              color: Colors.deepPurple,
              borderRadius: BorderRadius.circular(8),
              boxShadow: const [
                BoxShadow(
                  color: Colors.black26,
                  blurRadius: 8,
                  offset: Offset(0, 4),
                ),
              ],
            ),
            alignment: Alignment.center,
            child: Text(
              _isAtStart ? 'START' : 'END',
              style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ),
        ),
        // Nút bấm để thay đổi vị trí của hộp
        Positioned(
          bottom: -50, // Đặt nút bên dưới Stack để không che hộp
          child: ElevatedButton(
            onPressed: _togglePosition,
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.blueAccent,
              foregroundColor: Colors.white,
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(30),
              ),
            ),
            child: const Text('Di chuyển hộp'),
          ),
        ),
      ],
    );
  }
}

Cách hoạt động của ví dụ:

  1. Ban đầu, _textDirectionTextDirection.ltr (Trái sang Phải).
  2. Khi bạn nhấn nút "Di chuyển hộp", biến _isAtStart sẽ chuyển đổi.
    • Nếu _isAtStarttrue, AnimatedPositionedDirectional sẽ có start: 10.0end: null. Hộp sẽ nằm cách mép trái (start) 10 đơn vị.
    • Nếu _isAtStartfalse, AnimatedPositionedDirectional sẽ có start: nullend: 10.0. Hộp sẽ nằm cách mép phải (end) 10 đơn vị.
  3. Điểm đặc biệt: Bây giờ, hãy nhấn nút Icons.swap_horiz trên AppBar để chuyển _textDirection thành TextDirection.rtl (Phải sang Trái).
    • Bạn sẽ thấy hộp ngay lập tức nhảy sang vị trí mới mà không cần thay đổi code vị trí của hộp.
    • Khi bạn nhấn "Di chuyển hộp" lần nữa, nó vẫn sẽ di chuyển giữa startend, nhưng giờ đây start là mép phải và end là mép trái!

Thấy chưa? AnimatedPositionedDirectional đã giúp chúng ta xử lý sự khác biệt LTR/RTL một cách hoàn toàn tự động, chỉ bằng cách sử dụng startend thay vì leftright.

3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế

  1. Luôn ưu tiên Directional nếu có thể: Nếu ứng dụng của bạn có khả năng hỗ trợ đa ngôn ngữ (LTR/RTL), hãy luôn ưu tiên dùng AnimatedPositionedDirectional (và các widget Directional khác như PaddingDirectional, MarginDirectional) thay vì các phiên bản không có Directional (AnimatedPositioned, Padding, Margin). Nó giúp ứng dụng của bạn "tự thích nghi" mà không cần code logic riêng cho từng hướng, tiết kiệm thời gian và tránh lỗi.
  2. Hiểu rõ Directionality: AnimatedPositionedDirectional hoạt động dựa trên Directionality của ngữ cảnh widget. Nếu bạn không khai báo Directionality rõ ràng (ví dụ, thông qua MaterialApp hoặc một widget Directionality cụ thể), nó sẽ mặc định là TextDirection.ltr. Hãy chắc chắn rằng Directionality của ứng dụng hoặc phần UI bạn muốn hoạt ảnh là chính xác.
  3. Luôn nằm trong Stack: Giống như Positioned, AnimatedPositionedDirectional chỉ có ý nghĩa khi là con của một Stack. Nó dùng Stack làm "sân khấu" để định vị widget con một cách tương đối.
  4. Implicit Animation - Sức mạnh của sự đơn giản: Đây là một animation ngầm định (implicit). Bạn chỉ cần thay đổi các thuộc tính vị trí (như start, end, top, bottom, width, height), Flutter sẽ tự động lo phần chuyển động mượt mà. Đừng cố gắng tự viết AnimationController hay Tween cho nó trừ khi bạn cần kiểm soát cực kỳ chi tiết một hoạt ảnh phức tạp hơn. Với các chuyển động vị trí đơn giản, đây là lựa chọn tối ưu về hiệu suất và dễ dùng.
  5. Kết hợp linh hoạt: Bạn có thể kết hợp startend với top, bottom, width, height để tạo ra các hiệu ứng chuyển động đa dạng. Ví dụ, để một widget giãn ra từ start đến end, bạn có thể bỏ width và chỉ định startend.

Với AnimatedPositionedDirectional, bạn không chỉ di chuyển widget một cách mượt mà, mà còn đảm bảo ứng dụng của mình "thông minh" và thích ứng tốt với mọi ngôn ngữ, mọi người dùng. Đó chính là phong thái của một lập trình viên 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é!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!