Flutter堆叠布局插件stack_board的使用

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

Flutter堆叠布局插件stack_board的使用

stack_board 是一个用于Flutter的自定义堆叠布局插件,允许用户在屏幕上自由拖动、缩放和旋转各种类型的元素。本文将详细介绍如何使用 stack_board 插件,并提供一个完整的示例代码。

1. 使用 StackBoardController

StackBoardControllerstack_board 的核心控制器,用于管理堆叠板中的所有元素。你可以通过它添加、删除、更新元素,并控制它们的行为。

import 'package:stack_board/stack_board.dart';

// 创建 StackBoardController 实例
final _boardController = StackBoardController();

// 在 StackBoard 中使用控制器
StackBoard(
  controller: _boardController,
  // 添加背景
  background: const ColoredBox(color: Colors.grey),
),

2. 添加自适应文本

你可以通过 AdaptiveText 添加可编辑的文本元素。点击文本后,用户可以编辑其内容。

添加自适应文本

// 添加自适应文本
_boardController.add(
  const AdaptiveText(
    'Flutter Candies',
    tapToEdit: true,
    style: TextStyle(fontWeight: FontWeight.bold),
  ),
);

3. 添加自适应图片

你还可以通过 Image.network 添加网络图片,并将其作为堆叠板中的一个元素。

添加自适应图片

// 添加自适应图片
_boardController.add(
  StackBoardItem(
    child: Image.network('https://avatars.githubusercontent.com/u/47586449?s=200&v=4'),
  ),
);

4. 添加画板

StackDrawing 允许用户在屏幕上绘制图形。你可以设置画板的样式,例如边框颜色、图标颜色等。

添加画板

// 添加画板
_boardController.add(
  const StackDrawing(
    caseStyle: CaseStyle(
      borderColor: Colors.grey,
      iconColor: Colors.white,
      boxAspectRatio: 1,
    ),
  ),
);

5. 添加自定义 item

你可以通过继承 StackItemContentStackItem 来创建自定义的堆叠板元素。以下是一个简单的颜色块示例。

添加自定义 item

5.1 继承自 StackItemContentStackItem
class ColorContent extends StackItemContent {
  ColorContent({required this.color});

  final Color color;

  [@override](/user/override)
  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'color': color.value,
    };
  }
}

class ColorStackItem extends StackItem<ColorContent> {
  ColorStackItem({
    required Size size,
    String? id,
    Offset? offset,
    double? angle,
    StackItemStatus? status,
    ColorContent? content,
  }) : super(
          id: id,
          size: size,
          offset: offset,
          angle: angle,
          status: status,
          content: content,
        );

  [@override](/user/override)
  ColorStackItem copyWith({
    Size? size,
    Offset? offset,
    double? angle,
    StackItemStatus? status,
    ColorContent? content,
  }) {
    return ColorStackItem(
      id: id, // 必须!
      size: size ?? this.size,
      offset: offset ?? this.offset,
      angle: angle ?? this.angle,
      status: status ?? this.status,
      content: content ?? this.content,
    );
  }
}
5.2 使用控制器添加自定义 item
import 'dart:math' as math;

// 添加自定义 item
void _addCustomItem() {
  final Color color = Colors.primaries[math.Random().nextInt(Colors.primaries.length)];
  _boardController.addItem(
    ColorStackItem(
      size: const Size.square(100),
      content: ColorContent(color: color),
    ),
  );
}
5.3 使用 customBuilder 构建自定义 item

如果你使用了自定义的 StackItem,你需要通过 customBuilder 来构建这些自定义元素。

StackBoard(
  controller: _boardController,
  customBuilder: (StackItem<StackItemContent> item) {
    if (item is ColorStackItem) {
      return Container(
        width: item.size.width,
        height: item.size.height,
        color: item.content?.color,
      );
    }
    return const SizedBox.shink();
  },
)

6. 完整示例代码

以下是一个完整的示例代码,展示了如何使用 stack_board 插件来创建一个包含文本、图片、画板和自定义元素的堆叠板。

import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:stack_board/flutter_stack_board.dart';
import 'package:stack_board/stack_board_item.dart';
import 'package:stack_board/stack_case.dart';
import 'package:stack_board/stack_items.dart';

class ColorContent extends StackItemContent {
  ColorContent({required this.color});

  final Color color;

  [@override](/user/override)
  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'color': color.value,
    };
  }
}

class ColorStackItem extends StackItem<ColorContent> {
  ColorStackItem({
    required Size size,
    String? id,
    Offset? offset,
    double? angle,
    StackItemStatus? status,
    ColorContent? content,
  }) : super(
          id: id,
          size: size,
          offset: offset,
          angle: angle,
          status: status,
          content: content,
          lockZOrder: true,
        );

  [@override](/user/override)
  ColorStackItem copyWith({
    Size? size,
    Offset? offset,
    double? angle,
    StackItemStatus? status,
    bool? lockZOrder,
    ColorContent? content,
  }) {
    return ColorStackItem(
      id: id, // 必须!
      size: size ?? this.size,
      offset: offset ?? this.offset,
      angle: angle ?? this.angle,
      status: status ?? this.status,
      content: content ?? this.content,
    );
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomePage());
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late StackBoardController _boardController;

  [@override](/user/override)
  void initState() {
    super.initState();
    _boardController = StackBoardController();
  }

  [@override](/user/override)
  void dispose() {
    _boardController.dispose();
    super.dispose();
  }

  /// 删除拦截
  Future<void> _onDel(StackItem<StackItemContent> item) async {
    final bool? r = await showDialog<bool>(
      context: context,
      builder: (_) {
        return Center(
          child: SizedBox(
            width: 400,
            child: Material(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    const Padding(
                      padding: EdgeInsets.only(top: 10, bottom: 60),
                      child: Text('确认删除?'),
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        IconButton(
                            onPressed: () => Navigator.pop(context, true),
                            icon: const Icon(Icons.check)),
                        IconButton(
                            onPressed: () => Navigator.pop(context, false),
                            icon: const Icon(Icons.clear)),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );

    if (r == true) {
      _boardController.removeById(item.id);
    }
  }

  /// 添加文本元素
  void _addTextItem() {
    _boardController.addItem(
      StackTextItem(
        size: const Size(200, 100),
        content: TextItemContent(data: '哈哈哈哈哈'),
      ),
    );
  }

  /// 添加图片元素
  void _addImageItem() {
    _boardController.addItem(
      StackImageItem(
        size: const Size(300, 85),
        content: ImageItemContent(
          url:
              'https://files.flutter-io.cn/images/branding/flutterlogo/flutter-cn-logo.png',
        ),
      ),
    );
  }

  /// 添加画板元素
  void _addDrawItem() {
    _boardController.addItem(StackDrawItem(size: const Size.square(300)));
  }

  /// 添加自定义元素
  void _addCustomItem() {
    final Color color =
        Colors.primaries[Random().nextInt(Colors.primaries.length)];
    _boardController.addItem(
      ColorStackItem(
        size: const Size.square(100),
        content: ColorContent(color: color),
      ),
    );
  }

  /// 从 JSON 生成元素
  Future<void> _generateFromJson() async {
    final String jsonString =
        (await Clipboard.getData(Clipboard.kTextPlain))?.text ?? '';
    if (jsonString.isEmpty) {
      _showAlertDialog(
          title: '剪贴板为空',
          content: '请先将 JSON 字符串复制到剪贴板');
      return;
    }
    try {
      final List<dynamic> items = jsonDecode(jsonString) as List<dynamic>;

      for (final dynamic item in items) {
        if (item['type'] == 'StackTextItem') {
          _boardController.addItem(
            StackTextItem.fromJson(item),
          );
        } else if (item['type'] == 'StackImageItem') {
          _boardController.addItem(
            StackImageItem.fromJson(item),
          );
        } else if (item['type'] == 'StackDrawItem') {
          _boardController.addItem(
            StackDrawItem.fromJson(item),
          );
        }
      }
    } catch (e) {
      _showAlertDialog(title: '错误', content: e.toString());
    }
  }

  /// 获取 JSON
  Future<void> _getJson() async {
    final String json = jsonEncode(_boardController.getAllData());
    Clipboard.setData(ClipboardData(text: json));
    showDialog<void>(
      context: context,
      builder: (_) {
        return Center(
          child: SizedBox(
            width: 400,
            child: Material(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    const Padding(
                      padding: EdgeInsets.only(top: 10, bottom: 60),
                      child: Text('JSON'),
                    ),
                    Container(
                      constraints: const BoxConstraints(maxHeight: 500),
                      child: SingleChildScrollView(
                        child: Text(json),
                      ),
                    ),
                    const SizedBox(height: 20),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        IconButton(
                            onPressed: () => Navigator.pop(context),
                            icon: const Icon(Icons.check)),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: const Text('Stack Board Demo'),
        elevation: 0,
      ),
      body: StackBoard(
        onDel: _onDel,
        controller: _boardController,
        caseStyle: const CaseStyle(
          buttonBorderColor: Colors.grey,
          buttonIconColor: Colors.grey,
        ),
        // 背景
        background: ColoredBox(color: Colors.grey[100]!),
        customBuilder: (StackItem<StackItemContent> item) {
          if (item is StackTextItem) {
            return StackTextCase(item: item);
          } else if (item is StackDrawItem) {
            return StackDrawCase(item: item);
          } else if (item is StackImageItem) {
            return StackImageCase(item: item);
          } else if (item is ColorStackItem) {
            return Container(
              width: item.size.width,
              height: item.size.height,
              color: item.content?.color,
            );
          }

          return const SizedBox.shrink();
        },
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          Flexible(
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: <Widget>[
                  const SizedBox(width: 25),
                  FloatingActionButton(
                      onPressed: _addTextItem,
                      child: const Icon(Icons.border_color)),
                  const SizedBox(width: 5),
                  FloatingActionButton(
                      onPressed: _addImageItem, child: const Icon(Icons.image)),
                  const SizedBox(width: 5),
                  FloatingActionButton(
                      onPressed: _addDrawItem,
                      child: const Icon(Icons.color_lens)),
                  const SizedBox(width: 5),
                  FloatingActionButton(
                      onPressed: _addCustomItem,
                      child: const Icon(Icons.add_box)),
                ],
              ),
            ),
          ),
          Row(
            children: <Widget>[
              FloatingActionButton(
                onPressed: () => _boardController.clear(),
                child: const Icon(Icons.delete),
              ),
              const SizedBox(width: 5),
              FloatingActionButton(
                onPressed: _getJson,
                child: const Icon(Icons.file_download),
              ),
              const SizedBox(width: 5),
              FloatingActionButton(
                onPressed: _generateFromJson,
                child: const Icon(Icons.file_upload),
              ),
            ],
          ),
        ],
      ),
    );
  }

  void _showAlertDialog({required String title, required String content}) {
    showDialog<void>(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: Text(title),
          content: Text(content),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('确定'),
            ),
          ],
        );
      },
    );
  }
}

更多关于Flutter堆叠布局插件stack_board的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter堆叠布局插件stack_board的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter中使用stack_board(假设你指的是Stack布局,因为Flutter标准库中并没有直接名为stack_board的插件)的示例代码。Stack是一个强大的布局小部件,它允许你在Z轴上堆叠子小部件。

示例代码

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Stack Layout Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: StackExample(),
    );
  }
}

class StackExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stack Layout Example'),
      ),
      body: Center(
        child: Stack(
          alignment: Alignment.center, // 子部件将居中对齐
          children: <Widget>[
            // 底层:一个背景图片
            Image.network(
              'https://via.placeholder.com/300',
              fit: BoxFit.cover,
            ),
            
            // 中间层:一个半透明的覆盖层
            Container(
              color: Colors.black.withOpacity(0.5),
            ),
            
            // 上层:文本信息
            Text(
              'Hello, Flutter!',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            
            // 另一个上层:一个圆形的按钮
            Positioned(
              bottom: 20,
              right: 20,
              child: FloatingActionButton(
                onPressed: () {
                  // 按钮点击事件
                  print('Button pressed!');
                },
                tooltip: 'Press me',
                child: Icon(Icons.add),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

解释

  1. Stack 小部件Stack接受一个children列表,这些子小部件将按照列表中的顺序堆叠。

  2. alignment 属性:这个属性决定了如何对齐非定位(non-positioned)的子小部件。在这个例子中,所有子小部件都使用了默认的定位(即没有使用Positioned包装),但由于我们设置了alignment: Alignment.center,它们将居中对齐。然而,使用Positioned的小部件(如按钮)将不受此属性影响。

  3. Image.network:从网络加载并显示一个图片。

  4. Container:创建一个具有半透明黑色的覆盖层。

  5. Text:在覆盖层上显示白色文本。

  6. Positioned:用于在Stack内精确定位子小部件。在这个例子中,Positioned用于将一个浮动动作按钮放置在堆叠布局的右下角。

注意

  • Stack的子小部件默认会按照添加的顺序堆叠,即后添加的会覆盖先添加的。
  • 如果需要对某个子小部件进行精确定位,可以使用Positioned小部件包装它,并指定topbottomleftright属性。

这个示例展示了如何使用Stack布局来实现一个包含背景图片、覆盖层、文本和按钮的复杂布局。希望这对你有所帮助!

回到顶部