301 lines
9.9 KiB
Dart
301 lines
9.9 KiB
Dart
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<GestureCanvasPage> createState() => _GestureCanvasPageState();
|
||
}
|
||
|
||
class _GestureCanvasPageState extends State<GestureCanvasPage> {
|
||
final List<List<Offset>> _allStrokes = [];
|
||
final _currentStrokeNotifier = ValueNotifier<List<Offset>>([]);
|
||
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<void> _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<List<Offset>>(
|
||
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<Offset> currentStroke;
|
||
final Paint _paint;
|
||
|
||
// [修改] 接收新增的调试参数
|
||
final List<List<Offset>> 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<List<Offset>> 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;
|
||
}
|
||
}
|