Flutter无界面渲染插件headless_widgets的使用
Flutter无界面渲染插件headless_widgets的使用
简介
headless_widgets
是一个Flutter插件,提供了一组不依赖于具体UI呈现的控件逻辑实现。这些控件专注于功能的正确性和灵活性,适用于自定义设计系统和自定义控件集。目前,该插件实现了以下控件:
Button
:按钮控件Popover
:弹出菜单控件HoverRegion
:替代MouseRegion
的控件,支持鼠标捕获行为,并在滚动时延迟悬停事件
完整示例Demo
下面是一个完整的示例代码,展示了如何使用 headless_widgets
插件中的 Button
和 Popover
控件。
// ignore_for_file: avoid_print
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Colors, Typography;
import 'package:headless_widgets/headless_widgets.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Headless Widgets Example')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Regular Button Section
_Section(
title: '普通按钮',
description: '需要通过Tab键聚焦,默认行为适用于大多数平台。',
child: _ButtonRow(
children: [
SampleButton(
onPressed: () {
print('按下按钮 1');
},
child: const Text('按钮 1'),
),
SampleButton(
onPressed: () {
print('按下按钮 2');
},
child: const Text('按钮 2'),
),
const SampleButton(
child: Text('禁用'),
),
],
),
),
// Tap to Focus Section
_Section(
title: '点击聚焦',
description: '按钮在指针交互时自动聚焦。',
child: _ButtonRow(
children: [
SampleButton(
tapToFocus: true,
onPressed: () {
print('按下按钮 1');
},
child: const Text('按钮 1'),
),
SampleButton(
tapToFocus: true,
onPressed: () {
print('按下按钮 2');
},
child: const Text('按钮 2'),
),
const SampleButton(
tapToFocus: true,
child: Text('禁用'),
),
],
),
),
// Popover Section
_Section(
title: '弹出菜单',
description: '按钮按下时显示弹出菜单。',
child: _ButtonRow(
children: [
PopoverButton(
child: const Text('显示弹出菜单 1\n多行文本'),
),
const Spacer(),
PopoverButton(
child: const Text('显示弹出菜单 2'),
),
const Spacer(),
],
),
),
// KeyUp Timeout Section
_Section(
title: '按键抬起超时',
description: '如果按键按住超过 `keyUpTimeout` 时间,则在按键抬起后调用 `onPressed` 回调。默认行为适用于 macOS 和 Linux。',
child: _ButtonRow(
children: [
SampleButton(
keyUpTimeout: const Duration(milliseconds: 250),
onPressed: () {
print('按下按钮 1');
},
child: const Text('按钮 1'),
),
SampleButton(
keyUpTimeout: const Duration(milliseconds: 250),
onPressed: () {
print('按下按钮 2');
},
child: const Text('按钮 2'),
),
const SampleButton(
keyUpTimeout: const Duration(milliseconds: 250),
child: const Text('禁用'),
),
],
),
),
// Slider Section
_Section(
title: '滑块',
child: Row(children: [
_SliderExample(),
]),
),
],
),
),
),
);
}
}
// Helper Widgets
class _Section extends StatelessWidget {
final String title;
final String? description;
final Widget child;
const _Section({
required this.title,
this.description,
required this.child,
});
[@override](/user/override)
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: Colors.grey.shade300,
),
color: Colors.grey.shade200,
),
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.black,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
if (description != null)
Text(
description!,
style: TextStyle(color: Colors.grey.shade800),
),
const SizedBox(height: 12),
child,
],
),
);
}
}
class _ButtonRow extends StatelessWidget {
final List<Widget> children;
const _ButtonRow({
super.key,
required this.children,
});
[@override](/user/override)
Widget build(BuildContext context) {
return Row(
children: children
.intersperse(
const SizedBox(width: 10),
)
.toList(growable: false),
);
}
}
class _Popover extends StatefulWidget {
final PopoverController controller;
const _Popover({
super.key,
required this.controller,
});
[@override](/user/override)
State<_Popover> createState() => _PopoverState();
}
class _PopoverState extends State<_Popover> {
bool expanded = false;
[@override](/user/override)
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.4),
),
child: SafeArea(
child: AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: expanded ? const EdgeInsets.all(50) : const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'弹出菜单内容',
style: TextStyle(color: Colors.black),
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SampleButton(
onPressed: () {
widget.controller.hidePopover();
},
child: const Text('关闭'),
),
const SizedBox(width: 10),
SampleButton(
onPressed: () {
setState(() {
expanded = !expanded;
});
},
child: expanded
? const Text('折叠')
: const Text('展开'),
),
],
)
],
),
),
),
);
}
}
class PopoverButton extends StatefulWidget {
final Widget child;
const PopoverButton({
super.key,
required this.child,
});
[@override](/user/override)
State<PopoverButton> createState() => _PopoverButtonState();
}
class _PopoverButtonState extends State<PopoverButton> {
final _controller = PopoverController();
[@override](/user/override)
Widget build(BuildContext context) {
return PopoverAnchor(
controller: _controller,
delegate: () => SamplePopoverDelegate(attachments: [
const PopoverAttachment(
anchor: Alignment.centerLeft, popover: Alignment.centerRight),
const PopoverAttachment(
anchor: Alignment.bottomCenter, popover: Alignment.topCenter),
const PopoverAttachment(
anchor: Alignment.topCenter, popover: Alignment.bottomCenter),
]),
animationDuration: const Duration(milliseconds: 200),
animationReverseDuration: const Duration(milliseconds: 150),
child: SampleButton(
onPressed: () async {
await _controller.showPopover(_Popover(
controller: _controller,
));
},
child: widget.child,
),
);
}
}
class _SliderExample extends StatefulWidget {
const _SliderExample();
[@override](/user/override)
State<_SliderExample> createState() => _SliderExampleState();
}
class _SliderExampleState extends State<_SliderExample> {
double value = 5;
[@override](/user/override)
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 200,
child: SampleSlider(
min: 0,
max: 10,
value: value,
onKeyboardAction: (action) {
switch (action) {
case SliderKeyboardAction.increase:
setState(() {
value = (value + 1).clamp(0, 10).toDouble();
});
case SliderKeyboardAction.decrease:
setState(() {
value = (value - 1).clamp(0, 10).toDouble();
});
}
},
onChanged: (value) {
final newValue = value;
if (newValue != this.value) {
setState(() {
this.value = newValue;
});
}
},
),
),
const SizedBox(width: 10),
Text(
'值: ${value.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.black),
),
],
);
}
}
extension IntersperseExtensions<T> on Iterable<T> {
Iterable<T> intersperse(T element) sync* {
final iterator = this.iterator;
if (iterator.moveNext()) {
yield iterator.current;
while (iterator.moveNext()) {
yield element;
yield iterator.current;
}
}
}
}
更多关于Flutter无界面渲染插件headless_widgets的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter无界面渲染插件headless_widgets的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在Flutter中,headless_widgets
是一个用于无界面渲染的插件,特别适用于需要在后台执行渲染任务(例如生成图片、PDF等)而不需要实际显示UI的场景。以下是一个简单的代码示例,展示了如何使用 headless_widgets
进行无界面渲染。
首先,确保你已经在 pubspec.yaml
文件中添加了 headless_widgets
依赖:
dependencies:
flutter:
sdk: flutter
headless_widgets: ^0.x.x # 请替换为最新版本号
然后,运行 flutter pub get
来获取依赖。
接下来,你可以创建一个 Flutter 应用,并在其中使用 headless_widgets
进行无界面渲染。以下是一个简单的示例,演示如何生成一个包含文本的 PNG 图片:
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:headless_widgets/headless_widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 创建一个离屏画布
final ByteData? imageBytes = await renderImageToByteData(
builder: (BuildContext context) => MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Hello, Headless Widgets!',
style: TextStyle(fontSize: 24),
),
),
),
),
pixelRatio: ui.window.devicePixelRatio,
);
if (imageBytes != null) {
// 将生成的图片保存到文件(这里仅演示,实际使用时可能需要保存到指定路径)
final Uint8List pngBytes = imageBytes.buffer.asUint8List();
// 注意:这里为了示例简单,没有实际写入文件,你可以使用 File 类来保存 pngBytes 到本地文件
print('Image generated successfully, PNG data length: ${pngBytes.length}');
} else {
print('Failed to generate image.');
}
}
Future<ByteData?> renderImageToByteData({
required WidgetBuilder builder,
required double pixelRatio,
}) async {
final RenderRepaintBoundary boundary = RenderRepaintBoundary();
await HeadlessWidgetsBinding.ensureInitialized();
final LayeredCanvas layeredCanvas = LayeredCanvas(boundary);
// 使用自定义的 Canvas 层来渲染 Widget
final ui.Canvas canvas = ui.Canvas(layeredCanvas);
final Size size = Size(800, 600); // 设置渲染目标大小
// 开始一个帧的绘制
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
canvas.save();
canvas.translate(-size.width / 2.0, -size.height / 2.0);
LayeredCanvas.layer(canvas, () {
final BuildContext context = HeadlessBuildOwner().buildScope(builder);
boundary.paint(canvas, size.toOffset());
});
canvas.restore();
final ui.Scene scene = sceneBuilder.build();
// 将场景渲染为图片
final ui.Image image = await scene.toImage(pixelRatio: pixelRatio);
final ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData;
}
注意:
- 上面的代码示例使用了
HeadlessWidgetsBinding
和HeadlessBuildOwner
,这些类在headless_widgets
插件中提供,用于在无界面环境下初始化 Flutter 引擎和构建 Widget 树。 - 由于
headless_widgets
插件可能在不同版本中有更新或变化,请参考插件的官方文档和示例代码以确保兼容性。 - 在实际项目中,你可能需要将生成的图片保存到文件系统或通过网络发送,这里仅展示了生成图片的基本流程。
请确保在实际使用中替换 headless_widgets
的版本号为最新版本,并根据需要进行错误处理和资源释放。