สร้าง 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 จะมีขั้นตอนหลักๆ ดังนี้:
- สร้าง Widget class: สร้าง Widget class ที่สืบทอดมาจาก
SingleChildRenderObjectWidgetหรือMultiChildRenderObjectWidgetขึ้นอยู่กับว่า Layout ของเรามี Widget ลูกกี่ตัว - สร้าง RenderObject class: สร้าง RenderObject class ที่สืบทอดมาจาก
RenderBoxและ override method ที่สำคัญ เช่นperformLayoutและgetSize - Override createRenderObject: ใน Widget class override method
createRenderObjectเพื่อสร้าง instance ของ RenderObject class ที่เราสร้างขึ้น - (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<Widget> children,
}) : super(key: key, children: children);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderEvenlySpacedRow();
}
}
class RenderEvenlySpacedRow extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
@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 เป็นชนิดMultiChildLayoutParentDatacomputeDryLayout: คำนวณขนาดที่ 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