theme: cyanosis
本系列,将通过 Flutter 实现一个全平台的像素编辑器应用。源码见开源项目 【pix_editor】
上一篇完成了 Flutter 像素编辑器的点击交互,绘制像素。本篇继续完善像素编辑器,划分布局区域,并运行修改项目和画笔的配置。如下所示,是 Flutter 像素编辑器的第二版:
1. Flutter 像素编辑器布局结构
在桌面端中,第二版将应用划分为五个区域:
- 顶部菜单栏 MenuToolBar :放置菜单以及操作按钮。
- 左侧编辑工具 ToolBar : 放置编辑按钮。
- 右侧操作面板 OperationArea : 放置配置操作的内容。
- 底部信息栏 BottomBar : 展示信息。
- 中间绘制区 EditorArea :交互绘制像素的区域。
通过行列布局,结合 Expanded
组件,可以是中间的 EditorArea 绘制区大小自适应窗口。
```dart class PixEditorPage extends StatelessWidget { const PixEditorPage({super.key});
@override Widget build(BuildContext context) { return const Scaffold( backgroundColor: Colors.white, body: Column( children: [ MenuToolBar(), Expanded( child: Row( children: [ ToolBar(), Expanded(child: Column( children: [Expanded(child: EditorArea()), BottomBar()] )), OperationArea() ], )), ], ), ); } } ```
目前 BottomBar、MenuToolBar、ToolBar 三个区域只是简单展示一下,具体功能后续完善。本篇主要着重介绍 OperationArea 操作区,如何影响 EditorArea 绘图区。
2、数据变化的业务逻辑
OperationArea 操作区在编辑时,绘图区的内容需要实时变化。比如下面修改网格的数量,输入过程中绘图区的个数会相对改变:
所以需要数据的变化可以通知画板进行更新。这里定义 ProjectConfigLogic
类维护配置状态数据,混入 ChangeNotifier
拥有通知监听的能力。其中主要维护 PixEditorConfig
配置数据,并提供一些方法,来修改某个维度的数据。数据变化后通过 notifyListeners
通知更新:
```dart class ProjectConfigLogic with ChangeNotifier {
late TextEditingController rowCtrl = TextEditingController(text: config.row.toString()); late TextEditingController columnCtrl = TextEditingController(text: config.column.toString());
@override void dispose() { rowCtrl.dispose(); columnCtrl.dispose(); super.dispose(); }
PixEditorConfig _config = PixEditorConfig( column: 5, row: 5, name: "新建文件", backgroundColor: const Color(0xfff0f0f0), gridColor: Colors.white, showGrid: true, );
PixEditorConfig get config => _config;
set config(PixEditorConfig config){ _config = config; notifyListeners(); }
void updateRow(int row){ rowCtrl.text = row.toString(); config = _config.copyWith(row: row); } void updateBackgroundColor(Color color){ config = _config.copyWith(backgroundColor: color); } void updateGridColor(Color color) { config = _config.copyWith(gridColor: color); }
void updateColumn(int column){ columnCtrl.text = column.toString(); config = _config.copyWith(column: column); }
void toggleShowGrid(){ config = config.copyWith(showGrid: !config.showGrid); } } ```
3、项目配置的状态数据管理
接下来就需要访问 ProjectConfigLogic 的数据进行界面构建,并触发其方法,修改数据触发更新。这里拿是否展示网格的这条功能需求,介绍一下如何处理:
目前功能并不是很复杂,使用 Flutter 内置的 InheritedNotifier 来共享 ProjectConfigLogic 即可。后面功能复杂之后,再考虑使用状态管理的库,来维护 ProjectConfigLogic 的功能。如下所示,定义 ProjectConfigScope
向子树提供状态数据:
```dart class ProjectConfigScope extends InheritedNotifier { const ProjectConfigScope({ required super.notifier, required super.child, super.key, });
static ProjectConfigLogic of(BuildContext context) => context.dependOnInheritedWidgetOfExactType ()!.notifier!;
static ProjectConfigLogic read(BuildContext context) => context.getInheritedWidgetOfExactType ()!.notifier!; } ```
然后再需要共享数据组件们的上层嵌套 ProjectConfigScope
,来达到向子树共享数据的目的:
ps:之前在 《 Flutter 组件集录 | InheritedNotifier 内置状态管理组件》 一文中介绍过 InheritedNotifier 的使用。
这样,下层的组件可以通过 ProjectConfigScope.of(context)
根据上下文得到 ProjectConfigLogic
业务逻辑对象。对于是否显示网格来说 Checkbox
的 value 可以访问 configLogic 中的数据;点击事件 onChanged 中,通过 configLogic
对象触发 toggleShowGrid 方法,修改是否展示网格的配置数据,并触发更新:
```dart ProjectConfigLogic configLogic = ProjectConfigScope.of(context);
Row( children: [ Text("显示网格", style: TextStyle(fontSize: 12)), const SizedBox(width: 8), Container( width: 20, height: 20, child: Checkbox( value: configLogic.config.showGrid, onChanged: (bool? value) { configLogic.toggleShowGrid(); }, ), ) ], ), ```
其他的配置项数据的修改同理。
4、绘制信息的状态数据管理
绘制信息中目前增加了画笔的颜色,我们也可以通过业务逻辑层,来封装绘制方面的状态数据。如下定义 PixPaintLogic
来维护像素点列表 _pixCells
,以及画笔颜色 _paintColor
。这样命中像素点数据变化逻辑,就可以写在 PixPaintLogic
中。
```dart class PixPaintLogic with ChangeNotifier { final List _pixCells = [];
List get pixCells => _pixCells;
Color get paintColor => _paintColor;
set paintColor(Color value){ _paintColor = value; notifyListeners(); }
Color _paintColor = const Color(0xff5ec8f8);
// 命中像素点 void hitPix(int x,int y) { bool hasPix = _pixCells.where((e) => e.position == (x, y)).isNotEmpty; if (hasPix) { _pixCells.removeWhere((e) => e.position == (x, y)); } else { _pixCells.add(PixCell(color: _paintColor, position: (x,y))); } notifyListeners(); } } ```
同理,提供 PixPaintScope
向子树共享 PixPaintLogic
业务逻辑对象:
```dart class PixPaintScope extends InheritedNotifier { const PixPaintScope({ required super.notifier, required super.child, super.key, });
static PixPaintLogic of(BuildContext context) => context.dependOnInheritedWidgetOfExactType ()!.notifier!;
static PixPaintLogic read(BuildContext context) => context.getInheritedWidgetOfExactType ()!.notifier!; } ```
此时剩下最后一件事,如何在两个业务逻辑对象更新时,通知画板进行重新绘制呢? CustomPainter
可以指定 repaint
参数,监听可监听对象,当其进行通知时,会触发画板的重绘。所以只要将两个可监听的,业务逻辑对象传入画板中即可:
在共享区域的子树,有上下文的地方,就可以得到业务逻辑对象。这里可以通过 read
方法,让绘制区不建立依赖关系,这样更新时 EditorArea 不会重新构建,仅通知画板进行更新:
5、性能方面
目前 100*100
的网格中,需要绘制 10000 个方格,此时 windows 中的帧率远远低于 120 FPS。没有任何性能问题,后续随着功能的增加,会多多注意性能方面的变化,那本文就到这里,谢谢观看 ~