Files
nj/app/lib/features/editor/views/editor_page.dart
iven e07da7addb perf(app): 手写引擎性能优化 — 双层架构 + 光栅化缓存 + O(1) 点缓冲
性能优化:
- 新建 StrokeRasterCache: 已完成笔画光栅化为 ui.Image 合成位图
- 新建 CachedStrokesPainter: 每帧仅 drawImage,O(1) 开销
- 新建 ActiveStrokePainter: 仅渲染当前笔画,isComplete: false
- _currentPoints 改为可变缓冲区 + ValueNotifier 驱动,消除 O(N²) 列表拷贝
- 双层 Stack 架构: 已缓存层(不随指针移动重绘) + 实时层(仅当前笔画)

Bug 修复:
- 橡皮擦 saveLayer 合成: BlendMode.dstOut 在离屏缓冲区中正确工作
- pointsToOutline 新增 isComplete 参数: 实时绘制传 false,完成笔画传 true
- 模式切换不再销毁 HandwritingCanvas: IgnorePointer 替代 if/else 分支

架构改进:
- 提取 createPaintForStroke() 为顶层函数,供缓存和 Painter 共用
- 移除旧 StrokePainter 类,由双层 Painter 替代
- LayoutBuilder 跟踪画布尺寸,尺寸变化时缓存自动失效

文件变更:
- 新建 stroke_cache.dart (~210 行)
- 新建 cached_strokes_painter.dart (~35 行)
- 新建 active_stroke_painter.dart (~70 行)
- 重写 handwriting_canvas.dart (~300 行)
- 重构 stroke_renderer.dart (~185 行, 移除旧 Painter)
- 修改 editor_page.dart (IgnorePointer 模式切换)

验证: flutter analyze 0 error
2026-06-01 13:18:36 +08:00

235 lines
6.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 手账编辑器页面 — 三层 Stack 架构
//
// Layer 1 (底层): HandwritingCanvas — 手写画布
// Layer 2 (中层): DraggableElements — 贴纸/照片/文字元素
// Layer 3 (顶层): EditorToolbar — 底部工具栏 + 顶栏操作
//
// 交互逻辑:
// - 画笔模式 → Layer 1 接收手势Layer 2 透传
// - 选择模式 → Layer 2 接收手势Layer 1 透传
// - 工具栏 → 始终在最顶层
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../core/constants/design_tokens.dart';
import '../../../data/models/journal_element.dart';
import '../bloc/editor_bloc.dart';
import '../widgets/handwriting_canvas.dart';
import '../widgets/draggable_element.dart';
import '../widgets/editor_toolbar.dart';
/// 手账编辑器页面
class EditorPage extends StatelessWidget {
final String? journalId;
const EditorPage({super.key, this.journalId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => EditorBloc(
onSave: (state) {
// TODO: 通过 JournalRepository 保存到 Isar
debugPrint('自动保存: ${state.strokes.length} 笔画, ${state.elements.length} 元素');
},
),
child: _EditorView(journalId: journalId),
);
}
}
class _EditorView extends StatelessWidget {
final String? journalId;
const _EditorView({this.journalId});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surface,
body: SafeArea(
child: Column(
children: [
// 顶栏
_buildTopBar(context),
// 编辑区域(三层 Stack
Expanded(
child: BlocBuilder<EditorBloc, EditorState>(
builder: (context, state) {
return _EditorStack(state: state);
},
),
),
// 底部工具栏
BlocBuilder<EditorBloc, EditorState>(
builder: (context, state) {
return EditorToolbar(
state: state,
onEvent: (event) => context.read<EditorBloc>().add(event),
);
},
),
],
),
),
);
}
/// 顶部操作栏 — 返回/日记标题/完成
Widget _buildTopBar(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing8),
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border(
bottom: BorderSide(color: colorScheme.outline.withValues(alpha: 0.1)),
),
),
child: Row(
children: [
// 返回按钮
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back_rounded),
tooltip: '返回',
),
const SizedBox(width: DesignTokens.spacing8),
// 日记标题
Expanded(
child: Text(
journalId != null ? '编辑日记' : '新建日记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
// 完成按钮
FilledButton.tonal(
onPressed: () {
// TODO: 保存并返回
context.pop();
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.spacing16),
minimumSize: const Size(0, 36),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('完成'),
),
],
),
);
}
}
// ============================================================
// 编辑器三层 Stack
// ============================================================
/// 编辑器 Stack — 三层叠加结构
///
/// Layer 1 (底层): HandwritingCanvas
/// Layer 2 (中层): 可拖拽元素(贴纸/照片/文字)
/// Layer 3 (顶层): 由 _EditorView 中的工具栏处理
class _EditorStack extends StatelessWidget {
final EditorState state;
const _EditorStack({required this.state});
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
// Layer 1: 手写画布(底层)
// 始终渲染,通过 IgnorePointer 控制交互(避免模式切换时销毁重建)
IgnorePointer(
ignoring: !state.isDrawingMode,
child: HandwritingCanvas(
brushType: state.brushType,
brushColor: state.brushColor,
brushWidth: state.brushWidth,
strokes: state.strokes,
onStrokeCompleted: (stroke) {
context.read<EditorBloc>().add(StrokeCompleted(stroke));
},
),
),
// Layer 2: 可拖拽元素(中层)
if (state.elements.isNotEmpty)
_buildElementLayer(context),
// 空状态提示
if (state.strokes.isEmpty && state.elements.isEmpty)
_buildEmptyHint(context),
],
);
}
/// 元素层 — 所有日记元素叠加显示
Widget _buildElementLayer(BuildContext context) {
// 按 zIndex 排序
final sorted = List<JournalElement>.from(state.elements)
..sort((a, b) => a.zIndex.compareTo(b.zIndex));
return Stack(
children: sorted.map((element) {
return DraggableElement(
key: ValueKey(element.id),
element: element,
isSelected: state.selectedElementId == element.id,
onTap: (id) {
context.read<EditorBloc>().add(ElementSelected(id));
},
onMoved: (id, x, y) {
context.read<EditorBloc>().add(ElementMoved(
elementId: id,
positionX: x,
positionY: y,
));
},
onDeleted: (id) {
context.read<EditorBloc>().add(ElementRemoved(id));
},
);
}).toList(),
);
}
/// 空状态提示
Widget _buildEmptyHint(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.draw_rounded,
size: 48,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.15),
),
const SizedBox(height: DesignTokens.spacing12),
Text(
'在这里开始书写吧 ✏️',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
),
),
],
),
);
}
}