การสร้าง Custom Render Objects ใน Flutter
ใน Flutter, Widget เป็นเพียง "พิมพ์เขียว" ของสิ่งที่ต้องการแสดงผลบนหน้าจอ การแสดงผลจริงถูกจัดการโดย Render Objects ซึ่งเป็นส่วนสำคัญของ Render Tree ที่ Flutter ใช้เพื่อวาด UI Render Objects มีหน้าที่คำนวณขนาดและตำแหน่งของตัวเอง รวมถึงวาดตัวเองบนหน้าจอ บทความนี้จะพาคุณไปสำรวจการสร้าง Custom Render Objects เพื่อควบคุมการแสดงผล UI ได้อย่างละเอียด
บทความก่อนหน้า เราได้เรียนรู้การใช้งาน TensorFlow Lite (TFLite) ใน Flutter เพื่อเพิ่มความสามารถด้าน Machine Learning ให้กับแอปของเรา ในบทความนี้ เราจะเปลี่ยนมุมมองมาเน้นที่การปรับแต่งการแสดงผล UI โดยการสร้าง Custom Render Objects ซึ่งเป็นอีกหนึ่งวิธีที่ช่วยให้แอป Flutter ของคุณมีความยืดหยุ่นและตอบโจทย์ความต้องการเฉพาะเจาะจงได้มากขึ้น
ทำความเข้าใจ RenderObject
RenderObject คือคลาสพื้นฐานสำหรับทุกสิ่งที่ถูกวาดบนหน้าจอใน Flutter มันมีหน้าที่หลัก 3 อย่าง:
- PerformLayout: คำนวณขนาดและตำแหน่งของ Render Object และลูกๆ ของมัน
- Paint: วาด Render Object บน Canvas
- HitTest: ตรวจสอบว่าตำแหน่งที่กำหนดอยู่ภายในขอบเขตของ Render Object หรือไม่
เมื่อคุณสร้าง Custom Render Object คุณจะต้องสืบทอดจากคลาส RenderObject หรือคลาสย่อยของมัน (เช่น RenderBox) และ override method เหล่านี้เพื่อให้ได้พฤติกรรมที่คุณต้องการ
เหตุผลเชิงออกแบบ: ทำไมต้องมี RenderObject แยกจาก Widget? การแยกส่วนนี้ช่วยให้ Flutter ทำงานได้อย่างมีประสิทธิภาพ Widget เป็น immutable (เปลี่ยนแปลงไม่ได้) และถูกสร้างใหม่เมื่อมีการเปลี่ยนแปลงข้อมูล RenderObject เป็น mutable (เปลี่ยนแปลงได้) และถูกนำกลับมาใช้ใหม่เมื่อเป็นไปได้ การแยกส่วนนี้ช่วยลดภาระในการสร้างและทำลาย object จำนวนมาก ทำให้ UI ของ Flutter ทำงานได้อย่างราบรื่น
การสร้าง Custom RenderBox
RenderBox เป็นคลาสย่อยของ RenderObject ที่ใช้สำหรับ Render Objects ที่มีขอบเขตเป็นสี่เหลี่ยมผืนผ้า ซึ่งเป็นกรณีที่พบบ่อยที่สุดในการสร้าง Layout UI เราจะเริ่มต้นด้วยการสร้าง Custom RenderBox อย่างง่าย:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class CustomBox extends RenderBox {
Color color;
CustomBox({required this.color});
@override
void performLayout() {
size = constraints.biggest; // กำหนดขนาดเป็นขนาดที่ใหญ่ที่สุดที่ Constraints อนุญาต
}
@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
final rect = offset & size; // สร้าง Rect จาก Offset และ Size
final paint = Paint()..color = color;
canvas.drawRect(rect, paint);
}
}
class CustomWidget extends SingleChildRenderObjectWidget {
final Color color;
const CustomWidget({Key? key, required this.color, Widget? child}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return CustomBox(color: color);
}
@override
void updateRenderObject(BuildContext context, CustomBox renderObject) {
renderObject.color = color;
}
}
คำอธิบาย Code:
CustomBox: คลาสที่สืบทอดจากRenderBoxperformLayout(): method นี้ถูกเรียกเมื่อ Render Object ต้องการคำนวณขนาดของตัวเอง เรากำหนดขนาดเป็นขนาดที่ใหญ่ที่สุดที่ Constraints อนุญาต (constraints.biggest)paint(): method นี้ถูกเรียกเมื่อ Render Object ต้องการวาดตัวเองบน Canvas เราสร้างRectจากOffsetและSizeแล้ววาดสี่เหลี่ยมผืนผ้าด้วยสีที่กำหนดCustomWidget: เป็น Widget ที่สร้างและอัปเดตCustomBoxRender ObjectcreateRenderObject(): สร้าง instance ของCustomBoxupdateRenderObject(): อัปเดต properties ของCustomBoxเมื่อ Widget ถูก rebuild
การใช้งาน:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Render Object')),
body: Center(
child: CustomWidget(
color: Colors.blue,
child: const Text('Hello, Custom Render Object!'),
),
),
),
),
);
}
Code นี้จะแสดงสี่เหลี่ยมผืนผ้าสีฟ้าที่ครอบคลุมพื้นที่ทั้งหมดของ Center Widget และมีข้อความ "Hello, Custom Render Object!" อยู่ตรงกลาง
การจัดการ Constraints
Constraints เป็นข้อจำกัดที่กำหนดขนาดและตำแหน่งของ Render Object Constraints มีหลายประเภท เช่น:
BoxConstraints: ใช้สำหรับ RenderBox และกำหนดความกว้างและความสูงขั้นต่ำและสูงสุดSizeConstraints: ใช้สำหรับ RenderObject ทั่วไปและกำหนดขนาดที่แน่นอน
เมื่อคุณ implement PerformLayout คุณต้องพิจารณา Constraints ที่ได้รับและคำนวณขนาดที่เหมาะสม RenderBox มี method helper หลายอย่างที่ช่วยในการจัดการ Constraints เช่น computeDryLayout ซึ่งช่วยให้คุณคำนวณขนาดโดยไม่ส่งผลกระทบต่อ Layout จริง
@override
Size computeDryLayout(BoxConstraints constraints) {
// คำนวณขนาดโดยไม่ส่งผลกระทบต่อ Layout จริง
return constraints.biggest;
}
แนวปฏิบัติที่ดี: พยายามใช้ Constraints อย่างมีประสิทธิภาพ อย่าละเลย Constraints ที่ได้รับ เพราะอาจทำให้ Layout ผิดพลาดได้
การวาด (Painting) ขั้นสูง
Paint method ให้คุณเข้าถึง Canvas ซึ่งเป็นพื้นผิวที่คุณสามารถวาดรูปทรง, ข้อความ, และอื่นๆ ได้ คุณสามารถใช้ Paint class เพื่อกำหนดลักษณะการวาด เช่น สี, ความหนา, และรูปแบบ
@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
final rect = offset & size;
final paint = Paint()
..color = color
..strokeWidth = 5.0
..style = PaintingStyle.stroke; // วาดเฉพาะเส้นขอบ
canvas.drawRect(rect, paint);
}
Code นี้จะวาดสี่เหลี่ยมผืนผ้าสีฟ้าที่มีเส้นขอบหนา 5 pixels
เหตุผลเชิงออกแบบ: การใช้ PaintingContext ช่วยให้ Flutter จัดการ layering และ compositing ได้อย่างมีประสิทธิภาพ คุณไม่จำเป็นต้องจัดการ Canvas โดยตรง แต่ใช้ PaintingContext เพื่อวาดลงใน layer ที่เหมาะสม
การสร้าง Layout ที่ซับซ้อน
Custom Render Objects สามารถใช้สร้าง Layout ที่ซับซ้อนได้ โดยการจัดการ Child Render Objects RenderObject มี method หลายอย่างที่ช่วยในการจัดการ Child Render Objects เช่น:
add(RenderObject child): เพิ่ม Child Render Objectremove(RenderObject child): ลบ Child Render ObjectvisitChildren(RenderObjectVisitor visitor): เยี่ยมชม Child Render Objects ทั้งหมด
คุณต้อง override PerformLayout เพื่อกำหนดขนาดและตำแหน่งของ Child Render Objects
ตัวอย่าง: การสร้าง Custom Render Object ที่จัดเรียง Child Render Objects ในแนวนอน:
class CustomHorizontalLayout extends RenderBox with ContainerRenderObjectMixin<RenderBox, ParentData> {
@override
void performLayout() {
double x = 0.0;
RenderBox? child = firstChild;
while (child != null) {
child.layout(constraints, parentUsesSize: true);
final ParentData childParentData = child.parentData! as ParentData;
childParentData.offset = Offset(x, 0.0);
x += child.size.width;
child = childParentData.nextSibling;
}
size = Size(x, constraints.maxHeight);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
class CustomHorizontalLayoutWidget extends MultiChildRenderObjectWidget {
const CustomHorizontalLayoutWidget({Key? key, required List<Widget> children}) : super(key: key, children: children);
@override
RenderObject createRenderObject(BuildContext context) {
return CustomHorizontalLayout();
}
}
class ParentData extends ContainerBoxParentData<RenderBox> {}
คำอธิบาย Code:
CustomHorizontalLayout: คลาสที่สืบทอดจากRenderBoxและใช้ContainerRenderObjectMixinเพื่อจัดการ Child Render ObjectsperformLayout(): จัดเรียง Child Render Objects ในแนวนอน โดยกำหนดตำแหน่ง (offset) ให้กับ Child แต่ละตัวCustomHorizontalLayoutWidget: Widget ที่สร้างและจัดการCustomHorizontalLayoutParentData: คลาสที่ใช้เก็บข้อมูลของ Child Render Objects
ข้อควรระวัง: การสร้าง Layout ที่ซับซ้อนด้วย Custom Render Objects อาจทำให้ code ซับซ้อนและยากต่อการ maintain พิจารณาใช้ Widget ที่มีอยู่แล้ว (เช่น Row, Column, Stack) ก่อนที่จะสร้าง Custom Render Object
สรุป
การสร้าง Custom Render Objects เป็นเทคนิคขั้นสูงที่ช่วยให้คุณควบคุมการแสดงผล UI ใน Flutter ได้อย่างละเอียด คุณสามารถใช้ Custom Render Objects เพื่อสร้าง Layout ที่ซับซ้อน, ปรับแต่งการวาด, และเพิ่มประสิทธิภาพการทำงานของแอป
ในบทความถัดไป เราจะสำรวจการใช้ Metal หรือ Vulkan API ใน Flutter เพื่อเพิ่มประสิทธิภาพการแสดงผล graphics ในระดับต่ำ