Skip to main content

เนื้อหาภาษาไทย / Thai content

System guide Content reviewed

สร้าง Layout ที่ซับซ้อนด้วย Custom Layouts

บทความนี้แนะนำวิธีการสร้าง Custom Layouts ใน Flutter เพื่อให้สามารถสร้าง Layout ที่ซับซ้อนและยืดหยุ่นกว่าการใช้ `Row` และ `Column` โดยอธิบายถึงพื้นฐานสำคัญอย่าง...

สร้าง Layout ที่ซับซ้อนด้วย Custom Layouts ใน Flutter

ในบทความที่แล้ว เราได้เรียนรู้เกี่ยวกับ Widget Lifecycle ใน Flutter ซึ่งเป็นพื้นฐานสำคัญในการทำความเข้าใจการทำงานของ Widget ต่างๆ ใน Flutter ในบทความนี้ เราจะก้าวไปอีกขั้นและเรียนรู้วิธีสร้าง Layout ที่ซับซ้อนและยืดหยุ่นเกินกว่าการใช้เพียง Row และ Column ด้วย Custom Layouts

โดยปกติแล้ว เราจะใช้ Row, Column, Stack, Wrap และ Widget อื่นๆ ที่ Flutter เตรียมไว้ให้ในการจัดวาง Layout แต่บางครั้งความต้องการของเราอาจซับซ้อนกว่านั้น เช่น ต้องการให้ Widget จัดเรียงตัวตามรูปแบบวงกลม หรือต้องการให้ Widget มีขนาดเปลี่ยนแปลงตามสัดส่วนของหน้าจอ Custom Layouts ช่วยให้เราสามารถสร้าง Layout ที่ตอบโจทย์ความต้องการเหล่านี้ได้

ทำไมต้อง Custom Layouts?

  • ความยืดหยุ่น: สามารถสร้าง Layout ที่ซับซ้อนและไม่สามารถทำได้ด้วย Widget พื้นฐาน
  • การควบคุม: ควบคุมตำแหน่งและขนาดของ Widget ได้อย่างละเอียด
  • ประสิทธิภาพ: ปรับแต่ง Layout ให้เหมาะสมกับความต้องการเพื่อประสิทธิภาพที่ดีที่สุด (ในบางกรณี)

พื้นฐานสำคัญ: RenderObject และ RenderBox

ก่อนที่จะเริ่มสร้าง Custom Layouts เราต้องทำความเข้าใจกับ RenderObject และ RenderBox ซึ่งเป็นหัวใจสำคัญของการจัดการ Layout ใน Flutter

RenderObject: คือ Object ที่รับผิดชอบในการวาด (paint) และจัดวาง (layout) Widget บนหน้าจอ โดย RenderObject จะถูกสร้างขึ้นโดย Widget ที่มี RenderObjectWidget เป็น parent class

RenderBox: คือ subclass ของ RenderObject ที่ใช้สำหรับ Widget ที่มีขนาดและตำแหน่งที่แน่นอน (ในเชิงเรขาคณิต) Widget ส่วนใหญ่ที่เราใช้งานกันทั่วไป เช่น Container, Text, Image จะใช้ RenderBox

พูดง่ายๆ คือ RenderObject เป็นเหมือนพิมพ์เขียว ส่วน RenderBox เป็นเหมือนบ้านที่สร้างตามพิมพ์เขียว โดย RenderBox จะมีข้อมูลเพิ่มเติมเกี่ยวกับขนาดและตำแหน่ง

การสร้าง Custom Layouts ส่วนใหญ่จะเกี่ยวข้องกับการสร้าง RenderObject หรือ RenderBox ที่กำหนดวิธีการจัดวาง Widget ลูก (children) ของตัวเอง

ขั้นตอนการสร้าง Custom Layout

โดยทั่วไป การสร้าง Custom Layout จะมีขั้นตอนหลักๆ ดังนี้:

  1. สร้าง Widget class: สร้าง Widget class ที่สืบทอดมาจาก SingleChildRenderObjectWidget หรือ MultiChildRenderObjectWidget ขึ้นอยู่กับว่า Layout ของเรามี Widget ลูกกี่ตัว
  2. สร้าง RenderObject class: สร้าง RenderObject class ที่สืบทอดมาจาก RenderBox และ override method ที่สำคัญ เช่น performLayout และ getSize
  3. Override createRenderObject: ใน Widget class override method createRenderObject เพื่อสร้าง instance ของ RenderObject class ที่เราสร้างขึ้น
  4. (Optional) Override updateRenderObject: ถ้าต้องการให้ Layout เปลี่ยนแปลงเมื่อ Widget ถูก update ให้ override method updateRenderObject

ตัวอย่าง: Custom Layout ที่จัดเรียง Widget ในแนวนอนโดยเว้นระยะห่างเท่าๆ กัน

ลองมาดูตัวอย่างการสร้าง Custom Layout ที่จัดเรียง Widget ลูกในแนวนอนโดยเว้นระยะห่างเท่าๆ กัน

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class EvenlySpacedRow extends MultiChildRenderObjectWidget {
  EvenlySpacedRow({
    Key? key,
    required List&ltWidget&gt children,
  }) : super(key: key, children: children);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderEvenlySpacedRow();
  }
}

class RenderEvenlySpacedRow extends RenderBox
    with
        ContainerRenderObjectMixin&ltRenderBox, MultiChildLayoutParentData&gt,
        RenderBoxContainerDefaultsMixin&ltRenderBox, MultiChildLayoutParentData&gt {

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! MultiChildLayoutParentData) {
      child.parentData = MultiChildLayoutParentData();
    }
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    // Simulate the layout process without actually laying out the children.
    // This is used by the parent to determine the size of this RenderBox
    double totalWidth = 0;
    double maxHeight = 0;

    RenderBox? child = firstChild;
    while (child != null) {
      final Size childSize = child.getDryLayout(constraints);
      totalWidth += childSize.width;
      maxHeight = Math.max(maxHeight, childSize.height);
      final MultiChildLayoutParentData childParentData =
          child.parentData as MultiChildLayoutParentData;
      child = childParentData.nextSibling;
    }

    return constraints.constrain(Size(totalWidth, maxHeight));
  }


  @override
  void performLayout() {
    if (children.isEmpty) {
      size = constraints.smallest; // Use smallest instead of zero
      return;
    }

    double totalWidth = 0;
    double maxHeight = 0;

    RenderBox? child = firstChild;
    while (child != null) {
      child.layout(constraints, parentUsesSize: true);
      totalWidth += child.size.width;
      maxHeight = Math.max(maxHeight, child.size.height);
      final MultiChildLayoutParentData childParentData =
          child.parentData as MultiChildLayoutParentData;
      child = childParentData.nextSibling;
    }

    final double availableWidth = constraints.maxWidth;
    final double spacing = (availableWidth - totalWidth) / (children.length + 1);

    double currentX = spacing;
    child = firstChild;
    while (child != null) {
      final MultiChildLayoutParentData childParentData =
          child.parentData as MultiChildLayoutParentData;
      childParentData.offset = Offset(currentX, 0);
      currentX += child.size.width + spacing;
      child = childParentData.nextSibling;
    }

    size = constraints.constrain(Size(availableWidth, maxHeight));

    // Call markNeedsPaint to ensure the children are repainted.
    markNeedsPaint();
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }
}

คำอธิบาย Code:

  • EvenlySpacedRow: เป็น Widget ที่ใช้ RenderObject ของเรา
  • RenderEvenlySpacedRow: เป็น RenderObject ที่ทำการจัด Layout จริง
  • setupParentData: กำหนดว่า ParentData ของ child เป็นชนิด MultiChildLayoutParentData
  • computeDryLayout: คำนวณขนาดที่ RenderBox ต้องการ โดยไม่ทำการ Layout จริง (ใช้สำหรับ query ขนาด)
  • performLayout: ทำการ Layout จริง โดยคำนวณระยะห่างระหว่าง Widget และกำหนดตำแหน่งของ Widget แต่ละตัว
  • paint: ทำการวาด Widget

การใช้งาน:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Custom Layout Example')),
        body: Container(
          width: double.infinity,
          padding: const EdgeInsets.all(20),
          child: EvenlySpacedRow(
            children: [
              ElevatedButton(onPressed: () {}, child: const Text('Button 1')),
              ElevatedButton(onPressed: () {}, child: const Text('Button 2')),
              ElevatedButton(onPressed: () {}, child: const Text('Button 3')),
            ],
          ),
        ),
      ),
    ),
  );
}

Code นี้จะสร้าง EvenlySpacedRow ที่มีปุ่ม 3 ปุ่ม โดยแต่ละปุ่มจะเว้นระยะห่างเท่าๆ กัน

LayoutBuilder: ตัวช่วยในการสร้าง Layout ที่ปรับเปลี่ยนตามขนาดหน้าจอ

LayoutBuilder เป็น Widget ที่ช่วยให้เราสามารถสร้าง Layout ที่ปรับเปลี่ยนตามขนาดของหน้าจอได้ โดย LayoutBuilder จะให้ข้อมูลเกี่ยวกับขนาดของพื้นที่ที่ Widget ครอบครอง ทำให้เราสามารถปรับแต่ง Layout ให้เหมาะสมกับขนาดนั้นๆ ได้

LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    if (constraints.maxWidth > 600) {
      // Layout สำหรับหน้าจอขนาดใหญ่
      return Row(
        children: [
          Expanded(child: Text('Left')),
          Expanded(child: Text('Right')),
        ],
      );
    } else {
      // Layout สำหรับหน้าจอขนาดเล็ก
      return Column(
        children: [
          Text('Top'),
          Text('Bottom'),
        ],
      );
    }
  },
)

ในตัวอย่างนี้ เราใช้ LayoutBuilder เพื่อตรวจสอบความกว้างของหน้าจอ ถ้าความกว้างมากกว่า 600 pixels จะใช้ Row ในการจัด Layout ถ้าไม่ จะใช้ Column

สรุป

Custom Layouts เป็นเครื่องมือที่มีประสิทธิภาพในการสร้าง Layout ที่ซับซ้อนและยืดหยุ่นใน Flutter การทำความเข้าใจ RenderObject, RenderBox และ LayoutBuilder จะช่วยให้คุณสามารถสร้าง Layout ที่ตอบโจทย์ความต้องการได้อย่างเต็มที่ ถึงแม้ว่าการสร้าง Custom Layouts จะต้องใช้ความเข้าใจในระดับหนึ่ง แต่ผลลัพธ์ที่ได้ก็คุ้มค่ากับความพยายาม

ในบทความถัดไป เราจะเรียนรู้เกี่ยวกับการจัดการ State อย่างมีประสิทธิภาพด้วย Provider ซึ่งเป็นอีกหนึ่งแนวคิดที่สำคัญในการพัฒนาแอป Flutter

Office.in.th Custom Service

Want to build or adapt a system from this structure?

Office.in.th designs and develops enterprise web applications, API integrations, and public- and private-sector management systems, with long-term support.

See our software development service