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
This commit is contained in:
iven
2026-06-01 13:18:36 +08:00
parent 8331db63ba
commit e07da7addb
6 changed files with 573 additions and 153 deletions

View File

@@ -1,12 +1,14 @@
/// 手写 Canvas 主组件 — 暖记手写引擎的用户交互层。
///
/// 核心设计决策
/// - 使用 [Listener] 而非 [GestureDetector] 处理指针事件(降低延迟)
/// - 支持触控笔模式下的掌心抑制palm rejection
/// - 轻量去抖±1px 移动平均过滤,消除手指微小抖动
/// - [RepaintBoundary] 隔离重绘范围,避免影响父组件
/// 双层渲染架构
/// - Layer 1 (CachedStrokesPainter): 已完成笔画的合成位图,仅 drawImage
/// - Layer 2 (ActiveStrokePainter): 当前正在绘制的笔画,实时计算轮廓
///
/// 性能目标p99 延迟 < 16ms满足 60fps 要求)。
/// 性能优化:
/// - 使用 [Listener] 而非 [GestureDetector] 处理指针事件(降低延迟)
/// - 可变 `_currentPoints` 缓冲区 + `ValueNotifier` 驱动 Layer 2 重绘
/// - Layer 1 仅在笔画完成/撤销/清除时更新(不随指针移动重绘)
/// - [RepaintBoundary] 隔离重绘范围
library;
import 'package:flutter/gestures.dart';
@@ -14,7 +16,9 @@ import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'stroke_model.dart';
import 'stroke_renderer.dart';
import 'stroke_cache.dart';
import 'cached_strokes_painter.dart';
import 'active_stroke_painter.dart';
// ============================================================
// 手写 Canvas 组件
@@ -68,8 +72,14 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
// 状态
// ============================================================
/// 正在绘制中的笔画采样点。
List<StrokePoint> _currentPoints = const [];
/// 正在绘制中的笔画采样点可变缓冲区O(1) 添加)
final List<StrokePoint> _currentPoints = [];
/// 版本号,每次 pointer move 递增,驱动 ActiveStrokePainter 重绘。
final ValueNotifier<int> _strokeVersion = ValueNotifier(0);
/// 笔画光栅化缓存。
final StrokeRasterCache _cache = StrokeRasterCache();
/// RepaintBoundary 的 key用于隔离重绘。
final GlobalKey _repaintBoundaryKey = GlobalKey();
@@ -82,9 +92,6 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
// ============================================================
/// 是否启用掌心抑制。
///
/// 当设备支持触控笔 (stylus) 时启用:只响应 stylus 输入,
/// 忽略触摸 (touch) 输入,防止手掌误触。
bool _palmRejectionEnabled = false;
// ============================================================
@@ -97,14 +104,48 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
/// 去抖阈值(像素):移动距离小于此值的点将被过滤。
static const double _debounceThreshold = 1.0;
// ============================================================
// 生命周期
// ============================================================
@override
void initState() {
super.initState();
// 初始同步:如果有预加载的笔画(如编辑已有日记)
if (widget.strokes.isNotEmpty) {
_syncCacheAfterBuild();
}
}
@override
void dispose() {
_cache.dispose();
_strokeVersion.dispose();
super.dispose();
}
@override
void didUpdateWidget(HandwritingCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
// 检测 strokes 列表变化(撤销/重做/清除/外部加载)
if (!identical(widget.strokes, oldWidget.strokes)) {
_syncCacheAfterBuild();
}
}
/// 在 build 完成后同步缓存(避免在 build 中触发异步操作)
void _syncCacheAfterBuild() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_cache.syncStrokes(widget.strokes);
});
}
// ============================================================
// 指针事件处理
// ============================================================
/// 指针按下:开始新笔画。
///
/// 掌心抑制逻辑:如果已检测到触控笔设备,则只响应 stylus 事件,
/// 忽略 touch 事件。首次检测到 stylus 时自动启用抑制。
void _onPointerDown(PointerDownEvent event) {
// 检测触控笔设备并启用掌心抑制
if (event.kind == PointerDeviceKind.stylus) {
@@ -124,8 +165,10 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_currentPoints = [point];
_currentPoints.clear();
_currentPoints.add(point); // O(1)
_lastAcceptedPoint = Offset(point.x, point.y);
_strokeVersion.value++; // 通知 ActiveStrokePainter 重绘
}
/// 指针移动:添加采样点(带去抖过滤)。
@@ -152,10 +195,9 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
timestamp: DateTime.now().millisecondsSinceEpoch,
);
setState(() {
_currentPoints = [..._currentPoints, point];
});
_currentPoints.add(point); // O(1) amortized替代旧的 [...spread]
_lastAcceptedPoint = candidate;
_strokeVersion.value++; // 仅驱动 Layer 2 重绘,不触发 setState
}
/// 指针抬起:完成笔画并通过回调传递给上层。
@@ -169,36 +211,38 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
// 至少需要 2 个点才能构成有意义的笔画
if (_currentPoints.length < 2) {
_currentPoints = const [];
_currentPoints.clear();
_lastAcceptedPoint = null;
_strokeVersion.value++; // 清除当前笔画显示
return;
}
// 创建不可变的 Stroke 对象
// 创建不可变的 Stroke 对象(快照当前点列表)
final stroke = Stroke(
id: _uuid.v4(),
points: List.unmodifiable(_currentPoints),
points: List.unmodifiable(List.of(_currentPoints)),
brushType: widget.brushType,
color: widget.brushColor,
width: widget.brushWidth,
);
// 通知上层
// 清除当前绘制状态
_currentPoints.clear();
_lastAcceptedPoint = null;
_strokeVersion.value++; // 清除 Layer 2
// 通知上层BLoC
widget.onStrokeCompleted?.call(stroke);
// 重置当前绘制状态
setState(() {
_currentPoints = const [];
_lastAcceptedPoint = null;
});
// 光栅化新笔画到缓存(异步,不阻塞 UI
_cache.addStroke(stroke);
}
/// 指针取消(如来电打断):丢弃当前笔画。
void _onPointerCancel(PointerCancelEvent event) {
setState(() {
_currentPoints = const [];
_lastAcceptedPoint = null;
});
_currentPoints.clear();
_lastAcceptedPoint = null;
_strokeVersion.value++; // 清除 Layer 2
}
// ============================================================
@@ -207,17 +251,6 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
@override
Widget build(BuildContext context) {
// 构造当前正在绘制的笔画(用于实时预览)
final currentStroke = _currentPoints.length >= 2
? Stroke(
id: '__current__', // 临时 ID不会被持久化
points: _currentPoints,
brushType: widget.brushType,
color: widget.brushColor,
width: widget.brushWidth,
)
: null;
return RepaintBoundary(
key: _repaintBoundaryKey,
child: ClipRect(
@@ -227,13 +260,45 @@ class _HandwritingCanvasState extends State<HandwritingCanvas> {
onPointerMove: _onPointerMove,
onPointerUp: _onPointerUp,
onPointerCancel: _onPointerCancel,
child: CustomPaint(
painter: StrokePainter(
completedStrokes: widget.strokes,
currentStroke: currentStroke,
),
// 占满父组件可用空间
size: Size.infinite,
child: LayoutBuilder(
builder: (context, constraints) {
final canvasSize = Size(
constraints.maxWidth,
constraints.maxHeight,
);
_cache.ensureSize(canvasSize);
return Stack(
fit: StackFit.expand,
children: [
// Layer 1: 已完成笔画(光栅化缓存合成图)
CustomPaint(
painter: CachedStrokesPainter(
compositeImage: _cache.compositeImage,
layerVersion: _cache.layerVersion,
),
size: Size.infinite,
),
// Layer 2: 当前正在绘制的笔画
ListenableBuilder(
listenable: _strokeVersion,
builder: (context, _) {
return CustomPaint(
painter: ActiveStrokePainter(
points: _currentPoints,
brushType: widget.brushType,
color: widget.brushColor,
width: widget.brushWidth,
version: _strokeVersion.value,
),
size: Size.infinite,
);
},
),
],
);
},
),
),
),