import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:making_school_asignment_app/common/utils/gesture_recognition/ramer_douglas_peucker.dart'; import 'package:making_school_asignment_app/common/utils/gesture_recognition/shape_recognizer.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return ScreenUtilInit( designSize: const Size(375, 812), builder: () => MaterialApp( title: '手势识别实验', theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), home: const GestureCanvasPage(), ), ); } } class GestureCanvasPage extends StatefulWidget { const GestureCanvasPage({super.key}); @override State createState() => _GestureCanvasPageState(); } class _GestureCanvasPageState extends State { final List> _allStrokes = []; final _currentStrokeNotifier = ValueNotifier>([]); ui.Image? _cachedImage; GestureType _recognizedGesture = GestureType.none; late ShapeRecognizer _recognizer; // [新增] 用于控制是否显示可视化特征点的状态 bool _showSimplifiedPoints = true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { // 在这里初始化识别器,可以方便地调整参数 _recognizer = ShapeRecognizer( rdpEpsilon: 8.r, // 你可以集中在这里调整这个值 /// 勾的最小角度调小一些,避免漏识别 checkAngleMin: 30.0, /// 勾的最大角度调大一些,避免误识别 checkAngleMax: 150.0, crossAngleMin: 30.0, crossAngleMax: 160.0, minIntersectionDistance: 0.1, checkLengthRatio: 1.1, // [核心修改] 放宽对斜线斜率的限制 slashSlopeMin: 0.2, // 这个值允许用户绘制相当平缓的斜线(角度低至约11度),这覆盖了绝大多数的书写习惯,同时这个斜率也足以和基本水平的线条区分开。 slashSlopeMax: 5.0, // 这个值允许用户绘制非常陡峭的斜线(角度高达约79度)。对于比这更陡峭的线条,它们已经非常接近垂直线,而我们之前优化过的 _isSlash 函数中有专门处理垂直线的逻辑 (deltaX < 1e-6),所以这个组合非常稳健,几乎可以覆盖所有非水平的直线。 ); } }); } Future _updateCachedImage() async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final size = context.size!; _GestureCachePainter(allStrokes: _allStrokes).paint(canvas, size); final picture = recorder.endRecording(); final newImage = await picture.toImage(size.width.toInt(), size.height.toInt()); if (mounted) { setState(() { _cachedImage = newImage; }); } } void _onPanStart(DragStartDetails details) { _currentStrokeNotifier.value = [details.localPosition]; } void _onPanUpdate(DragUpdateDetails details) { _currentStrokeNotifier.value = List.from(_currentStrokeNotifier.value)..add(details.localPosition); } void _onPanEnd(DragEndDetails details) { if (_currentStrokeNotifier.value.isNotEmpty) { _allStrokes.add(List.from(_currentStrokeNotifier.value)); _currentStrokeNotifier.value = []; _updateCachedImage().then((_) { if (mounted) { setState(() { _recognizedGesture = _recognizer.recognize(_allStrokes); print('识别到的手势: $_recognizedGesture'); }); } }); } } void _clearCanvas() { setState(() { _allStrokes.clear(); _recognizedGesture = GestureType.none; _cachedImage = null; }); _currentStrokeNotifier.value = []; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('手势识别 (可视化调试)'), actions: [ // [新增] 在AppBar中添加一个信息提示 Padding( padding: const EdgeInsets.only(right: 16.0), child: Icon( Icons.science_outlined, color: _showSimplifiedPoints ? Colors.green : Colors.grey, ), ) ], ), body: Stack( children: [ GestureDetector( onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, child: ValueListenableBuilder>( valueListenable: _currentStrokeNotifier, builder: (context, currentStroke, child) { return CustomPaint( painter: _GestureCanvasPainter( cachedImage: _cachedImage, currentStroke: currentStroke, // [修改] 将调试所需的数据传入Painter allStrokes: _allStrokes, rdpEpsilon: _recognizer.rdpEpsilon, showSimplifiedPoints: _showSimplifiedPoints, ), size: Size.infinite, ); }, ), ), Align( alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.all(16.0), child: Text( '识别结果: ${_recognizedGesture.toString().split('.').last}\nEpsilon: ${_recognizer.rdpEpsilon.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue), ), ), ), ], ), // [修改] 使用Column包裹多个FAB floatingActionButton: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ FloatingActionButton( onPressed: _clearCanvas, tooltip: '清空画布', child: const Icon(Icons.clear), ), const SizedBox(height: 10), // [新增] 用于切换可视化模式的按钮 FloatingActionButton( onPressed: () { setState(() { _showSimplifiedPoints = !_showSimplifiedPoints; }); }, tooltip: '切换特征点显示', backgroundColor: _showSimplifiedPoints ? Colors.green : Colors.grey[400], child: const Icon(Icons.science_outlined), ), ], ), ); } } class _GestureCanvasPainter extends CustomPainter { final ui.Image? cachedImage; final List currentStroke; final Paint _paint; // [修改] 接收新增的调试参数 final List> allStrokes; final double rdpEpsilon; final bool showSimplifiedPoints; final Paint _debugPaint; // [新增] 专门用于绘制特征点的画笔 _GestureCanvasPainter({ required this.cachedImage, required this.currentStroke, required this.allStrokes, required this.rdpEpsilon, required this.showSimplifiedPoints, }) : _paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5.0 ..style = PaintingStyle.stroke, // [新增] 初始化调试画笔 _debugPaint = Paint() ..color = Colors.red ..strokeCap = StrokeCap.round ..strokeWidth = 8.0; @override void paint(Canvas canvas, Size size) { // 步骤1: 绘制缓存的图片 (非常快) if (cachedImage != null) { canvas.drawImage(cachedImage!, Offset.zero, Paint()); } // 步骤2: 绘制当前正在画的笔画 if (currentStroke.length > 1) { final path = Path(); path.moveTo(currentStroke.first.dx, currentStroke.first.dy); for (int i = 1; i < currentStroke.length; i++) { path.lineTo(currentStroke[i].dx, currentStroke[i].dy); } canvas.drawPath(path, _paint); } // [新增] 步骤3: 如果开启了可视化,则绘制简化后的特征点 if (showSimplifiedPoints) { // 为所有已完成的笔画绘制特征点 for (final stroke in allStrokes) { if (stroke.length > 1) { final simplifiedPoints = RamerDouglasPeucker.simplify(stroke, rdpEpsilon); canvas.drawPoints(ui.PointMode.points, simplifiedPoints, _debugPaint); } } // 为当前正在绘制的笔画也实时显示特征点 if (currentStroke.length > 1) { final simplifiedPoints = RamerDouglasPeucker.simplify(currentStroke, rdpEpsilon); canvas.drawPoints(ui.PointMode.points, simplifiedPoints, _debugPaint); } } } @override bool shouldRepaint(covariant _GestureCanvasPainter oldDelegate) { // [修改] 当笔画、总笔画数或可视化开关变化时,都需要重绘 return oldDelegate.currentStroke != currentStroke || oldDelegate.showSimplifiedPoints != showSimplifiedPoints || oldDelegate.allStrokes.length != allStrokes.length; } } // 缓存绘制器保持不变 class _GestureCachePainter extends CustomPainter { final List> allStrokes; final Paint _paint; _GestureCachePainter({required this.allStrokes}) : _paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5.0 ..style = PaintingStyle.stroke; @override void paint(Canvas canvas, Size size) { for (final stroke in allStrokes) { if (stroke.length > 1) { final path = Path(); path.moveTo(stroke.first.dx, stroke.first.dy); for (int i = 1; i < stroke.length; i++) { path.lineTo(stroke[i].dx, stroke[i].dy); } canvas.drawPath(path, _paint); } } } @override bool shouldRepaint(covariant _GestureCachePainter oldDelegate) { return oldDelegate.allStrokes != allStrokes; } }