Flutter无界面渲染插件headless_widgets的使用

发布于 1周前 作者 songsunli 来自 Flutter

Flutter无界面渲染插件headless_widgets的使用

简介

headless_widgets 是一个Flutter插件,提供了一组不依赖于具体UI呈现的控件逻辑实现。这些控件专注于功能的正确性和灵活性,适用于自定义设计系统和自定义控件集。目前,该插件实现了以下控件:

  • Button:按钮控件
  • Popover:弹出菜单控件
  • HoverRegion:替代 MouseRegion 的控件,支持鼠标捕获行为,并在滚动时延迟悬停事件

完整示例Demo

下面是一个完整的示例代码,展示了如何使用 headless_widgets 插件中的 ButtonPopover 控件。

// 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

1 回复

更多关于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;
}

注意

  1. 上面的代码示例使用了 HeadlessWidgetsBindingHeadlessBuildOwner,这些类在 headless_widgets 插件中提供,用于在无界面环境下初始化 Flutter 引擎和构建 Widget 树。
  2. 由于 headless_widgets 插件可能在不同版本中有更新或变化,请参考插件的官方文档和示例代码以确保兼容性。
  3. 在实际项目中,你可能需要将生成的图片保存到文件系统或通过网络发送,这里仅展示了生成图片的基本流程。

请确保在实际使用中替换 headless_widgets 的版本号为最新版本,并根据需要进行错误处理和资源释放。

回到顶部