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:
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user