CustomMultiChildLayout: Khi Layout Chuẩn Không Đủ Sức Chơi
Flutter

CustomMultiChildLayout: Khi Layout Chuẩn Không Đủ Sức Chơi

Author

Admin System

@root

Ngày xuất bản

18 Mar, 2026

Lượt xem

19 Lượt

"CustomMultiChildLayout"

Chào các "kiến trúc sư" tương lai của vũ trụ Flutter! Anh Creyt đây, và hôm nay chúng ta sẽ cùng nhau "đục khoét" một trong những công cụ mạnh mẽ nhưng ít được biết đến, cái tên nghe có vẻ hơi "nguy hiểm" nhưng lại cực kỳ thần thánh: CustomMultiChildLayout.

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

Em hình dung thế này, khi em xây nhà bằng LEGO, em có các khối hình chữ nhật, hình vuông, em cứ xếp chồng lên nhau, đặt cạnh nhau. Đó là Row, Column, Stack – những layout widget cơ bản, "mì ăn liền" của Flutter. Chúng rất tiện, rất nhanh, nhưng đôi khi em muốn xây một cái tháp Eiffel, hay một con rồng uốn lượn, thì mấy khối LEGO hình chữ nhật kia… chào thua!

CustomMultiChildLayout chính là lúc em vứt hết mấy cái khối LEGO đóng gói sẵn đó đi, và tự tay đẽo gọt từng viên gạch, từng thanh sắt, rồi em tự tay đặt chúng vào đúng vị trí em muốn, với kích thước em mong muốn. Nó là một widget cho phép em hoàn toàn kiểm soát việc đo lường (measure) và định vị (position) các widget con của nó. Em không còn bị ràng buộc bởi các quy tắc bố cục có sẵn nữa.

Để làm gì ư? Khi em cần một bố cục mà không có bất kỳ widget nào của Flutter (hay package bên thứ ba) có thể cung cấp. Ví dụ: sắp xếp các avatar theo hình tròn, tạo một biểu đồ phức tạp với các nhãn tùy chỉnh, một giao diện người dùng game độc đáo, hoặc bất kỳ thứ gì yêu cầu sự chính xác đến từng pixel và không theo khuôn mẫu.

Nói tóm lại, nó là "kế hoạch B" (hay "kế hoạch Z" thì đúng hơn) khi mọi giải pháp layout khác đều "bó tay chấm com".

Illustration

2. Code Ví Dụ Minh Hoạ: Bố Cục "Fan" Độc Đáo

Để em dễ hình dung, chúng ta hãy tạo một bố cục "fan" (cánh quạt) đơn giản, nơi các widget con được sắp xếp xòe ra như một chiếc quạt giấy. Điều này không thể làm dễ dàng với Row hay Stack thông thường.

Để sử dụng CustomMultiChildLayout, em cần hai thứ:

Gợi Ý Đọc Tiếp
Hướng dẫn "AnimatedTheme" - Flutter

1 Lượt xem

  1. CustomMultiChildLayout Widget: Cái khung chứa. Nó nhận một danh sách children và một delegate.
  2. MultiChildLayoutDelegate: Đây là "bộ não", nơi chứa logic đo lường và định vị. Em phải kế thừa lớp này và override hai phương thức quan trọng: performLayoutshouldRelayout.

Đặc biệt, mỗi widget con trong CustomMultiChildLayout cần được bọc bởi một LayoutId. LayoutId này có một id duy nhất mà em sẽ dùng để tham chiếu đến widget con đó trong delegate của mình.

import 'dart:math';
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: 'Custom Multi-Child Layout Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('CustomMultiChildLayout Fan Demo'),
        ),
        body: Center(
          child: Container(
            color: Colors.grey[200],
            width: 300,
            height: 300,
            child: CustomMultiChildLayout(
              delegate: FanLayoutDelegate(),
              children: [
                LayoutId(
                  id: 'item1',
                  child: Container(
                    width: 50, height: 50, color: Colors.red,
                    alignment: Alignment.center,
                    child: const Text('1', style: TextStyle(color: Colors.white)),
                  ),
                ),
                LayoutId(
                  id: 'item2',
                  child: Container(
                    width: 50, height: 50, color: Colors.green,
                    alignment: Alignment.center,
                    child: const Text('2', style: TextStyle(color: Colors.white)),
                  ),
                ),
                LayoutId(
                  id: 'item3',
                  child: Container(
                    width: 50, height: 50, color: Colors.blue,
                    alignment: Alignment.center,
                    child: const Text('3', style: TextStyle(color: Colors.white)),
                  ),
                ),
                LayoutId(
                  id: 'item4',
                  child: Container(
                    width: 50, height: 50, color: Colors.purple,
                    alignment: Alignment.center,
                    child: const Text('4', style: TextStyle(color: Colors.white)),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// Bộ não của Fan Layout
class FanLayoutDelegate extends MultiChildLayoutDelegate {
  @override
  void performLayout(Size size) {
    // Kích thước của CustomMultiChildLayout (Container 300x300)
    final double parentWidth = size.width;
    final double parentHeight = size.height;

    // Tâm của vòng cung (góc dưới bên trái của parent)
    final Offset center = Offset(0, parentHeight);

    // Bán kính của vòng cung
    const double radius = 150.0;

    // Góc bắt đầu và kết thúc của quạt (tính bằng radian)
    // Ví dụ: từ 90 độ (pi/2) đến 0 độ (0) quay ngược kim đồng hồ
    const double startAngle = pi / 2; // Bắt đầu từ 90 độ (trên trục Y)
    const double endAngle = 0;      // Kết thúc ở 0 độ (trên trục X)
    
    // Số lượng item
    final int itemCount = layoutChildren.length;

    // Tính toán góc giữa các item
    final double angleStep = (startAngle - endAngle) / (itemCount > 1 ? (itemCount - 1) : 1);

    // Duyệt qua từng item và định vị chúng
    for (int i = 0; i < itemCount; i++) {
      final Object? childId = 'item${i + 1}'; // Lấy ID của con

      if (hasChild(childId)) {
        // Bước 1: Đo lường kích thước của từng con
        // constraint: Kích thước tối đa mà con có thể có (ở đây là không giới hạn)
        final Size childSize = layoutChild(childId, BoxConstraints.loose(size));

        // Tính toán góc hiện tại cho item này
        final double currentAngle = startAngle - (angleStep * i);

        // Tính toán vị trí X, Y trên vòng cung
        // Lưu ý: cos(angle) cho X, sin(angle) cho Y
        // Trừ đi childSize.width/2 và childSize.height/2 để đặt tâm của child vào đúng vị trí
        final double x = center.dx + (radius * cos(currentAngle)) - (childSize.width / 2);
        final double y = center.dy - (radius * sin(currentAngle)) - (childSize.height / 2); // Trừ vì Y tăng xuống dưới

        // Bước 2: Định vị con
        positionChild(childId, Offset(x, y));
      }
    }
  }

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
    // Trả về true nếu cần bố cục lại (ví dụ: khi dữ liệu thay đổi)
    // Trong ví dụ này, layout không thay đổi nên luôn false.
    return false;
  }
}

Giải thích sơ bộ:

  • performLayout(Size size): Đây là trái tim của delegate. size chính là kích thước của CustomMultiChildLayout (trong ví dụ là 300x300). Em dùng layoutChild(id, constraints) để đo kích thước của từng con, và positionChild(id, offset) để đặt vị trí của nó. Logic tính toán x, y dựa trên hình học (góc và bán kính) để tạo ra hiệu ứng quạt.
  • shouldRelayout(oldDelegate): Phương thức này quyết định liệu performLayout có cần chạy lại hay không khi widget thay đổi. Nếu layout của em phụ thuộc vào các tham số thay đổi (ví dụ: số lượng item, bán kính, góc), em sẽ cần so sánh các tham số đó giữa this (delegate hiện tại) và oldDelegate để trả về true khi cần re-layout.

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

Anh Creyt có vài lời khuyên chân thành thế này:

  • Khi nào dùng? Chỉ khi nào Row, Column, Stack, Wrap, GridView, Flow hay Table đều "bó tay". CustomMultiChildLayout là một công cụ mạnh, nhưng cũng như "dao mổ trâu", đừng lôi ra mổ gà. Nó phức tạp hơn, có thể tốn tài nguyên hơn nếu không được viết cẩn thận.
  • Tư duy "Cha Mẹ" Tuyệt Đối: Em là bố/mẹ của các widget con. Em có toàn quyền đo đạc (measure) và đặt vị trí (position) chúng. Các con không được phép tự quyết định kích thước hay vị trí của mình (trừ khi em truyền BoxConstraints.loose để chúng tự co giãn).
  • LayoutId là chìa khóa: Luôn nhớ gán một LayoutId duy nhất cho mỗi widget con mà em muốn thao tác trong delegate. Nó giống như số căn cước công dân để em gọi tên từng đứa con vậy.
  • Hiểu về BoxConstraints: Khi em gọi layoutChild(id, constraints), constraints là giới hạn mà em đặt ra cho widget con. BoxConstraints.loose(size) nghĩa là "con được phép lớn tối đa bằng size nhưng cũng có thể nhỏ hơn tùy ý con". BoxConstraints.tight(size) nghĩa là "con phải đúng bằng size này". Hiểu và sử dụng đúng constraints là cực kỳ quan trọng.
  • Vẽ trước khi code: Với những bố cục phức tạp, hãy lấy giấy bút ra vẽ phác thảo. Xác định tâm, góc, bán kính, các điểm mốc. Nó sẽ giúp em chuyển đổi ý tưởng thành code dễ dàng hơn rất nhiều.
  • shouldRelayout quan trọng cho hiệu năng: Nếu delegate của em có các tham số thay đổi, hãy triển khai shouldRelayout một cách thông minh để chỉ re-layout khi thực sự cần. Tránh trả về true vô điều kiện nếu không cần thiết, vì nó sẽ gây lãng phí tài nguyên.

4. Ví dụ thực tế các ứng dụng/website đã ứng dụng

Thực ra, rất khó để chỉ ra một ứng dụng cụ thể nào đó công khai tuyên bố "chúng tôi dùng CustomMultiChildLayout ở đây!" vì nó thường là một chi tiết triển khai nội bộ. Tuy nhiên, em có thể hình dung nó được dùng trong các trường hợp sau:

  • Ứng dụng chỉnh sửa ảnh/video: Các lớp layer, sticker, text overlay mà em có thể kéo thả, xoay, thay đổi kích thước tự do trên canvas. Việc sắp xếp các layer này theo một trật tự z-index và vị trí chính xác thường cần đến một cơ chế layout tùy chỉnh.
  • Biểu đồ/Dashboard phức tạp: Khi các biểu đồ không chỉ là cột hay đường thẳng mà là những hình dạng phức tạp, có các nhãn, chú thích được đặt ở vị trí rất riêng biệt, thậm chí chồng lấn lên nhau theo một quy tắc nào đó.
  • Giao diện người dùng game: Trong các game di động, UI thường rất độc đáo và không theo các quy tắc layout chuẩn. Ví dụ, một vòng tròn các icon kỹ năng, các bảng thông báo pop-up xếp chồng lên nhau một cách nghệ thuật.
  • Ứng dụng vẽ/thiết kế: Các công cụ như Figma, Canva (phiên bản di động) có thể sử dụng các nguyên lý tương tự để quản lý vị trí và kích thước của các phần tử trên bảng vẽ.

Nhớ nhé, CustomMultiChildLayout không phải là "thuốc tiên" chữa bách bệnh, mà là "con dao phẫu thuật" tinh xảo dành cho những ca khó đỡ nhất. Hãy dùng nó một cách khôn ngoan và có trách nhiệm, em nhé!

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!