/// 手写 Canvas 主组件 — 暖记手写引擎的用户交互层。 /// /// 双层渲染架构: /// - Layer 1 (CachedStrokesPainter): 已完成笔画的合成位图,仅 drawImage /// - Layer 2 (ActiveStrokePainter): 当前正在绘制的笔画,实时计算轮廓 /// /// 性能优化: /// - 使用 [Listener] 而非 [GestureDetector] 处理指针事件(降低延迟) /// - 可变 `_currentPoints` 缓冲区 + `ValueNotifier` 驱动 Layer 2 重绘 /// - Layer 1 仅在笔画完成/撤销/清除时更新(不随指针移动重绘) /// - [RepaintBoundary] 隔离重绘范围 library; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import 'stroke_model.dart'; import 'stroke_cache.dart'; import 'cached_strokes_painter.dart'; import 'active_stroke_painter.dart'; // ============================================================ // 手写 Canvas 组件 // ============================================================ /// 手写画布,接收用户输入并实时渲染笔画。 /// /// 使用方式: /// ```dart /// HandwritingCanvas( /// brushType: BrushType.pen, /// brushColor: '#2D2420', /// brushWidth: 3.0, /// strokes: editorState.strokes, /// onStrokeCompleted: (stroke) => context.read().add( /// StrokeCompleted(stroke), /// ), /// ) /// ``` class HandwritingCanvas extends StatefulWidget { const HandwritingCanvas({ super.key, this.brushType = BrushType.pen, this.brushColor = '#2D2420', this.brushWidth = 3.0, this.onStrokeCompleted, this.strokes = const [], }); /// 当前选中的画笔类型。 final BrushType brushType; /// 当前画笔颜色(CSS 十六进制格式,如 '#2D2420')。 final String brushColor; /// 当前画笔宽度(基准 3.0)。 final double brushWidth; /// 笔画完成时的回调,将新笔画传递给上层(通常是 BLoC)。 final ValueChanged? onStrokeCompleted; /// 已有的笔画列表(用于重绘历史内容)。 final List strokes; @override State createState() => _HandwritingCanvasState(); } class _HandwritingCanvasState extends State { // ============================================================ // 状态 // ============================================================ /// 正在绘制中的笔画采样点(可变缓冲区,O(1) 添加)。 final List _currentPoints = []; /// 版本号,每次 pointer move 递增,驱动 ActiveStrokePainter 重绘。 final ValueNotifier _strokeVersion = ValueNotifier(0); /// 笔画光栅化缓存。 final StrokeRasterCache _cache = StrokeRasterCache(); /// RepaintBoundary 的 key,用于隔离重绘。 final GlobalKey _repaintBoundaryKey = GlobalKey(); /// UUID 生成器实例。 static const _uuid = Uuid(); // ============================================================ // 掌心抑制 // ============================================================ /// 是否启用掌心抑制。 bool _palmRejectionEnabled = false; // ============================================================ // 去抖 // ============================================================ /// 上一个有效采样点的坐标(用于去抖计算)。 Offset? _lastAcceptedPoint; /// 去抖阈值(像素):移动距离小于此值的点将被过滤。 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); }); } // ============================================================ // 指针事件处理 // ============================================================ /// 指针按下:开始新笔画。 void _onPointerDown(PointerDownEvent event) { // 检测触控笔设备并启用掌心抑制 if (event.kind == PointerDeviceKind.stylus) { _palmRejectionEnabled = true; } // 掌心抑制:如果已启用且当前非 stylus 输入,忽略 if (_palmRejectionEnabled && event.kind != PointerDeviceKind.stylus) { return; } // 开始新笔画 final point = StrokePoint( x: event.localPosition.dx, y: event.localPosition.dy, pressure: event.pressure, timestamp: DateTime.now().millisecondsSinceEpoch, ); _currentPoints.clear(); _currentPoints.add(point); // O(1) _lastAcceptedPoint = Offset(point.x, point.y); _strokeVersion.value++; // 通知 ActiveStrokePainter 重绘 } /// 指针移动:添加采样点(带去抖过滤)。 void _onPointerMove(PointerMoveEvent event) { if (_currentPoints.isEmpty) return; // 掌心抑制 if (_palmRejectionEnabled && event.kind != PointerDeviceKind.stylus) { return; } final candidate = Offset(event.localPosition.dx, event.localPosition.dy); // 轻量去抖:移动距离 < 阈值时忽略(消除手指微抖) if (_lastAcceptedPoint != null) { final distance = (candidate - _lastAcceptedPoint!).distance; if (distance < _debounceThreshold) return; } final point = StrokePoint( x: candidate.dx, y: candidate.dy, pressure: event.pressure, timestamp: DateTime.now().millisecondsSinceEpoch, ); _currentPoints.add(point); // O(1) amortized,替代旧的 [...spread] _lastAcceptedPoint = candidate; _strokeVersion.value++; // 仅驱动 Layer 2 重绘,不触发 setState } /// 指针抬起:完成笔画并通过回调传递给上层。 void _onPointerUp(PointerUpEvent event) { if (_currentPoints.isEmpty) return; // 掌心抑制 if (_palmRejectionEnabled && event.kind != PointerDeviceKind.stylus) { return; } // 至少需要 2 个点才能构成有意义的笔画 if (_currentPoints.length < 2) { _currentPoints.clear(); _lastAcceptedPoint = null; _strokeVersion.value++; // 清除当前笔画显示 return; } // 创建不可变的 Stroke 对象(快照当前点列表) final stroke = Stroke( id: _uuid.v4(), 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); // 光栅化新笔画到缓存(异步,不阻塞 UI) _cache.addStroke(stroke); } /// 指针取消(如来电打断):丢弃当前笔画。 void _onPointerCancel(PointerCancelEvent event) { _currentPoints.clear(); _lastAcceptedPoint = null; _strokeVersion.value++; // 清除 Layer 2 } // ============================================================ // 构建 // ============================================================ @override Widget build(BuildContext context) { return RepaintBoundary( key: _repaintBoundaryKey, child: ClipRect( child: Listener( behavior: HitTestBehavior.opaque, onPointerDown: _onPointerDown, onPointerMove: _onPointerMove, onPointerUp: _onPointerUp, onPointerCancel: _onPointerCancel, 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, ); }, ), ], ); }, ), ), ), ); } }