857 lines
33 KiB
Dart
857 lines
33 KiB
Dart
/*
|
||
* @Author: wangyang 1147192855@qq.com
|
||
* @Date: 2022-07-23 11:55:16
|
||
* @LastEditors: wangyang 1147192855@qq.com
|
||
* @LastEditTime: 2022-09-28 17:01:15
|
||
* @FilePath: \marking_app\lib\components\PictureOverview.dart
|
||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||
*/
|
||
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:crypto/crypto.dart' as crypto;
|
||
|
||
import 'package:cached_network_image/cached_network_image.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/rendering.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/common/base_structure_result.dart';
|
||
import 'package:marking_app/common/model/common/upload_img_secret_key.dart';
|
||
import 'package:marking_app/common/model/enum/review_marks_bottom_btns_enum.dart';
|
||
import 'package:marking_app/common/model/event_bus/bottom_annotation_switch_cleanall.dart';
|
||
import 'package:marking_app/common/model/job/test_questions_image_info.dart';
|
||
import 'package:marking_app/common/model/job/upload_file_interface_config.dart';
|
||
import 'package:marking_app/common/model/job/upload_file_interface_config_params.dart';
|
||
import 'package:marking_app/common/model/marking/annotation_graffiti_switch.dart';
|
||
import 'package:marking_app/common/model/marking/do_marking_keyboard_model.dart';
|
||
import 'package:marking_app/common/model/marking/marking_history_zoom_info.dart';
|
||
import 'package:marking_app/common/model/marking/marking_text_question.dart';
|
||
import 'package:marking_app/common/model/marking/switch_keyboard_to_reload_images.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/pages/marking/hooks/use_zoom_image_history.dart';
|
||
import 'package:marking_app/pages/marking/provider/do_paper_bottom_review_marks_provider.dart';
|
||
import 'package:marking_app/pages/marking/provider/zoom_height_provider.dart';
|
||
import 'package:marking_app/pages/marking/provider/zoom_history_provider.dart';
|
||
import 'package:marking_app/provider/annotation_graffiti_switch_provider.dart';
|
||
import 'package:marking_app/provider/do_marking_provider.dart';
|
||
import 'package:marking_app/provider/upload_file_provider.dart';
|
||
import 'package:marking_app/utils/index.dart';
|
||
import 'package:marking_app/utils/my_text.dart';
|
||
import 'package:marking_app/utils/request/rest_client.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:uuid/uuid.dart';
|
||
import 'package:zoom_widget/zoom_widget.dart';
|
||
|
||
import '../pages/marking/provider/draw_marking_provider.dart';
|
||
part 'PictureOverview.g.dart';
|
||
|
||
typedef PageChanged = void Function(int index);
|
||
|
||
//这里就是关键的代码,定义一个key
|
||
final GlobalKey<PictureOverviewState> pictureOverviewKey = GlobalKey<PictureOverviewState>();
|
||
|
||
class PictureOverview extends StatefulHookConsumerWidget {
|
||
final double imageScale;
|
||
final Offset? imagePosition;
|
||
|
||
final String questionNum;
|
||
final int markingUserId;
|
||
|
||
final bool homework;
|
||
final List imageItems; //图片列表
|
||
final bool annotationsFlag;
|
||
final String testQuestionNumber;
|
||
final Map<String, String> commentImageMap;
|
||
final MarkingTextQuestion data;
|
||
final Function callAnnotationTips;
|
||
|
||
const PictureOverview({
|
||
required this.imageItems,
|
||
required this.annotationsFlag,
|
||
required this.commentImageMap,
|
||
required this.testQuestionNumber,
|
||
required this.questionNum,
|
||
required this.markingUserId,
|
||
required this.data,
|
||
required this.callAnnotationTips,
|
||
this.homework = false,
|
||
this.imageScale = 1,
|
||
this.imagePosition,
|
||
Key? key,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
PictureOverviewState createState() => PictureOverviewState();
|
||
}
|
||
|
||
class PictureOverviewState extends ConsumerState<PictureOverview> with CommonMixin, EventBusMixin {
|
||
final GlobalKey theglobalKey = GlobalKey();
|
||
final GlobalKey<_ExamPaperDrawingState> examPaperDrawingKey = GlobalKey<_ExamPaperDrawingState>();
|
||
final GlobalKey zoomGlobalKey = GlobalKey(); // zoom
|
||
double? initScale;
|
||
Offset? zoomOffset;
|
||
|
||
ImageStreamListener? _imageStreamListener;
|
||
TestQuestionsImageInfo? imagInfoModel; // 试题图片数据
|
||
ImageStream? _imageStream;
|
||
|
||
final int currentIndex = 0;
|
||
late AnnotationGraffitiSwitch graffitiSwitch;
|
||
late RemoveListener _annotationsListener; // 批注关闭监听
|
||
File? temFile; // 批注临时数据
|
||
|
||
// 用于记录绘图结果的变量
|
||
Offset? globalPosition; // 是否正在绘制
|
||
MarkingHistoryZoomInfo? zoomInfo;
|
||
bool illegalArea = false; // 非法区域(批阅笔迹不在试题图片中)
|
||
final GlobalKey _zoomKey = GlobalKey<State<Zoom>>();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
FastData.getInstance().getMarkingZoomInfo().then((value) {
|
||
if (value == null) return;
|
||
bool flag = value.questionNum == widget.questionNum && value.markingUserId == widget.markingUserId;
|
||
if (flag) {
|
||
if (value.positionX != 0 && value.positionY != 0) {
|
||
// zoomOffset = Offset(value.positionX, value.positionY);
|
||
}
|
||
if (value.scale < 1) {
|
||
initScale = value.scale;
|
||
// 5
|
||
Future.delayed(Duration(seconds: 1), () => ref.read(zoomHistoryProvider.notifier).setState(initScale!));
|
||
}
|
||
}
|
||
});
|
||
Future.delayed(Duration.zero, () {
|
||
ref.read(zoomHeightProvider.notifier).setState(0.0);
|
||
});
|
||
_annotationsListener = ref.read(annotationGraffitiSwitchProvider.notifier).addListener((state) {
|
||
graffitiSwitch = state;
|
||
toUpState(setState, () {}, mounted);
|
||
});
|
||
|
||
// 事件总线监听 清空数据
|
||
eventOn(callback: (BottomAnnotationSwitchCleanallOfMarking item) async {
|
||
widget.callAnnotationTips(); // 调用回调 通知父级批注记录被更改
|
||
if (ref.read(drawMarkingProvider).data.isEmpty) {
|
||
if (widget.data.commentImageUrl.isNotEmpty) {
|
||
bool? res = await showDialog<bool>(
|
||
// 表示点击灰色背景的时候是否消失弹出框
|
||
barrierDismissible: false,
|
||
context: context,
|
||
builder: (context1) {
|
||
return AlertDialog(content: quickText("是否撤销上次批阅批注痕迹"), actions: <Widget>[
|
||
TextButton(child: quickText("取消"), onPressed: () => Navigator.pop(context1, false)),
|
||
TextButton(child: quickText("确定", color: Theme.of(context).primaryColor), onPressed: () => Navigator.pop(context1, true))
|
||
]);
|
||
},
|
||
);
|
||
if (res == true) {
|
||
widget.data.commentImageUrl.removeAt(currentIndex);
|
||
var theUrl = widget.imageItems[currentIndex];
|
||
widget.commentImageMap[theUrl] = theUrl;
|
||
toUpState(setState, () {}, mounted);
|
||
}
|
||
} else {
|
||
ToastUtils.showInfo('批注已清空');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_annotationsListener();
|
||
eventCancel();
|
||
try {
|
||
_imageStream?.removeListener(_imageStreamListener!);
|
||
if (temFile != null) {
|
||
bool flieExist = temFile!.existsSync();
|
||
if (flieExist) temFile!.delete();
|
||
}
|
||
// if (zoomOffset != null) saveZoomPosition();
|
||
} catch (e) {}
|
||
super.dispose();
|
||
}
|
||
|
||
Future<FileResult?> saveImage() async {
|
||
try {
|
||
ToastUtils.showLoading();
|
||
if (temFile == null) {
|
||
if (examPaperDrawingKey.currentState?.pointsPureData.isEmpty ?? true) return null;
|
||
// 没有图片就上传图片
|
||
RenderRepaintBoundary? boundary = theglobalKey.currentContext!.findRenderObject() as RenderRepaintBoundary?;
|
||
if (boundary == null) return null;
|
||
// double dpr = MediaQuery.of(context).devicePixelRatio;
|
||
|
||
double pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||
if (imagInfoModel?.pixelRatio != null) pixelRatio = imagInfoModel!.pixelRatio;
|
||
|
||
ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
|
||
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||
if (byteData == null) {
|
||
/// 等于null 已经是异常了
|
||
return null;
|
||
}
|
||
Directory appDocDir = await getApplicationDocumentsDirectory();
|
||
List<int> bytes = byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes);
|
||
String filePath = '${appDocDir.path}/${Uuid().v1()}.png'; // 文件路径及名称
|
||
File file = File(filePath); // 创建文件对象
|
||
await file.writeAsBytes(bytes); // 将ByteData写入文件
|
||
temFile = file;
|
||
}
|
||
|
||
if (imagInfoModel != null) {
|
||
// 剪切图片
|
||
}
|
||
|
||
crypto.Digest fileMd5 = crypto.md5.convert(await temFile!.readAsBytes());
|
||
RestClient _client = await getClient();
|
||
BaseStructureResult<UploadFileInterfaceConfig> resUploadConfig = await _client.getUploadFile(UploadFileInterfaceConfigParams(
|
||
fileName: '1.png',
|
||
fileMd5: fileMd5.toString(),
|
||
contentLength: temFile!.lengthSync(),
|
||
));
|
||
if (!resUploadConfig.success || resUploadConfig.data == null) {
|
||
ToastUtils.getFluttertoast(msg: '获取图片上传配置失败', context: context);
|
||
|
||
return null;
|
||
}
|
||
if (resUploadConfig.data!.uploadUri == null) {
|
||
return FileResult(myObject: '', success: true, url: resUploadConfig.data!.downloadUri);
|
||
}
|
||
FileResult? resFile = await ref.read(uploadFileProvider.notifier).getUploadFileConfig(resUploadConfig.data!, temFile!);
|
||
// FileResult? resFile = await ref
|
||
// .read(uploadFileProvider.notifier)
|
||
// .uploadFile(temFile!.path, widget.imageItems[currentIndex], ref.read(userProvider).id.toString());
|
||
if (resFile != null && resFile.success) {
|
||
resFile.otherParam = currentIndex;
|
||
return resFile;
|
||
}
|
||
} catch (e) {
|
||
toPrint(val: e.toString());
|
||
} finally {
|
||
// ToastUtils.dismiss();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 缩放组件 ==> 位置更新
|
||
void onPositionUpdate(Offset position) async {
|
||
MarkingHistoryZoomInfo? historyZoomInfo = await FastData.getInstance().getMarkingZoomInfo();
|
||
double? scale = historyZoomInfo?.scale ?? 1;
|
||
MarkingHistoryZoomInfo info = MarkingHistoryZoomInfo(
|
||
scale: scale,
|
||
positionX: position.dx,
|
||
positionY: position.dy,
|
||
questionNum: widget.questionNum,
|
||
markingUserId: widget.markingUserId,
|
||
);
|
||
FastData.getInstance().setMarkingZoomInfo(info);
|
||
}
|
||
|
||
void saveZoomPosition() async {
|
||
MarkingHistoryZoomInfo? historyZoomInfo = await FastData.getInstance().getMarkingZoomInfo();
|
||
if (historyZoomInfo != null) {
|
||
FastData.getInstance().setMarkingZoomInfo(MarkingHistoryZoomInfo(
|
||
scale: historyZoomInfo.scale,
|
||
positionX: zoomOffset!.dx,
|
||
positionY: zoomOffset!.dy,
|
||
questionNum: historyZoomInfo.questionNum,
|
||
markingUserId: historyZoomInfo.markingUserId,
|
||
));
|
||
}
|
||
}
|
||
|
||
void onPanUpPosition(Offset val) async {
|
||
// 手指在移动 非物体移动的位置
|
||
// print('**************** 正在移动位置 YYY:${val.dy}');
|
||
// print('**************** 正在移动位置 XXX:${val.dx}');
|
||
zoomOffset = val;
|
||
}
|
||
|
||
// 缩放组件 ==> 缩放监听
|
||
void onScaleUpdate(double scale, double zoom) async {
|
||
// print('zoom:$zoom');
|
||
// print('scale:${scale}');
|
||
// MarkingHistoryZoomInfo? historyZoomInfo = await FastData.getInstance().getMarkingZoomInfo();
|
||
MarkingHistoryZoomInfo? historyZoomInfo = await FastData.getInstance().getMarkingZoomInfo();
|
||
double positionX = historyZoomInfo?.positionX ?? 0;
|
||
double positionY = historyZoomInfo?.positionY ?? 0;
|
||
MarkingHistoryZoomInfo info = MarkingHistoryZoomInfo(
|
||
scale: zoom,
|
||
positionX: positionX,
|
||
positionY: positionY,
|
||
questionNum: widget.questionNum,
|
||
markingUserId: widget.markingUserId,
|
||
);
|
||
zoomInfo = info;
|
||
// if (double.parse(zoom.toStringAsFixed(2)) <= 1) zoom = 1;
|
||
if (imagInfoModel != null) {
|
||
// 根据缩放比例重置被放大的图片的尺寸
|
||
imagInfoModel = TestQuestionsImageInfo(
|
||
// 获取图片的宽高
|
||
boxHeight: imagInfoModel!.boxHeight,
|
||
boxWidth: imagInfoModel!.boxWidth,
|
||
url: imagInfoModel!.url,
|
||
height: imagInfoModel!.height,
|
||
width: imagInfoModel!.width,
|
||
zoom: zoom,
|
||
);
|
||
}
|
||
FastData.getInstance().setMarkingZoomInfo(info);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
height: double.infinity,
|
||
alignment: Alignment.center,
|
||
child: LayoutBuilder(
|
||
builder: (BuildContext context, BoxConstraints constraints) {
|
||
double containerWidth = constraints.maxWidth;
|
||
double containerHeight = constraints.maxHeight;
|
||
|
||
return $LocalAndNetworkSwitch(
|
||
zoomGlobalKey: zoomGlobalKey,
|
||
containerWidth: containerWidth,
|
||
containerHeight: containerHeight,
|
||
imagePosition: widget.imagePosition ?? Offset(0, 0),
|
||
imageScale: widget.imageScale,
|
||
homework: widget.homework,
|
||
theglobalKey: theglobalKey,
|
||
graffitiSwitch: graffitiSwitch,
|
||
drawFlag: widget.annotationsFlag,
|
||
examGlobalKey: examPaperDrawingKey,
|
||
testQuestionNumber: widget.testQuestionNumber,
|
||
imageUrl: widget.commentImageMap[widget.imageItems[currentIndex]]!,
|
||
updateTempFileCall: (File? file) {
|
||
temFile = file;
|
||
print('更新需要上传的文件');
|
||
},
|
||
imageBuilder: (imageBuilderContext, imageProvider) {
|
||
Image imageWidget = Image(image: imageProvider, fit: BoxFit.fitWidth);
|
||
if (imagInfoModel == null || (imagInfoModel?.boxHeight != containerHeight || imagInfoModel?.boxWidth != containerWidth)) {
|
||
if (_imageStreamListener != null) _imageStream?.removeListener(_imageStreamListener!);
|
||
_imageStreamListener = ImageStreamListener((ImageInfo info, bool _) {
|
||
imagInfoModel = TestQuestionsImageInfo(
|
||
// 获取图片的宽高
|
||
boxHeight: containerHeight,
|
||
boxWidth: containerWidth,
|
||
url: widget.commentImageMap[widget.imageItems[currentIndex]]!,
|
||
height: info.image.height.toDouble(),
|
||
width: info.image.width.toDouble(),
|
||
);
|
||
Future.delayed(Duration.zero, () {
|
||
ref.read(zoomHeightProvider.notifier).setState(imagInfoModel?.scaleHeight ?? 0.0);
|
||
});
|
||
});
|
||
_imageStream = imageWidget.image.resolve(ImageConfiguration())..addListener(_imageStreamListener!);
|
||
}
|
||
|
||
var btnEnum = ref.watch(doPaperBottomReviewMarksProvider);
|
||
// return imageWidget;
|
||
return Listener(
|
||
behavior: HitTestBehavior.opaque,
|
||
onPointerMove: (PointerMoveEvent details) {
|
||
if (btnEnum != ReviewMarksBottomBtnsEnum.HANDWRITING) return;
|
||
if (globalPosition != null) {
|
||
// 预防双指同时作用于屏幕
|
||
double dx = globalPosition!.dx;
|
||
double dy = globalPosition!.dy;
|
||
|
||
double dxNew = details.localPosition.dx;
|
||
double dyNew = details.localPosition.dy;
|
||
if ((dxNew - dx).abs() > 22 || (dyNew - dy).abs() > 22) return;
|
||
}
|
||
globalPosition = details.localPosition;
|
||
Offset localPosition = globalPosition!;
|
||
|
||
// if (imagInfoModel != null &&
|
||
// (localPosition.dy < imagInfoModel!.imageHeightOffsetStart! || localPosition.dy > imagInfoModel!.imageHeightOffsetend!)) {
|
||
// // 笔迹画出图片区域 直接断笔
|
||
// var dataVal = ref.read(drawMarkingProvider).data;
|
||
// if (dataVal.length - 1 > -1 && dataVal[dataVal.length - 1].data != null) {
|
||
// var newVal = ref.read(drawMarkingProvider).data..add(GestureRecording(eraser: graffitiSwitch.openEraser));
|
||
// var newVal1 = ref.read(drawMarkingProvider).offsets..add(null);
|
||
// ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal(newVal, newVal1));
|
||
// }
|
||
// illegalArea = true;
|
||
// return;
|
||
// }
|
||
// illegalArea = false;
|
||
|
||
var _theKey = _zoomKey.currentState;
|
||
print(_theKey);
|
||
|
||
double remainingHeight = imagInfoModel!.imageHeightOffsetStart!; // 剩余高度
|
||
if (remainingHeight > 1) {
|
||
localPosition = Offset(localPosition.dx, localPosition.dy - remainingHeight);
|
||
}
|
||
|
||
// print(localPosition.dy);
|
||
print(localPosition.dx);
|
||
|
||
double _theZoomVal = imagInfoModel?.zoom ?? 1;
|
||
var _dx = zoomOffset?.dx ?? 0;
|
||
_dx = _dx > 0 ? 0 : _dx.abs() / _theZoomVal;
|
||
var _dy = zoomOffset?.dy ?? 0;
|
||
_dy = _dy > 0 ? 0 : _dy.abs() / _theZoomVal;
|
||
|
||
if (_theZoomVal > 1) {
|
||
// 计算视图被放大比例 还原笔迹坐标
|
||
localPosition = Offset(localPosition.dx / _theZoomVal, localPosition.dy / _theZoomVal);
|
||
if (zoomOffset != null) {
|
||
// 如果滚动条有触动就加上滚动条滚动的位置
|
||
localPosition = Offset(localPosition.dx + _dx, localPosition.dy + _dy);
|
||
}
|
||
} else if (_theZoomVal < 1) {
|
||
// 试图被缩小
|
||
double imgSpaceWidthOfSingle = (imagInfoModel!.boxWidth - imagInfoModel!.scaleWidth!) / 2;
|
||
localPosition = Offset((localPosition.dx - imgSpaceWidthOfSingle) / _theZoomVal, localPosition.dy / _theZoomVal + _dy);
|
||
// localPosition = Offset(localPosition.dx * _theZoomVal - imgSpaceWidthOfSingle, localPosition.dy / _theZoomVal);
|
||
} else {
|
||
localPosition = Offset(localPosition.dx, localPosition.dy + _dy);
|
||
}
|
||
|
||
var newVal = ref.read(drawMarkingProvider).data..add(GestureRecording(eraser: graffitiSwitch.openEraser, data: localPosition));
|
||
var newVal1 = ref.read(drawMarkingProvider).offsets..add(localPosition);
|
||
ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal(newVal, newVal1));
|
||
widget.callAnnotationTips();
|
||
},
|
||
// onPointerDown: (PointerDownEvent event) {
|
||
// },
|
||
onPointerUp: (PointerUpEvent details) {
|
||
if (btnEnum != ReviewMarksBottomBtnsEnum.HANDWRITING) return;
|
||
// 如果在空白区域 非试题图片区域就返回
|
||
if (illegalArea) return;
|
||
|
||
globalPosition = null;
|
||
|
||
var newVal = ref.read(drawMarkingProvider).data..add(GestureRecording(eraser: graffitiSwitch.openEraser));
|
||
var newVal1 = ref.read(drawMarkingProvider).offsets..add(null);
|
||
ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal(newVal, newVal1));
|
||
},
|
||
child: IgnorePointer(
|
||
ignoring: btnEnum != ReviewMarksBottomBtnsEnum.DRAG,
|
||
child: Zoom(
|
||
key: _zoomKey,
|
||
// initTotalZoomOut: true,
|
||
child: ExamPaperDrawing(
|
||
key: examPaperDrawingKey,
|
||
globalKey: theglobalKey,
|
||
child: imageWidget,
|
||
graffitiSwitch: graffitiSwitch,
|
||
decoration: const BoxDecoration(color: const Color.fromRGBO(249, 250, 254, 1)),
|
||
),
|
||
maxZoomWidth: containerWidth,
|
||
canvasColor: Colors.transparent,
|
||
backgroundColor: Colors.transparent,
|
||
maxZoomHeight: imagInfoModel?.scaleHeight != null ? (imagInfoModel!.scaleHeight! / imagInfoModel!.zoom) : null,
|
||
initScale: initScale ?? 1,
|
||
initPosition: zoomOffset,
|
||
onScaleUpdate: onScaleUpdate,
|
||
onPositionUpdate: onPanUpPosition,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
));
|
||
}
|
||
}
|
||
|
||
// 试卷绘制
|
||
class ExamPaperDrawing extends StatefulHookConsumerWidget {
|
||
// String imgUrl;
|
||
Widget child;
|
||
BoxDecoration? decoration;
|
||
AnnotationGraffitiSwitch graffitiSwitch;
|
||
GlobalKey globalKey;
|
||
// Function(String) imageCall;
|
||
ExamPaperDrawing({
|
||
// required this.imgUrl,
|
||
required this.child,
|
||
required this.graffitiSwitch,
|
||
required this.globalKey,
|
||
this.decoration,
|
||
Key? key,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
_ExamPaperDrawingState createState() => _ExamPaperDrawingState();
|
||
}
|
||
|
||
class _ExamPaperDrawingState extends ConsumerState<ExamPaperDrawing> with EventBusMixin<BottomAnnotationSwitchCleanallOfMarking> {
|
||
// 用于记录手指位置的变量
|
||
late RemoveListener removeListener;
|
||
late ValueNotifier<List<GestureRecording>> _vnHandWritings;
|
||
late List<Offset?> pointsPureData = [];
|
||
|
||
@override
|
||
void initState() {
|
||
_vnHandWritings = ValueNotifier<List<GestureRecording>>([]);
|
||
removeListener = ref.read(drawMarkingProvider.notifier).addListener((state) {
|
||
pointsPureData = state.offsets;
|
||
_vnHandWritings.value = [...state.data];
|
||
}, fireImmediately: false);
|
||
|
||
// 事件总线监听
|
||
eventOn(callback: (BottomAnnotationSwitchCleanallOfMarking item) {
|
||
if (item.previousStep) {
|
||
if (ref.read(drawMarkingProvider).data.isEmpty) {
|
||
return;
|
||
}
|
||
var index = pointsPureData.toList().lastIndexOf(null);
|
||
if (index != -1) {
|
||
if (index + 1 == pointsPureData.length) {
|
||
pointsPureData = pointsPureData.sublist(0, index);
|
||
|
||
ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal(ref.read(drawMarkingProvider).data.sublist(0, index), pointsPureData));
|
||
index = pointsPureData.toList().lastIndexOf(null);
|
||
index == -1 ? -1 : index + 1;
|
||
}
|
||
if (index != -1) {
|
||
pointsPureData = pointsPureData.sublist(0, index);
|
||
ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal(ref.read(drawMarkingProvider).data.sublist(0, index), pointsPureData));
|
||
} else {
|
||
item.cleanAll = true;
|
||
}
|
||
} else {
|
||
item.cleanAll = true;
|
||
}
|
||
}
|
||
|
||
if (item.cleanAll) {
|
||
pointsPureData.clear();
|
||
ref.read(drawMarkingProvider.notifier).setState(DrawMarkingVal([], []));
|
||
}
|
||
});
|
||
super.initState();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
eventCancel();
|
||
removeListener();
|
||
_vnHandWritings.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
print('_ExamPaperDrawingState的build....');
|
||
|
||
return RepaintBoundary(
|
||
key: widget.globalKey,
|
||
child: CustomPaint(
|
||
isComplex: true,
|
||
willChange: true,
|
||
foregroundPainter: DrawingPainter(ctrl: _vnHandWritings),
|
||
child: widget.child,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class DrawingPainter extends CustomPainter {
|
||
final ValueNotifier<List<GestureRecording>> ctrl;
|
||
final Paint paintBrush = Paint();
|
||
DrawingPainter({required this.ctrl}) : super(repaint: ctrl) {
|
||
paintBrush
|
||
..color = Colors.red
|
||
..strokeCap = StrokeCap.round
|
||
..strokeWidth = 1.5.r;
|
||
}
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
var points = ctrl.value;
|
||
var pointsLength = points.length;
|
||
print('数据.....................[[[[[${points.length}]]]]]');
|
||
for (int i = 0; i < pointsLength; i++) {
|
||
GestureRecording item = points[i];
|
||
Offset? offsetData = item.data;
|
||
Offset? nextOffsetData = pointsLength - 1 == i ? null : points[i + 1].data;
|
||
if (offsetData != null && nextOffsetData != null) {
|
||
canvas.drawLine(offsetData, nextOffsetData, paintBrush);
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||
print('FFFFFF55555555555555');
|
||
// if (oldDelegate is DrawingPainter) {
|
||
// var repaint = ctrl.value.length != oldDelegate.ctrl.value.length || oldDelegate.ctrl.value != ctrl.value;
|
||
// print('调用是否绘制:$repaint');
|
||
|
||
// return repaint;
|
||
// }
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 手势记录
|
||
*/
|
||
class GestureRecording {
|
||
bool eraser; // 是否是橡皮擦
|
||
|
||
Offset? data; // 位置记录 可能为null
|
||
|
||
GestureRecording({required this.eraser, this.data});
|
||
}
|
||
|
||
@hwidget
|
||
Widget $myCachedNetworkImage({
|
||
required String imageUrl,
|
||
required File? tempFile,
|
||
required double width,
|
||
required double height,
|
||
required ImageWidgetBuilder? imageBuilder,
|
||
}) {
|
||
UseCachedImgRefresh _useImgRefsh = UseCachedImgRefresh.use();
|
||
|
||
if (tempFile != null) {
|
||
// 注释临时本地图片
|
||
return Image.file(tempFile, fit: BoxFit.contain, width: double.infinity, height: double.infinity);
|
||
}
|
||
|
||
useEffect(() {
|
||
_useImgRefsh.eventOnSub<SwitchKeyboardToReloadImages>(callback: (SwitchKeyboardToReloadImages event) {
|
||
if (event.reload) {
|
||
_useImgRefsh.imageKey.value = UniqueKey();
|
||
}
|
||
});
|
||
|
||
return () {
|
||
_useImgRefsh.eventCancelSub();
|
||
};
|
||
}, []);
|
||
|
||
return CacheNetImageView(
|
||
cacheNetImageKey: _useImgRefsh.imageKey.value,
|
||
imageUrl: imageUrl,
|
||
imageBuilder: imageBuilder,
|
||
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),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
class CacheNetImageView extends ConsumerWidget {
|
||
final Key cacheNetImageKey;
|
||
final String imageUrl;
|
||
final ImageWidgetBuilder? imageBuilder;
|
||
final LoadingErrorWidgetBuilder? errorWidget;
|
||
const CacheNetImageView({
|
||
required this.cacheNetImageKey,
|
||
required this.imageUrl,
|
||
required this.imageBuilder,
|
||
required this.errorWidget,
|
||
super.key,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
double zoomHeight = ref.watch(zoomHeightProvider);
|
||
double initScale = ref.watch(zoomHistoryProvider);
|
||
|
||
print('zoomView的视图最大高度:$zoomHeight 和 缩放比例$initScale');
|
||
return CachedNetworkImage(
|
||
key: cacheNetImageKey,
|
||
imageUrl: imageUrl,
|
||
fit: BoxFit.fitWidth,
|
||
width: double.infinity,
|
||
alignment: Alignment.center,
|
||
imageBuilder: imageBuilder,
|
||
placeholder: (context, url) => Center(child: SpinKitWave(color: Theme.of(context).primaryColor, size: 50.r)),
|
||
errorWidget: errorWidget,
|
||
);
|
||
}
|
||
}
|
||
|
||
// double zoomHeight = ref.watch(zoomHeightProvider);
|
||
|
||
/// 网络图和本地图切换
|
||
@hwidget
|
||
Widget $localAndNetworkSwitch(
|
||
BuildContext context, {
|
||
double imageScale = 1,
|
||
Offset imagePosition = const Offset(0, 0),
|
||
required ImageWidgetBuilder? imageBuilder,
|
||
required GlobalKey zoomGlobalKey,
|
||
required double containerWidth,
|
||
required double containerHeight,
|
||
required String testQuestionNumber, // 试题题号
|
||
required String imageUrl, // 图片
|
||
required bool drawFlag, // 是否打开绘画操作
|
||
required GlobalKey theglobalKey,
|
||
required bool homework,
|
||
required GlobalKey<_ExamPaperDrawingState> examGlobalKey,
|
||
required AnnotationGraffitiSwitch graffitiSwitch,
|
||
required Function(File?) updateTempFileCall,
|
||
}) {
|
||
// ImageStream? imageStream; // 图片监听数据
|
||
// TestQuestionsImageInfo? imagInfoModel; // 试题图片数据
|
||
// ImageStreamListener theImageStreamListener;
|
||
|
||
UseLocalAndNetworkSwitch _useSwitch = UseLocalAndNetworkSwitch.use(!homework);
|
||
UseZoomImageHistory _useZoomHistory = UseZoomImageHistory.use(testQuestionNumber);
|
||
|
||
// TransformationController _transContller = useTransformationController();
|
||
|
||
useValueChanged<File?, String>(
|
||
_useSwitch.temFile.value,
|
||
(oldValue, oldResult) {
|
||
updateTempFileCall(_useSwitch.temFile.value);
|
||
return null;
|
||
},
|
||
);
|
||
|
||
useValueChanged<String, String>(testQuestionNumber, (oldValue, oldResult) {
|
||
if (testQuestionNumber.length > 0) {
|
||
_useZoomHistory.initInfo(testQuestionNumber);
|
||
}
|
||
});
|
||
|
||
useValueChanged<bool, String>(drawFlag, (oldValue, oldResult) {
|
||
if (!drawFlag) {
|
||
// 关闭的时候创建临时图片文件在设备
|
||
_useSwitch.createTempFile(context, theglobalKey: theglobalKey, examGlobalKey: examGlobalKey).then((File? theFile) {
|
||
if (theFile == null) {
|
||
// TODO 代表保存失败的逻辑
|
||
// 当前情况:_useSwich.showZoomImg.value 没有设置为true还是展示的原来的绘图组件ExamPaperDrawing
|
||
toPrint(val: '进入错误逻辑.........');
|
||
}
|
||
_useSwitch.showZoomImg.value = true;
|
||
});
|
||
return;
|
||
}
|
||
_useSwitch.showZoomImg.value = !drawFlag;
|
||
});
|
||
useEffect(() {
|
||
_useZoomHistory.initInfo(); // 初始化历史数据
|
||
|
||
// _useScrollController
|
||
// ..addListener(() {
|
||
// _scrollPosition.value = _useScrollController.position.pixels;
|
||
// });
|
||
return () {
|
||
// try {
|
||
// _useImageSize.imageStream.value?.removeListener(_useImageSize.imageListener.value!);
|
||
// } catch (e) {}
|
||
// ..removeListener(() {})
|
||
};
|
||
}, []);
|
||
|
||
print('是否更新视图.... ${_useZoomHistory.initPosition.value}');
|
||
return $MyCachedNetworkImage(
|
||
imageUrl: imageUrl,
|
||
tempFile: _useSwitch.temFile.value,
|
||
width: containerWidth,
|
||
height: containerHeight,
|
||
imageBuilder: imageBuilder,
|
||
);
|
||
}
|
||
|
||
class UseLocalAndNetworkSwitch {
|
||
ValueNotifier<bool> showZoomImg;
|
||
ValueNotifier<File?> temFile;
|
||
|
||
ValueNotifier<List<GestureRecording>?> points;
|
||
ValueNotifier<List<dynamic>?> pointsPureData;
|
||
ValueNotifier<Map<String, ui.Image>> imageLoaded;
|
||
|
||
UseLocalAndNetworkSwitch._({
|
||
required this.showZoomImg,
|
||
required this.temFile,
|
||
required this.points,
|
||
required this.pointsPureData,
|
||
required this.imageLoaded,
|
||
});
|
||
|
||
// 工厂构造函数
|
||
factory UseLocalAndNetworkSwitch.use(bool defaultVal) {
|
||
return UseLocalAndNetworkSwitch._(
|
||
points: useState(null),
|
||
pointsPureData: useState(null),
|
||
showZoomImg: useState(defaultVal),
|
||
temFile: useState(null),
|
||
imageLoaded: useState({}),
|
||
);
|
||
}
|
||
|
||
Future<File?> createTempFile(
|
||
BuildContext context, {
|
||
required GlobalKey theglobalKey,
|
||
required GlobalKey<_ExamPaperDrawingState> examGlobalKey,
|
||
}) async {
|
||
Timer? _timer;
|
||
try {
|
||
_timer = Timer(Duration(seconds: 1), () {
|
||
// 执行操作的代码
|
||
ToastUtils.showLoading();
|
||
});
|
||
if (examGlobalKey.currentState?.pointsPureData.isEmpty ?? true) {
|
||
try {
|
||
temFile.value?.delete();
|
||
} catch (e) {}
|
||
temFile.value = null;
|
||
return null;
|
||
}
|
||
|
||
RenderRepaintBoundary? boundary = theglobalKey.currentContext!.findRenderObject() as RenderRepaintBoundary?;
|
||
if (boundary == null) return null;
|
||
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
|
||
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||
if (byteData != null) {
|
||
Directory appDocDir = await getApplicationDocumentsDirectory();
|
||
List<int> bytes = byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes);
|
||
String filePath = '${appDocDir.path}/${Uuid().v1()}.png'; // 文件路径及名称
|
||
File file = File(filePath); // 创建文件对象
|
||
await file.writeAsBytes(bytes); // 将ByteData写入文件
|
||
|
||
temFile.value?.delete();
|
||
temFile.value = file; // 保存临时文件
|
||
|
||
points.value = examGlobalKey.currentState?._vnHandWritings.value;
|
||
pointsPureData.value = examGlobalKey.currentState?.pointsPureData;
|
||
toPrint(val: '图片保存成功:');
|
||
return temFile.value;
|
||
}
|
||
} catch (e) {
|
||
toPrint(val: '图片生成错误:${e}');
|
||
toPrint(val: e.toString());
|
||
ToastUtils.getFluttertoast(context: context, msg: '保存图片报错,请稍后重试');
|
||
} finally {
|
||
_timer?.cancel();
|
||
ToastUtils.dismiss();
|
||
}
|
||
return null;
|
||
}
|
||
}
|