อันนี้มาจดบันทึกไว้ ตอนเขียนแอปที่ต้องการจะเอา Widget ที่แสดงบนจอมาพิมพ์ ซึ่งการ RenderRepaintBoundary สามารถเอามา render ตัว Widget ที่ต้องการ ออกมาในความละเอียดที่ระบุ จากนั้นนำมาเป็นภาพ PNG ได้ ซึ่งสามารถนำเอามาใช้งานต่อได้เช่น เอามาบันทึกเป็นไฟล์ PNG ส่วนงานลูกค้าที่เอามาใช้คือต้องการพิมพ์ออกกระดาษ หรือสร้างเป็นไฟล์ PDF ซึ่ืงมันมี Package ที่ช่วยเรื่องนี้อยู่แล้ว
การใช้งาน RenderRepaintBoundary ไปดูตัวอย่างวิธีใช้ของคนอื่นที่เขาแชร์เอาไว้ ไม่ได้คิดเอง 😅 ต้องขอบคุณสังคมแห่งการแบ่งปัน
การสร้าง RenderRepaintBoundary
มีวิธีการดังนี้
RenderRepaintBoundary({
RenderBox? child,
})
ตัวอย่างสร้าง MyWidget โดยเอาไว้ใน RenderRepaintBoundary เพื่อลองเอามาขยายขนาดเป็น 2 เท่าและแปลงเป็นข้อมูลแบบไฟล์ PNG ดู
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Text('This is sample widget'),
OutlinedButton(onPressed: () {}, child: const Text('sample button')),
Image.asset('asset/Icon-192.png'),
const Divider(color: Colors.green)
]),
);
}
}
เพื่อให้เข้าถึงตัว RenderRepaintBoundary ที่สร้างขึ้นได้ในฟังก์ชั่นที่ใช้สร้าง PNG โดยตรง เลยกำหนด GlobalKey()
เข้าไปด้วยดังนี้
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
final GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
Widget sampleWidget = RepaintBoundary(key: _globalKey, child: const MyWidget());
}
}
ในฟังก์ชั่นที่ใช้สำหรับสร้าง PNG ชื่อ _createPng()
จะมีการอ้างถึง _globalKey
ที่เอาไปผูกไว้กับ RepaintBoundary โดยคำสั่ง .currentContext
จะคืนค่า Context ของตัว Widget ที่ render อยู่บนจอ ถ้าเป็น null
แสดงว่าไม่สามารถเข้าถึงได้แล้ว ในการเขียนใช้งานจริง ควรใส่ try-catch เพื่อดักจับ Exception ด้วย
class _MainAppState extends State<MainApp> {
final GlobalKey _globalKey = GlobalKey();
Future<Uint8List?>? _output; // for store result from _createPng()
Future<Uint8List?> _createPng() async {
if (_globalKey.currentContext == null) {
return null;
}
RenderRepaintBoundary boundary = _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
double ratio = 2; // render in 2x size
ui.Image image = await boundary.toImage(pixelRatio: ratio);
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData != null) {
return byteData.buffer.asUint8List();
} else {
return null;
}
}
}
คำสั่ง .toImage()
จะทำงานแบบ asynchronous หากใช้คำสั่ง .toImageSync()
จะเป็นแบบ synchronous
ตัวอย่าง จะเป็นการแสดง Widget ที่จะใช้ทดลอง ในที่นี้คือ MyWidget และมีปุ่มกด create PNG in x2 size
เมื่อกดแล้ว จะเรียก setState()
ทำการกำหนดค่า _output = _createPng()
และวาดหน้าจอใหม่
@override
Widget build(BuildContext context) {
Widget sampleWidget = RepaintBoundary(key: _globalKey, child: const MyWidget());
if (_output == null) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
sampleWidget,
OutlinedButton(
onPressed: () => setState(() => _output = _createPng()),
child: const Text('create PNG in x2 size'))
],
)),
),
);
} else {
// after user press the button → _output = _createPng()
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
sampleWidget,
const Text('output PNG'),
Container(
decoration: BoxDecoration(border: Border.all()),
child: FutureBuilder(
future: _output,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.memory(snapshot.data!); // snapshot.data → data from _output
} else {
return const Text('error: invalid _output');
}
},
),
)
],
)),
),
);
}
}
ผลการทดสอบกดปุ่มเพื่อสร้างภาพ PNG ขนาด 2 เท่า
จากผลการทำงานจะเห็นภาพผลที่ได้ ตัวหนังสือและขอบปุ่มที่แปลงมามีความคมชัด ส่วนภาพ icon ดูเบลอเนื่องจากเป็น bitmap
ไฟล์ main.dart อย่าลืมเตรียมไฟล์ asset/Icon-192.png
สำหรับตัวอย่างด้วย หากใช้ภาพอื่นให้เปลี่ยนใน class MyWidget
หรือลบทิ้งหากไม่ต้องการ
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
final GlobalKey _globalKey = GlobalKey();
Future<Uint8List?>? _output;
Future<Uint8List?> _createPng() async {
if (_globalKey.currentContext == null) {
return null;
}
RenderRepaintBoundary boundary = _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
double ratio = 2;
ui.Image image = await boundary.toImage(pixelRatio: ratio);
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData != null) {
return byteData.buffer.asUint8List();
} else {
return null;
}
}
@override
Widget build(BuildContext context) {
Widget sampleWidget = RepaintBoundary(key: _globalKey, child: const MyWidget());
if (_output == null) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
sampleWidget,
OutlinedButton(
onPressed: () => setState(() => _output = _createPng()),
child: const Text('create PNG in x2 size'))
],
)),
),
);
} else {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
sampleWidget,
const Text('output PNG'),
Container(
decoration: BoxDecoration(border: Border.all(color: const ui.Color.fromARGB(255, 255, 230, 0))),
child: FutureBuilder(
future: _output,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.memory(snapshot.data!);
} else {
return const Text('error: invalid _output');
}
},
),
)
],
)),
),
);
}
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Text('This is sample widget'),
OutlinedButton(onPressed: () {}, child: const Text('sample button')),
Image.asset('asset/Icon-192.png'),
const Divider(color: Colors.green)
]),
);
}
}