From 8c10e6eb4de40a07c3c0eb008764e1f11d9c9421 Mon Sep 17 00:00:00 2001 From: "1147192855@qq.com" <1147192855@qq.com> Date: Wed, 24 Apr 2024 13:46:19 +0800 Subject: [PATCH] no message --- .gitignore | 2 + .../common/model/job/gesture_recording.dart | 19 + .../lib/common/model/job/job_handwriting.dart | 21 +- .../model/job/test_questions_image_info.dart | 15 +- .../pages/homework_correction/job_home.dart | 21 +- ...ndwriting_drawing_trajectory_provider.dart | 18 + .../quick_check_personal.dart | 12 +- .../widget/answer_handwriting.dart | 893 +++++++++++++++--- marking_app/lib/utils/my_time_util.dart | 65 ++ 9 files changed, 912 insertions(+), 154 deletions(-) create mode 100644 marking_app/lib/pages/homework_correction/providers/handwriting_drawing_trajectory_provider.dart create mode 100644 marking_app/lib/utils/my_time_util.dart diff --git a/.gitignore b/.gitignore index 64f504f..166ba42 100644 --- a/.gitignore +++ b/.gitignore @@ -225,3 +225,5 @@ marking_app/lib/pages/homework_correction/job_home.g.dart marking_app/lib/common/model/marking/keyboard_assist_event.g.dart marking_app/lib/common/model/marking/marking_history_zoom_info.g.dart marking_app/lib/common/model/job/job_handwriting.g.dart +marking_app/lib/utils/my_time_util.g.dart +marking_app/lib/pages/homework_correction/widget/answer_handwriting.g.dart diff --git a/marking_app/lib/common/model/job/gesture_recording.dart b/marking_app/lib/common/model/job/gesture_recording.dart index a2ea382..5e36be8 100644 --- a/marking_app/lib/common/model/job/gesture_recording.dart +++ b/marking_app/lib/common/model/job/gesture_recording.dart @@ -14,11 +14,30 @@ class GestureRecording { bool scopeBox; + int intervalTime; // 间隔时间 + GestureRecording({ required this.eraser, required this.annotationSwitch, this.data, this.usageTime, this.scopeBox = false, + this.intervalTime = 0, + }); +} + +/** + * 手势记录(原稿笔记还原) + */ +class GestureHandwritingRecording { + int stroke; + int usageTime; // 用时 + Offset data; + int intervalTime; // 间隔时间 + GestureHandwritingRecording({ + required this.stroke, + required this.data, + required this.usageTime, + required this.intervalTime, }); } diff --git a/marking_app/lib/common/model/job/job_handwriting.dart b/marking_app/lib/common/model/job/job_handwriting.dart index 4d947fe..72931c9 100644 --- a/marking_app/lib/common/model/job/job_handwriting.dart +++ b/marking_app/lib/common/model/job/job_handwriting.dart @@ -16,12 +16,7 @@ class JobHandwriting extends Object { @JsonKey(name: 'pageCount') int pageCount; - JobHandwriting( - this.lattices, - this.paperPicture, - this.pageNum, - this.pageCount, - ); + JobHandwriting(this.lattices, this.paperPicture, this.pageNum, this.pageCount); factory JobHandwriting.fromJson(Map srcJson) => _$JobHandwritingFromJson(srcJson); @@ -34,20 +29,18 @@ class Lattices extends Object { int stroke; @JsonKey(name: 'x') - int x; + double x; @JsonKey(name: 'y') - int y; + double y; @JsonKey(name: 'time') int time; - Lattices( - this.stroke, - this.x, - this.y, - this.time, - ); + @JsonKey(name: 'intervalTime') + int intervalTime; + + Lattices(this.stroke, this.x, this.y, this.time, [this.intervalTime = 0]); factory Lattices.fromJson(Map srcJson) => _$LatticesFromJson(srcJson); diff --git a/marking_app/lib/common/model/job/test_questions_image_info.dart b/marking_app/lib/common/model/job/test_questions_image_info.dart index fed08ac..e9a1e7c 100644 --- a/marking_app/lib/common/model/job/test_questions_image_info.dart +++ b/marking_app/lib/common/model/job/test_questions_image_info.dart @@ -36,12 +36,7 @@ class TestQuestionsImageInfo extends Object { double? imageHeightOffsetend; TestQuestionsImageInfo( - {required this.width, - required this.height, - required this.url, - required this.boxWidth, - required this.boxHeight, - this.pixelRatio = 1}) { + {required this.width, required this.height, required this.url, required this.boxWidth, required this.boxHeight, this.pixelRatio = 1}) { // print('图片宽度:$width'); // print('图片高度:$height'); @@ -60,6 +55,14 @@ class TestQuestionsImageInfo extends Object { } } + // 重新计算 + void calculateStartAndEndHeight([double otherHeight = 0]) { + if (scaleHeight != null) { + imageHeightOffsetStart = (boxHeight - (scaleHeight! + otherHeight)) / 2; + imageHeightOffsetend = imageHeightOffsetStart! + scaleHeight!; + } + } + factory TestQuestionsImageInfo.fromJson(Map srcJson) => _$TestQuestionsImageInfoFromJson(srcJson); Map toJson() => _$TestQuestionsImageInfoToJson(this); diff --git a/marking_app/lib/pages/homework_correction/job_home.dart b/marking_app/lib/pages/homework_correction/job_home.dart index a442f57..9e067a7 100644 --- a/marking_app/lib/pages/homework_correction/job_home.dart +++ b/marking_app/lib/pages/homework_correction/job_home.dart @@ -76,14 +76,17 @@ class _JobHomeState extends State with CommonMixin, EventBusMixin, Auto child: ListView( children: [ Container( - height: 200.h, - width: double.infinity, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/job_home_top_bgm.png'), - fit: BoxFit.fill, // 完全填充 - ), + constraints: BoxConstraints( + minHeight: 200.h, + maxWidth: double.infinity, ), + // decoration: BoxDecoration( + // image: DecorationImage( + // image: AssetImage('assets/images/job_home_top_bgm.png'), + // fit: BoxFit.fitWidth, // 完全填充 + // ), + // ), + child: Image.asset('assets/images/job_home_top_bgm.png', fit: BoxFit.fitWidth), ), SizedBox(height: 30.h), SlidingData([ @@ -104,8 +107,8 @@ class _JobHomeState extends State with CommonMixin, EventBusMixin, Auto navigationUrl: '${RouterManager.jobStudentGroupPath}?page=set', ) ], 0), - spaceWidth, - $TermRow([EntranceModel(title: '批阅设置', image: 'assets/images/job_home_marking_set.png', navigationUrl: '')], 0), + // spaceWidth, + // $TermRow([EntranceModel(title: '批阅设置', image: 'assets/images/job_home_marking_set.png', navigationUrl: '')], 0), ], ), ), diff --git a/marking_app/lib/pages/homework_correction/providers/handwriting_drawing_trajectory_provider.dart b/marking_app/lib/pages/homework_correction/providers/handwriting_drawing_trajectory_provider.dart new file mode 100644 index 0000000..30f40bf --- /dev/null +++ b/marking_app/lib/pages/homework_correction/providers/handwriting_drawing_trajectory_provider.dart @@ -0,0 +1,18 @@ +/// 原稿作业回显 +// 回显批注轨迹 + +import 'package:marking_app/common/model/job/gesture_recording.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:marking_app/common/mixin/common.dart'; + +final jobHandwritingDrawingTrajectoryProvider = + StateNotifierProvider>( + (ref) => JobHandwritingDrawingTrajectoryProviderHandle([])); + +class JobHandwritingDrawingTrajectoryProviderHandle extends StateNotifier> with CommonMixin { + JobHandwritingDrawingTrajectoryProviderHandle(List progress) : super(progress); + + setVal(List val) { + state = val; + } +} diff --git a/marking_app/lib/pages/homework_correction/quick_check_personal.dart b/marking_app/lib/pages/homework_correction/quick_check_personal.dart index b6ef307..0c8fe89 100644 --- a/marking_app/lib/pages/homework_correction/quick_check_personal.dart +++ b/marking_app/lib/pages/homework_correction/quick_check_personal.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:marking_app/common/mixin/common.dart'; import 'package:marking_app/common/model/common/base_structure_result.dart'; import 'package:marking_app/common/model/job/job_data_report.dart'; @@ -12,19 +13,20 @@ import 'package:marking_app/utils/common_utils.dart'; import 'package:marking_app/utils/request/rest_client.dart'; import 'package:marking_app/utils/toast_utils.dart'; +import 'providers/handwriting_drawing_trajectory_provider.dart'; import 'widget/answer_handwriting.dart'; -class QuickCheckPersonal extends StatefulWidget { +class QuickCheckPersonal extends StatefulHookConsumerWidget { final int jobId; final int studentId; const QuickCheckPersonal({Key? key, required this.jobId, required this.studentId}) : super(key: key); @override - State createState() => _QuickCheckPersonalState(); + ConsumerState createState() => _QuickCheckPersonalState(); } -class _QuickCheckPersonalState extends State with CommonMixin { +class _QuickCheckPersonalState extends ConsumerState with CommonMixin { StudentDetails? studentInfo; void initState() { @@ -120,7 +122,9 @@ class _QuickCheckPersonalState extends State with CommonMixi ), InkWell( onTap: () { - showAnswerHandwriting(context, jobId: widget.jobId, studentId: widget.studentId); + showAnswerHandwriting(context, jobId: widget.jobId, studentId: widget.studentId).then((value) { + ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal([]); + }); }, child: Container( width: 93.r, diff --git a/marking_app/lib/pages/homework_correction/widget/answer_handwriting.dart b/marking_app/lib/pages/homework_correction/widget/answer_handwriting.dart index 8bf8322..bb5591a 100644 --- a/marking_app/lib/pages/homework_correction/widget/answer_handwriting.dart +++ b/marking_app/lib/pages/homework_correction/widget/answer_handwriting.dart @@ -1,12 +1,25 @@ +import 'dart:async'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:functional_widget_annotation/functional_widget_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:marking_app/common/mixin/common.dart'; import 'package:marking_app/common/model/job/job_handwriting.dart'; +import 'package:marking_app/common/model/job/test_questions_image_info.dart'; +import 'package:marking_app/pages/common/event_bus_mixin.dart'; +import 'package:marking_app/pages/homework_correction/hooks/use_cached_img_refresh.dart'; import 'package:marking_app/utils/index.dart'; import 'package:marking_app/utils/my_text.dart'; +import 'package:marking_app/utils/my_time_util.dart'; + +import '../../../common/model/job/gesture_recording.dart'; +import '../providers/handwriting_drawing_trajectory_provider.dart'; + +part 'answer_handwriting.g.dart'; /// 学生答题轨迹 class AnswerHandwriting extends Dialog { @@ -21,10 +34,15 @@ class AnswerHandwriting extends Dialog { Widget build(BuildContext context) { return Center( child: Container( - // color: Color.fromRGBO(0, 0, 0, 0.6), - width: ScreenUtil().screenWidth - 60.w, - height: ScreenUtil().screenHeight - 160.h, - child: AnswerHandwritingMainBox(jobId: jobId, studentId: studentId, pageNum: pageNum, questionNo: questionNo, closeCall: closeCall), + width: ScreenUtil().screenWidth - 60.r, + alignment: Alignment.center, + child: AnswerHandwritingMainBox( + jobId: jobId, + studentId: studentId, + pageNum: pageNum, + questionNo: questionNo, + closeCall: closeCall, + ), ), ); } @@ -67,6 +85,12 @@ class AnswerHandwritingMainBox extends HookWidget { var theData = _useStateModel.handwritingData.value; _useStateModel.pageNum.value = theData?.pageNum; _useStateModel.pageCount.value = theData?.pageCount ?? 0; + _useStateModel.playPause.value = false; + _useStateModel.constantFastSpeed.value = false; + Future.delayed(Duration.zero, () { + _useStateModel.handwritingKey.currentState?.ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal([]); + }); + _useStateModel.handwritingDetail.value = _useStateModel.getHandwritingDetail(theData); }); useValueChanged(_useStateModel.pageNum.value, (oldVal, __) { @@ -75,132 +99,42 @@ class AnswerHandwritingMainBox extends HookWidget { useEffect(() { _useStateModel.getData().catchError((e) => closeCall()); - _useStateModel.imageKey.value = UniqueKey(); return () {}; }, []); JobHandwriting? _data = _useStateModel.handwritingData.value; - print('这里是build:${_useStateModel.pageNum.value}'); + HandwritingInfo? _dataDetail = _useStateModel.handwritingDetail.value; - if (_data == null) return Container(); + if (_data == null || _dataDetail == null) return Container(); + print('数据长度:${_data.lattices.length}'); return Column( - mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Stack( + alignment: const FractionalOffset(0, 0.5), children: [ - Container( - child: CachedNetworkImage( - key: _useStateModel.imageKey.value, - fit: BoxFit.contain, - imageUrl: _data?.paperPicture ?? '', - imageBuilder: (context, imageProvider) { - return Image(image: imageProvider, fit: BoxFit.fitWidth); - }, - placeholder: (context, url) => Center(child: SpinKitWaveSpinner(color: Theme.of(context).primaryColor, size: 50.r)), - errorWidget: (context, url, error) { - return Center( - child: GestureDetector( - onTap: () => (_useStateModel.imageKey.value = UniqueKey()), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset('assets/images/test_paper_loading_failed.png'), - quickText('加载失败,点击重试', color: Color.fromRGBO(148, 163, 182, 1), size: 12.sp), - ], - ), - ), - ); - }, - ), + // 图片展示主框 + HandwritingDrawBox( + _data.paperPicture, + _dataDetail, + key: _useStateModel.handwritingKey, + ), + $PageNumberBox(_data.pageNum), + // 上一页按钮 + $PreviousNutton( + _useStateModel.pageNum.value, + () => _useStateModel.pageNum.value = _useStateModel.pageNum.value! - 1, + ), + // 下一题按钮 + $NextPageButton( + _useStateModel.pageNum.value, + _useStateModel.pageCount.value, + () => _useStateModel.pageNum.value = _useStateModel.pageNum.value! + 1, ), - if (_useStateModel.handwritingData.value != null && _useStateModel.pageNum.value != null && _useStateModel.pageNum.value! > 1) - Positioned( - left: 3.w, - top: 280.h, - child: FloatingActionButton( - heroTag: '点击前往上一题', - tooltip: '点击前往上一题', - focusColor: Theme.of(context).primaryColor, - backgroundColor: const Color.fromRGBO(24, 32, 32, 0.1), - elevation: 6.r, - onPressed: () => easyThrottle('answer_handwriting_previous', () { - _useStateModel.pageNum.value = _useStateModel.pageNum.value! - 1; - }), - child: Icon(Icons.arrow_back_ios, color: Colors.white, size: 22.sp), - ), - ), - // 下一题 按钮 - if (_useStateModel.handwritingData.value != null && - _useStateModel.pageNum.value != null && - _useStateModel.pageNum.value! < _useStateModel.pageCount.value) - Positioned( - right: 3.w, - top: 280.h, - child: FloatingActionButton( - heroTag: '点击前往下一题', - tooltip: '点击前往下一题', - elevation: 6.r, - backgroundColor: const Color.fromRGBO(24, 32, 32, 0.1), - onPressed: () => easyThrottle('answer_handwriting_next', () { - _useStateModel.pageNum.value = _useStateModel.pageNum.value! + 1; - }), - child: Icon(Icons.arrow_forward_ios, color: Colors.white, size: 22.sp), - ), - ), ], ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h), - alignment: Alignment.center, - color: Color.fromRGBO(0, 0, 0, 0.5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - InkWell( - onTap: () {}, - child: Icon( - // Icons.play_circle_outline - Icons.pause_circle_outline, - color: Colors.white, - size: 28.r, - ), - ), - SizedBox(width: 6.w), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 20.h, - color: Theme.of(context).primaryColor, - ), - SizedBox(height: 4.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - quickText('累计停顿:2次', color: Colors.white, size: 7.sp), - quickText('04:30', color: Colors.white, size: 7.sp), - ], - ) - ], - ), - ), - SizedBox(width: 16.w), - InkWell( - onTap: () {}, - child: Container( - // alignment: Alignment., - padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.5.h), - decoration: BoxDecoration(color: Color.fromRGBO(182, 197, 250, 1), borderRadius: BorderRadius.circular(4.r)), - child: quickText('原速播放', color: Color.fromRGBO(79, 114, 244, 1), size: 10.sp, align: TextAlign.center), - ), - ), - ], - ), - ), - ), + $BottomPlaybar(_dataDetail.timeConsuming, _dataDetail.pauseCount), ], ); } @@ -213,7 +147,13 @@ class UseMainBoxState with CommonMixin { final ValueNotifier pageNum; final ValueNotifier pageCount; final ValueNotifier handwritingData; - final ValueNotifier imageKey; + final ValueNotifier handwritingDetail; + + final ValueNotifier playPause; // 播放暂停 + final ValueNotifier constantFastSpeed; // 原速、快速 默认原速 + + GlobalKey<_HandwritingDrawBoxState> handwritingKey; + UseMainBoxState._({ required this.jobId, required this.studentId, @@ -221,7 +161,10 @@ class UseMainBoxState with CommonMixin { required this.handwritingData, required this.questionNo, required this.pageCount, - required this.imageKey, + required this.playPause, + required this.constantFastSpeed, + required this.handwritingDetail, + required this.handwritingKey, }); // 工厂构造函数 @@ -232,8 +175,11 @@ class UseMainBoxState with CommonMixin { questionNo: questionNo, pageNum: useState(pageNum), handwritingData: useState(null), + handwritingDetail: useState(null), pageCount: useState(0), - imageKey: useState(null), + playPause: useState(false), + constantFastSpeed: useState(false), + handwritingKey: GlobalKey(), ); } @@ -244,12 +190,717 @@ class UseMainBoxState with CommonMixin { var res = await _client.getHandwriting(jobId, studentId, questionNo, pageNum.value); if (res?.success ?? false) { handwritingData.value = res!.data; + if (handwritingData.value!.lattices.isEmpty) { + Future.delayed(Duration.zero, () => ToastUtils.showInfo('此页试题没有笔迹')); + } return; } Future.delayed(Duration(seconds: 1), () => ToastUtils.showError(res?.message ?? '笔记数据请求失败')); } catch (e) { + print(e); } finally { ToastUtils.dismiss(); } } + + HandwritingInfo? getHandwritingDetail(JobHandwriting? theData) { + if (theData == null) return null; + print('开始时间:${DateTime.now().millisecondsSinceEpoch}'); + // 笔画分组 + // var lattices = Map>.fromIterable( + // theData.lattices, + // key: (key) => key.stroke, + // value: (value) { + // // return theData.lattices.where((item) => item.stroke == value.stroke).toList()..sort((a, b) => a.time.compareTo(b.time)); + // return theData.lattices.where((item) => item.stroke == value.stroke).toList(); + // }, + // ); + var lattices = Map>(); + var theLattices = theData.lattices; + for (var i = 0; i < theLattices.length; i++) { + Lattices item = theLattices[i]; + if (!lattices.containsKey(item.stroke)) lattices[item.stroke] = []; + lattices[item.stroke]!.add(item); // 添加笔画数据 + } + + print('分组时间:${DateTime.now().millisecondsSinceEpoch}'); + List latticeKeys = lattices.keys.toList(); + int timeConsuming = 0; + if (latticeKeys.isNotEmpty) { + List? firstAction = lattices[latticeKeys[0]]; + List? lastAction = lattices[latticeKeys[latticeKeys.length - 1]]; + int firstTime = 0; + int lastTime = 0; + + if (firstAction?.isNotEmpty ?? false) { + // 第一个笔画集合 + firstTime = firstAction![0].time; + } + if (lastAction?.isNotEmpty ?? false) { + // 最后一笔画集合 + lastTime = lastAction![0].time; + } + timeConsuming = lastTime - firstTime; + } + print('获取数据总时间:${DateTime.now().millisecondsSinceEpoch}'); + var pauseCount = 0; // 停顿次数 + for (var i = 0; i < latticeKeys.length; i++) { + var currentLattices = lattices[latticeKeys[i]]!; // 当前循环笔画集合 + var prevLattices = i == 0 ? null : lattices[latticeKeys[i - 1]]!; // 下一个笔画集合 + for (var j = 0; j < currentLattices.length; j++) { + var intervalTime = 0; + var lattice = currentLattices[j]; + + if (j != 0) { + var prevItem = currentLattices[j - 1]; + var adjacentSpacingTime = lattice.time - prevItem.time; + if (adjacentSpacingTime > 5000) { + // 大于5秒算一次停顿 + pauseCount++; + } + intervalTime = adjacentSpacingTime + prevItem.intervalTime; + } else { + if (i != 0 && prevLattices != null) { + var prevLatticeLastItem = prevLattices[prevLattices.length - 1]; + var adjacentSpacingTime = lattice.time - prevLatticeLastItem.time; + if (adjacentSpacingTime > 5000) { + // 大于5秒算一次停顿 + pauseCount++; + } + intervalTime = adjacentSpacingTime + prevLatticeLastItem.intervalTime; + } + } + lattice.intervalTime = intervalTime; + } + } + return HandwritingInfo(pauseCount, timeConsuming, lattices); + } +} + +class HandwritingInfo { + int pauseCount; // 停顿次数 + int timeConsuming; // 耗时(毫秒) + Map> strokeMap; // 笔画数据 + + HandwritingInfo(this.pauseCount, this.timeConsuming, this.strokeMap); +} + +// 图片展示 +@hwidget +Widget $theCachedNetworkImage(ImageWidgetBuilder imageBuilder, {required String imageUrl}) { + UseCachedImgRefresh _useImgRefsh = UseCachedImgRefresh.use(); + + return CachedNetworkImage( + key: _useImgRefsh.imageKey.value, + fit: BoxFit.contain, + imageUrl: imageUrl, + imageBuilder: imageBuilder, + placeholder: (context, url) => Center(child: SpinKitWaveSpinner(color: Theme.of(context).primaryColor, size: 50.r)), + errorWidget: (context, url, error) { + return GestureDetector( + onTap: () => (_useImgRefsh.imageKey.value = UniqueKey()), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset('assets/images/test_paper_loading_failed.png'), + quickText('加载失败,点击重试', color: Color.fromRGBO(148, 163, 182, 1), size: 12.sp), + ], + ), + ); + }, + ); +} + +/// 上一页 +@swidget +Widget $previousNutton(BuildContext context, int? pageNum, Function call) { + if (pageNum != null && pageNum > 1) + return Positioned( + left: 3.w, + child: FloatingActionButton( + heroTag: '点击前往上一题', + tooltip: '点击前往上一题', + focusColor: Theme.of(context).primaryColor, + backgroundColor: const Color.fromRGBO(24, 32, 32, 0.1), + elevation: 6.r, + onPressed: () => easyThrottle('answer_handwriting_previous', () => call()), + child: Icon(Icons.arrow_back_ios, color: Colors.white, size: 22.sp), + ), + ); + + return SizedBox(); +} + +/// 下一页 +@swidget +Widget $nextPageButton(BuildContext context, int? pageNum, int totalNum, Function call) { + if (pageNum != null && pageNum < totalNum) + return Positioned( + right: 3.w, + child: FloatingActionButton( + heroTag: '点击前往下一题', + tooltip: '点击前往下一题', + elevation: 6.r, + backgroundColor: const Color.fromRGBO(24, 32, 32, 0.1), + onPressed: () => easyThrottle('answer_handwriting_next', () => call()), + child: Icon(Icons.arrow_forward_ios, color: Colors.white, size: 22.sp), + ), + ); + + return SizedBox(); +} + +// 笔记还原主框 +class HandwritingDrawBox extends StatefulHookConsumerWidget { + final String image; + final HandwritingInfo handwritingData; + // final double boxWidth; + // final double boxHeight; + const HandwritingDrawBox(this.image, this.handwritingData, {super.key}); + + @override + ConsumerState createState() => _HandwritingDrawBoxState(); +} + +class _HandwritingDrawBoxState extends ConsumerState with EventBusMixin { + ImageStream? imageStream; // 图片监听数据 + TestQuestionsImageInfo? imagInfoModel; // 试题图片数据 + late ImageStreamListener theImageStreamListener; + + List> _packagedHandwritingDatas = []; + List _packagedHandwritingDataAll = []; + List pendingData = []; // 待执行数据 + List timers = []; + int handwritingTime = 0; + int handwritingDuration = 0; + @override + void initState() { + super.initState(); + + eventOn(callback: (e) { + switch (e.runtimeType) { + case JobHandwritingRunTimeBus: + var _model = (e as JobHandwritingRunTimeBus); + var _runtime = _model.runTimeVal; + handwritingDuration = _model.totalVal; + handwritingTime = _runtime; + if (_runtime <= 0) { + pendingData.clear(); + } + break; + case JobHandwritingDragProgressBarBus: + var _model = (e as JobHandwritingDragProgressBarBus); + dragProgressBarInitData(_model.changeVal, _model.totalVal); + break; + case JobHandwritingPlaybarBus: + // 播放 暂停 + var _val = e as JobHandwritingPlaybarBus; + if (_val.play) { + // 播放 + toGoPlay(); + } else { + // 暂停 + toGoPause(); + } + break; + default: + } + }); + + theImageStreamListener = ImageStreamListener((ImageInfo info, bool _) { + if (imagInfoModel != null) return; + imagInfoModel = TestQuestionsImageInfo( + // 获取图片的宽高 + boxHeight: ScreenUtil().scaleHeight, // 图片展示自适应所以 这里无法获取容器的高度 + boxWidth: ScreenUtil().screenWidth - 60.r, + url: widget.image, + height: info.image.height.toDouble(), + width: info.image.width.toDouble(), + )..calculateStartAndEndHeight(60.h); // 60.h是底部的播放栏高度 + getCalculatedSize(); + }); + } + + @override + void dispose() { + timers.forEach((e) { + if (e.isActive) e.cancel(); + }); + try { + imageStream?.removeListener(theImageStreamListener); + eventCancel(); + } catch (e) {} + super.dispose(); + } + + // 暂停播放 + Future toGoPause() async { + timers.forEach((e) { + if (e.isActive) e.cancel(); + }); + timers = []; + if (pendingData.isNotEmpty && handwritingTime > 0 && (handwritingDuration - handwritingTime > 0)) { + // 待执行的数据不等于空 每个数据都需要减去当前暂停已经执行的时间 + pendingData = pendingData.map((e) { + return GestureHandwritingRecording( + stroke: e.stroke, + data: e.data, + usageTime: e.usageTime, + intervalTime: e.intervalTime - (handwritingDuration - handwritingTime) * 1000, + ); + }).toList(); + } + } + + /// 拖动进度条后重新初始化数据 + /// @param startTime 起始时间 单位秒 + Future dragProgressBarInitData(int startTime, int totalDuration) async { + timers.forEach((e) { + if (e.isActive) e.cancel(); + }); + timers = []; + pendingData.clear(); + + if (startTime == 0) { + ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal([]); + pendingData.addAll(_packagedHandwritingDataAll); + } else { + // 待执行的数据不等于空 每个数据都需要减去当前暂停已经执行的时间 + startTime = startTime * 1000; // 转为毫秒 + List executeImmediately = []; // 立即执行数据 + List waitingExecution = []; // 等待执行数据 + + for (var i = 0; i < _packagedHandwritingDataAll.length; i++) { + var item = _packagedHandwritingDataAll[i]; + + if (item.intervalTime <= startTime) { + // 需要直接装配到直接打印的容器 + executeImmediately.add(item); + } else { + // 需要等待的数据 + waitingExecution.add(GestureHandwritingRecording( + stroke: item.stroke, + data: item.data, + usageTime: item.usageTime, + intervalTime: item.intervalTime - startTime, + )); + } + } + + pendingData = waitingExecution; + ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal(executeImmediately); + eventFire(model: JobHandwritingPlaybarBus(true)); + } + } + + Future zhixinCall(GestureHandwritingRecording e) async { + if (mounted) { + List trajectorys = ref.read(jobHandwritingDrawingTrajectoryProvider)..add(e); + ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal(List.from(trajectorys)); + pendingData.remove(e); // 执行后删除容器中的当前动作 + } + } + + /// 开始播放 + Future toGoPlay() async { + handwritingTime = 0; + var executableData = _packagedHandwritingDataAll; + if (pendingData.isNotEmpty) { + // 待执行的数据没有执行完成 就继续执行待执行数据 + executableData = pendingData; + } else { + pendingData.addAll(_packagedHandwritingDataAll); + ref.read(jobHandwritingDrawingTrajectoryProvider.notifier).setVal([]); + } + + executableData.forEach((e) { + if (e.intervalTime == 0) { + zhixinCall(e); + } else { + var ter = Timer(Duration(milliseconds: e.intervalTime), () => zhixinCall(e)); + timers.add(ter); + } + }); + } + + // 计算尺寸 + Future getCalculatedSize() async { + if (imagInfoModel == null) return; + var dataInfo = widget.handwritingData; + Map> mapData = dataInfo.strokeMap; + if (mapData.isNotEmpty) { + List keys = mapData.keys.toList(); + + for (var i = 0; i < keys.length; i++) { + List newTrajectoryData = mapData[keys[i]]!.map((e) { + double theX = e.x * imagInfoModel!.scale!; + double theY = e.y * imagInfoModel!.scale!; + + return GestureHandwritingRecording( + data: Offset(theX, theY), + usageTime: e.time.toInt(), + intervalTime: e.intervalTime, + stroke: e.stroke, + ); + }).toList(); + + _packagedHandwritingDatas.add(newTrajectoryData); // 分组数据 + _packagedHandwritingDataAll.addAll(newTrajectoryData); // 不分组数据 + } + Future.delayed(Duration.zero, () => eventFire(model: JobHandwritingGetReadyBus())); // 通知外部可以播放笔迹 + } + } + + @override + Widget build(BuildContext context) { + List points = ref.watch(jobHandwritingDrawingTrajectoryProvider); + return Container( + alignment: Alignment.center, + child: CustomPaint( + foregroundPainter: DrawingPainter(points: points), + // size: Size(ScreenUtil().screenWidth - 60.r, widget.boxHeight), + child: RepaintBoundary( + child: $TheCachedNetworkImage( + imageUrl: widget.image, + (context, imageProvider) { + Image imageWidget = Image(image: imageProvider, fit: BoxFit.contain); + if (imagInfoModel == null) { + imageStream?.removeListener(theImageStreamListener); + // 视图中展示图片的尺寸计算获取 + imageStream = imageWidget.image.resolve(ImageConfiguration()); + imageStream?.addListener(theImageStreamListener); + } + + return imageWidget; + }, + ), + ), + ), + ); + } +} + +class DrawingPainter extends CustomPainter { + List points; + + DrawingPainter({required this.points}) : super(); + + // Paint paintBrush = Paint() + // ..color = Colors.black + // ..strokeCap = StrokeCap.round + // ..strokeWidth = 0.5.sp; + + //[定义画笔] + final Paint paintBrush = Paint() + //画笔颜色 + ..color = Colors.black + //画笔笔触类型 + ..strokeCap = StrokeCap.round + //是否启动抗锯齿 + // ..isAntiAlias = true + //绘画风格,默认为填充 + // ..style = PaintingStyle.fill + //画笔的宽度 + ..strokeWidth = 0.6.r; + + @override + void paint(Canvas canvas, Size size) { + // canvas.drawPoints(PointMode.points, thePoints, paintBrush); + var _length = points.length; + for (int i = 0; i < _length; i++) { + GestureHandwritingRecording item = points[i]; + GestureHandwritingRecording? nextItem = i + 1 >= _length ? null : points[i + 1]; + Offset? offsetData = item.data; + + Offset? nextOffsetData = nextItem?.data; + if (nextOffsetData != null && item.stroke == nextItem?.stroke) { + canvas.drawLine(offsetData, nextOffsetData, paintBrush); + } + } + } + + @override + bool shouldRepaint(DrawingPainter oldDelegate) => true; +} + +@swidget +Widget $pageNumberBox(int pageNum) { + return Positioned( + top: 6.h, + right: 4.w, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), + decoration: BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.47), + borderRadius: BorderRadius.circular(5.r), + ), + child: quickText('第$pageNum页', color: Colors.white, size: 10.sp), + ), + ); +} + +@hwidget +Widget $bottomPlaybar(BuildContext context, int timeConsuming, int pauseCount) { + var usePlaybar = UseBottomPlaybar.use(timeConsuming); + + useValueChanged(timeConsuming, (_, __) { + var seds = timeConsuming ~/ 1000; + if ((timeConsuming % 1000) > 500) seds += 1; + usePlaybar.handwritingDuration.value = seds; + }); + + useValueChanged(usePlaybar.handwritingDuration.value, (_, __) { + usePlaybar.useTime.value = usePlaybar.handwritingDuration.value; + }); + // 播放速度 + useValueChanged(usePlaybar.constantFastSpeed.value, (_, __) { + usePlaybar.eventFire(model: PlaybackSpeedBus(usePlaybar.constantFastSpeed.value.speed)); + }); + // 计时结束监听 + useValueChanged(usePlaybar.useTime.value, (_, __) { + var _runtime = usePlaybar.useTime.value; + if (_runtime <= 0 || usePlaybar.handwritingDuration.value == _runtime) { + Future.delayed(Duration.zero, () => (usePlaybar.playPause.value = false)); // 初始化播放按钮 + } + usePlaybar.eventFire(model: JobHandwritingRunTimeBus(_runtime, usePlaybar.handwritingDuration.value)); + }); + + useEffect(() { + usePlaybar.eventOn(callback: (e) { + // TODO 数据 + switch (e.runtimeType) { + case JobHandwritingPlaybarBus: + // 出发播放暂停 + var _val = e as JobHandwritingPlaybarBus; + if (_val.play) { + // 开始播放 + usePlaybar.playTimingStarts(); + if (!usePlaybar.playPause.value) { + usePlaybar.playPause.value = true; + } + } else { + // 暂停播放 + usePlaybar.playTimingSuspend(); + if (usePlaybar.playPause.value) { + usePlaybar.playPause.value = false; + } + } + break; + case JobHandwritingGetReadyBus: + // 作业笔迹已经计算好坐标 可以开始播放 + Future.delayed(Duration.zero, () => (usePlaybar.handWritingReady.value = true)); + break; + case PlaybackSpeedBus: + break; + default: + } + }); + + return () { + try { + usePlaybar.eventCancel(); + usePlaybar.timer.value?.cancel(); + } catch (e) { + print(e); + } + }; + }, []); + + return Container( + height: 60.h, + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h), + alignment: Alignment.center, + color: Color.fromRGBO(0, 0, 0, 0.5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (usePlaybar.handWritingReady.value) + InkWell( + onTap: () => easyThrottle('job_handwriting_play_pause', () { + usePlaybar.playPause.value = !usePlaybar.playPause.value; + usePlaybar.eventFire(model: JobHandwritingPlaybarBus(usePlaybar.playPause.value)); + }), + child: Icon( + !usePlaybar.playPause.value ? Icons.play_circle_outline : Icons.pause_circle_outline, + color: Colors.white, + size: 28.r, + ), + ) + else + SpinKitPouringHourGlassRefined(size: 40.sp, color: Colors.white), + SizedBox(width: 6.w), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 20.h, + // color: Theme.of(context).primaryColor, + child: Slider( + /// 进度条的值 + value: (usePlaybar.handwritingDuration.value - usePlaybar.useTime.value).toDouble(), + + /// 进度条的最小值(默认为 0) + min: 0.0, + + /// 进度条的最大值(默认为 1) + max: usePlaybar.handwritingDuration.value.toDouble(), + // divisions:1, + + /// 滑块以及滑块左侧的颜色 + // activeColor: Colors.red, + + /// 滑块右侧的颜色 + inactiveColor: Color.fromRGBO(217, 217, 217, 1), + + /// 进度值发生变化时触发的事件(注:把 onChanged 设置为 null 则说明 Slider 为不可用状态) + onChangeEnd: (value) { + usePlaybar.playTimingSuspend(); // 暂停计时器得暂停 + usePlaybar.eventFire(model: JobHandwritingDragProgressBarBus(value.toInt(), usePlaybar.handwritingDuration.value)); + usePlaybar.useTime.value = usePlaybar.handwritingDuration.value - value.toInt(); + }, + onChanged: (double value) { + usePlaybar.useTime.value = usePlaybar.handwritingDuration.value - value.toInt(); + }, + ), + + // /// 手指按下滑块时触发的事件 + // onChangeStart: (value) => log('onChangeStart: $value'), + + // /// 手指从滑块抬起时触发的事件 + // onChangeEnd: (value) => log('onChangeEnd: $value'), + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + quickText('累计停顿:$pauseCount次', color: Colors.white, size: 7.sp), + quickText(convertSeconds(usePlaybar.useTime.value)?.toString() ?? '', color: Colors.white, size: 7.sp), + ], + ) + ], + ), + ), + SizedBox(width: 16.w), + InkWell( + onTap: () => easyThrottle('job_handwriting_speed', () { + var theIndex = PlaybackSpeed.values.indexOf(usePlaybar.constantFastSpeed.value); + if (theIndex == PlaybackSpeed.values.length - 1) { + theIndex = -1; + } + usePlaybar.constantFastSpeed.value = PlaybackSpeed.values[theIndex + 1]; + }), + child: Container( + // alignment: Alignment., + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.5.h), + decoration: BoxDecoration(color: Color.fromRGBO(182, 197, 250, 1), borderRadius: BorderRadius.circular(4.r)), + child: quickText( + '${usePlaybar.constantFastSpeed.value.name}', + color: Color.fromRGBO(79, 114, 244, 1), + size: 10.sp, + align: TextAlign.center, + ), + ), + ), + ], + ), + ); +} + +class UseBottomPlaybar with EventBusMixin { + final ValueNotifier handwritingDuration; // 笔迹总时长 + final ValueNotifier playPause; // 播放暂停 + final ValueNotifier constantFastSpeed; // 原速、快速 默认原速 + final ValueNotifier handWritingReady; + + final ValueNotifier useTime; // 耗时 单位:(秒) + + final ValueNotifier timer; + + UseBottomPlaybar._( + {required this.handWritingReady, + required this.handwritingDuration, + required this.playPause, + required this.constantFastSpeed, + required this.useTime, + required this.timer}); + + // 工厂构造函数 + factory UseBottomPlaybar.use(int milliseconds) { + int handwritingDuration = milliseconds ~/ 1000; + if ((milliseconds % 1000) > 500) handwritingDuration += 1; + return UseBottomPlaybar._( + playPause: useState(false), + constantFastSpeed: useState(PlaybackSpeed.ORIGINAL_SPEED), + useTime: useState(handwritingDuration), + timer: useState(null), + handwritingDuration: useState(handwritingDuration), + handWritingReady: useState(false), + ); + } + + /// 开始计时 + void playTimingStarts() { + if (useTime.value > 0) { + timer.value?.cancel(); + timer.value = Timer.periodic(Duration(seconds: 1), (theTime) { + useTime.value -= 1; + if (useTime.value < 0) { + timer.value?.cancel(); + timer.value = null; + useTime.value = handwritingDuration.value; + } + }); + } + } + + /// 暂停 + void playTimingSuspend() { + timer.value?.cancel(); + timer.value = null; + } +} + +// 播放按钮 +class JobHandwritingPlaybarBus { + bool play; + JobHandwritingPlaybarBus(this.play); +} + +// 笔迹是否已经准备好(笔迹计算好坐标后通知通知栏可以开始播放) +class JobHandwritingGetReadyBus { + JobHandwritingGetReadyBus(); +} + +// 笔记运行时间 +class JobHandwritingRunTimeBus { + int runTimeVal; + int totalVal; + JobHandwritingRunTimeBus(this.runTimeVal, this.totalVal); +} + +// 拖动进度条 +class JobHandwritingDragProgressBarBus { + int changeVal; + int totalVal; + JobHandwritingDragProgressBarBus(this.changeVal, this.totalVal); +} + +// 播放速度 (原速播放/快速播放) +class PlaybackSpeedBus { + double speed; + PlaybackSpeedBus(this.speed); +} + +// 播放倍速 +enum PlaybackSpeed { + ORIGINAL_SPEED(name: '原速播放', speed: 1), + ONE_POINT_FIVE_SPEED(name: '1.5x播放', speed: 1.5), + DOUBLE_SPEED(name: '2.0x播放', speed: 2), + TRIPLE_SPEED(name: '3.0x播放', speed: 3); + + const PlaybackSpeed({required this.name, required this.speed}); + final double speed; + final String name; } diff --git a/marking_app/lib/utils/my_time_util.dart b/marking_app/lib/utils/my_time_util.dart new file mode 100644 index 0000000..237cfbd --- /dev/null +++ b/marking_app/lib/utils/my_time_util.dart @@ -0,0 +1,65 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'my_time_util.g.dart'; + +// 毫秒转小时、分钟、秒的函数 +TimeUnitModel? convertMilliseconds(int milliseconds) { + try { + int hours = milliseconds ~/ (1000 * 60 * 60); + int minutes = (milliseconds % (1000 * 60 * 60)) ~/ (1000 * 60); + int seconds = (milliseconds % (1000 * 60)) ~/ 1000; + + if ((milliseconds % 1000) > 500) seconds += 1; + + return TimeUnitModel(hours, minutes, seconds); + } catch (e) { + print('时间转换报错'); + } + return null; +} + +// 毫秒转小时、分钟、秒的函数 +TimeUnitModel? convertSeconds(int totalSeconds) { + try { + int hours = totalSeconds ~/ 3600; // 整除3600得到小时数 + int remainingSeconds = totalSeconds % 3600; // 取模3600得到剩余的秒数 + int minutes = remainingSeconds ~/ 60; // 整除60得到分钟数 + int seconds = remainingSeconds % 60; // 取模60得到最终的秒数 + + return TimeUnitModel(hours, minutes, seconds); + } catch (e) { + print('时间转换报错'); + } + return null; +} + +@JsonSerializable() +class TimeUnitModel extends Object { + int hours; + int minutes; + int seconds; + TimeUnitModel(this.hours, this.minutes, this.seconds); + + factory TimeUnitModel.fromJson(Map srcJson) => _$TimeUnitModelFromJson(srcJson); + + Map toJson() => _$TimeUnitModelToJson(this); + + @override + String toString() { + var timeStr = ''; + if (hours > 0) { + timeStr += '${hours > 9 ? hours : '0' + hours.toString()} '; + } + if (minutes > 0) { + timeStr += '${minutes > 9 ? minutes : '0' + minutes.toString()}'; + } + + if (timeStr.length > 0) { + timeStr += ':${seconds > 9 ? seconds : '0' + seconds.toString()}'; + } else { + timeStr += '00:${seconds > 9 ? seconds : '0' + seconds.toString()}'; + } + + return timeStr; + } +}