import 'dart:ui'; import 'package:fis_i18n/i18n.dart'; import 'package:fis_jsonrpc/rpc.dart'; import 'package:fis_measure/interfaces/date_types/int_size.dart'; import 'package:fis_measure/interfaces/enums/operate.dart'; import 'package:fis_measure/interfaces/process/calculators/output.dart'; import 'package:fis_measure/interfaces/process/player/play_controller.dart'; import 'package:fis_measure/interfaces/process/standard_line/calibration.dart'; import 'package:fis_measure/interfaces/process/workspace/application.dart'; import 'package:fis_measure/interfaces/process/workspace/mobile_measure_view_state_controller.dart'; import 'package:fis_measure/process/workspace/measure_data_controller.dart'; import 'package:fis_measure/process/workspace/measure_handler.dart'; import 'package:fis_measure/process/workspace/third_part/application.dart'; import 'package:fis_measure/process/workspace/third_part/calibration_controller.dart'; import 'package:fis_measure/utils/canvas.dart'; import 'package:fis_measure/utils/prompt_box.dart'; import 'package:fis_measure/values/colors.dart'; import 'package:fis_measure/view/button_group/button_group.dart'; import 'package:fis_measure/view/gesture/annotation/annotation_gesture.dart'; import 'package:fis_measure/view/gesture/mobile_annotation/mobile_annotation_gesture.dart'; import 'package:fis_measure/view/gesture/touch_gesture.dart'; import 'package:fis_measure/view/measure/capture_image.dart'; import 'package:fis_measure/view/measure/measure_result.dart'; import 'package:fis_measure/view/mobile_view/controller/mobile_measure_view_state_controller.dart'; import 'package:fis_measure/view/mobile_view/mobile_bottom_menu.dart'; import 'package:fis_measure/view/mobile_view/mobile_right_panel.dart'; import 'package:fis_measure/view/mobile_view/mobile_top_menu.dart'; import 'package:fis_measure/view/mobile_view/widgets/magnifier.dart'; import 'package:fis_measure/view/paint/ai_patint.dart'; import 'package:fis_measure/view/paint/ai_patint_controller.dart'; import 'package:fis_measure/view/result/results_panel.dart'; import 'package:fis_measure/view/standard_line/calibration_canvas.dart'; import 'package:fis_measure/view/standard_line/calibration_gesture.dart'; import 'package:fis_ui/index.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:fis_measure/view/canvas/active_canvas.dart'; import 'package:fis_measure/view/canvas/annotation_canvas.dart'; import 'package:fis_measure/view/canvas/records_canvas.dart'; import 'package:fis_measure/view/gesture/mouse_gesture.dart'; import 'package:fis_measure/view/player/controller.dart'; import 'package:fis_measure/view/player/player.dart'; class MobileMeasureMainView extends StatefulWidget implements FWidget { const MobileMeasureMainView({Key? key}) : super(key: key); @override State createState() => _MobileMeasureMainViewState(); } class _MobileMeasureMainViewState extends State { late final application = Get.find(); late final playerController = Get.find(); late final measureHandler = Get.find(); /// 测量数据 final measureData = Get.find(); late bool canMeasure = application.canMeasure; late final aiPatintController = Get.find(); late bool enableCarotid2DMeasure = false; bool isCaptureState = false; // 是否是截图状态 late double calibrationLine = 4; final List outputs = []; /// 是否显示顶部菜单 bool ifShowTopMenu = true; /// 是否显示进度条 bool ifShowBottomMenu = true; /// 是否为纯播放器模式 bool isPlayerMode = true; /// 参考校准线控制器 IStandardLineCalibrationController? standardLineCalibrationController; final playerKey = GlobalKey(); final _ImageCaptureAreakey = GlobalKey(); bool get inAnnotation => application.currentOperateType == MeasureOperateType.annotation; @override void initState() { super.initState(); installStandardLine(); getNoteCommentsList(); final mobileMeasureStateController = Get.put(MobileMeasureViewStateController()); mobileMeasureStateController.onModeChanged.addListener(_onViewModeChanged); // VidPlayerController vidPlayerController = // playerController as VidPlayerController; WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { onCanMeasureChanged(this, application.canMeasure); application.canMeasureChanged.addListener(onCanMeasureChanged); application.operateTypeChanged.addListener(onOperateTypeChanged); }); } @override void dispose() { final mobileMeasureStateController = Get.find(); mobileMeasureStateController.onModeChanged .removeListener(_onViewModeChanged); Get.delete(); application.canMeasureChanged.removeListener(onCanMeasureChanged); application.operateTypeChanged.removeListener(onOperateTypeChanged); uninstallStandardLine(); super.dispose(); } ///移动端测量模式切换 void _onViewModeChanged(Object s, MobileMeasureMode mode) { switch (mode) { case MobileMeasureMode.playerMode: setState(() { isPlayerMode = true; }); break; case MobileMeasureMode.measureMode: setState(() { isPlayerMode = false; }); break; case MobileMeasureMode.annotationMode: setState(() { isPlayerMode = false; }); break; } } /// 装载参考校准线 void installStandardLine() { if (application.isThirdPart) { final standradLine = (application as ThirdPartApplication).standardLine; standardLineCalibrationController = StandardLineCalibrationController(application, standradLine); standardLineCalibrationController!.editStateChanged .addListener(onStandardLineCalibrationStateChanged); Get.put( standardLineCalibrationController!); } } /// 卸载参考校准线 void uninstallStandardLine() { standardLineCalibrationController?.editStateChanged .removeListener(onStandardLineCalibrationStateChanged); Get.delete(); } /// 保存图片 void capturePng() async { print('capturePng'); if (!isCaptureState) { setState(() { isCaptureState = true; }); for (var item in application.measureItems) { if (item.calculator != null) { // 添加历史测量值 outputs.addAll(item.calculator!.outputs); if (item.feature?.isActive == true && item.calculator!.output != null) { // 添加活动测量值 outputs.add(item.calculator!.output!); } } } MeasureResult measureResult = MeasureResult( measureApplicationName: application.applicationName, outputList: outputs, ); await Future.delayed(const Duration(milliseconds: 10), () async { final RenderRepaintBoundary? boundary = _ImageCaptureAreakey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary != null) { final image = await boundary.toImage(); final byteData = await image.toByteData(format: ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); await measureData.saveImage.call( pngBytes, measureData.measureImageData.patientCode ?? '', measureData.measureImageData.recordCode ?? '', measureData.measureImageData.remedicalCode ?? '', measureResult.toDisplay(), ); PromptBox.snackbar("保存位置:测量图像", title: "截图保存成功", textColor: const Color.fromARGB(255, 219, 219, 219)); Future.delayed( const Duration( milliseconds: 800, ), () { Get.back(); }, ); setState(() { isCaptureState = false; }); } }); } } /// 注释获取 void getNoteCommentsList() async { List commentsList = []; var measureCommentItemResult = await measureData.getCommentsByApplicationAsync( application.applicationName, application.categoryName, ); measureData.measureCommentItemResult = measureCommentItemResult?.commentItems ?? []; measureCommentItemResult?.commentItems?.forEach((element) { commentsList.add(element.text ?? ''); }); measureData.getCommentsList = commentsList; } void onCanMeasureChanged(Object sender, bool e) { if (e != canMeasure) { setState(() { canMeasure = e; }); } } void onOperateTypeChanged(Object sender, MeasureOperateType e) { setState(() {}); } void onStandardLineCalibrationStateChanged( Object sender, StandardLineCalibrationEditState e) { setState(() { if (e == StandardLineCalibrationEditState.drawn) { Get.dialog(buildCalibrationLine()); } }); } FWidget buildCalibrationLine() { return FSimpleDialog( title: FText( i18nBook.measure.guideCalibration.t, style: const TextStyle( color: Colors.white, fontSize: 18, ), ), isDefault: true, onOk: () { standardLineCalibrationController!.confirmEdit(calibrationLine); Get.back(); }, onCancel: () { standardLineCalibrationController!.cancelEdit(); Get.back(); }, children: [ buildCalibrationLineItem( i18nBook.measure.length.t, FTextField( decoration: InputDecoration( hintText: i18nBook.common.input.t + i18nBook.measure.length.t, hintStyle: const TextStyle( fontSize: 16, ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white.withOpacity(0.5), width: 0.5, style: BorderStyle.solid, ), ), focusedBorder: const OutlineInputBorder( borderSide: BorderSide( color: Colors.blue, width: 0.5, style: BorderStyle.solid, ), ), filled: true, ), onChanged: (val) => calibrationLine = double.parse(val), ), ), buildCalibrationLineItem( i18nBook.measure.unit.t, const FText('cm'), ), ], ); } FWidget buildCalibrationLineItem(String name, FWidget itemWidget) { return FContainer( padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 25), child: FRow( children: [ FContainer( width: 60, child: FText(name), ), FExpanded( child: itemWidget, ), ], ), ); } /// 布局说明: /// 移动端横屏布局分为四个部分 /// 1 撑满全屏的多层 Canvas (CustomMultiChildLayout) /// 2 顶部菜单栏(MobileTopMenu) /// 3 底部菜单栏(MobileBottomMenu) /// 4 右侧操作面板(MobileRightPanel) /// 5 可变区域,播放器状态下,5 属于 4,非播放器状态下,5 属于 2 /// +------------------------------------+-----+ /// | 2 | 5 | /// |------------------------------------+-----| /// | | | /// | 1 | 4 | /// | | | /// |------------------------------------+-----| /// | 3 | /// +------------------------------------------+ /// 除了【区域1】之外,可以定义其他每个分区的尺寸 (width, height) : (W2, H2)、(W3, H3)、(W4, H4) /// 【区域2】 H2 : 50, W2 : 100%, 左侧设置 10 的 padding /// @override Widget build(BuildContext context) { MeasureCanvasExt.setFontFamily( Theme.of(context).textTheme.button?.fontFamily); bool canShowAI = [ DiagnosisConclusionEnum.Benign, DiagnosisConclusionEnum.Malignant, DiagnosisConclusionEnum.BenignAndMalignant ].contains(measureData.diagnosisConclusion); return Container( color: MeasureColors.Background, child: RepaintBoundary( child: Stack( alignment: Alignment.bottomCenter, children: [ RepaintBoundary( key: _ImageCaptureAreakey, child: CustomMultiChildLayout( delegate: _LayerLayoutDelegate(), children: [ LayoutId( id: _LayerLayoutIds.background, child: Container( color: MeasureColors.Background, ), ), LayoutId( id: _LayerLayoutIds.player, child: VidPlayer( playerController as VidPlayerController, ), ), if (!isPlayerMode && canMeasure) ...[ LayoutId( id: _LayerLayoutIds.recordsCanvas, child: const MeasureRecordsCanvasPanel(), ), LayoutId( id: _LayerLayoutIds.activeMeasure, child: const MeasureActiveCanvasPanel(), ), LayoutId( id: _LayerLayoutIds.activeAnnotation, child: const AnnotationCanvas(), ), if (application.isThirdPart) LayoutId( id: _LayerLayoutIds.standardLineCalibration, child: StandardLineCalibrationCanvas( standardLineCalibrationController!), ), LayoutId( id: _LayerLayoutIds.gesture, child: _buildGestureLayer(), ), LayoutId( id: _LayerLayoutIds.result, child: const SizedBox( height: 200, width: 140, child: MeasureResultPanel( resultFontSize: 12, )), ), LayoutId( id: _LayerLayoutIds.canvasMagnifier, child: const CanvasMagnifier(), ), ], if (isPlayerMode && canShowAI) ...[ LayoutId( id: _LayerLayoutIds.paintAI, child: AIPaintInfo( playerController as VidPlayerController, ), ) ], ], ), ), const MobileRightPanel(), ifShowTopMenu ? MobileTopMenu(capturePng: capturePng) : Container(), ifShowBottomMenu ? const MobileBottomMenu() : Container(), ], ), ), ); } Widget _buildGestureLayer() { if (application.isThirdPart) { if (standardLineCalibrationController!.isEditing) { return StandardLineCalibrationGesture( standardLineCalibrationController!); } } return inAnnotation ? const AnnotationTouchLayer() : const MeasureTouchGesturePanel(); } } class _LayerLayoutDelegate extends MultiChildLayoutDelegate { Offset? layoutOffset; Size? layoutSize; _LayerLayoutDelegate(); @override void performLayout(Size size) { if (!hasChild(_LayerLayoutIds.player)) return; final application = Get.find(); final vidFrame = application.frameData; final imageSize = IntSize.fill(vidFrame?.width ?? 0, vidFrame?.height ?? 0); /// 以Contain方式填充布局,计算定位偏移量 calcSize(size, imageSize); final offset = layoutOffset!; final renderSize = layoutSize!; /// 同步图像显示尺寸 application.displaySize = renderSize; layoutLayer(_LayerLayoutIds.background, Offset.zero, size); layoutLayer(_LayerLayoutIds.player, offset, renderSize); /// 其他层按播放器尺寸位置层叠布局 layoutLayer(_LayerLayoutIds.recordsCanvas, offset, renderSize); layoutLayer(_LayerLayoutIds.activeMeasure, offset, renderSize); layoutLayer(_LayerLayoutIds.activeAnnotation, offset, renderSize); layoutLayer(_LayerLayoutIds.standardLineCalibration, offset, renderSize); layoutLayer(_LayerLayoutIds.canvasMagnifier, offset, renderSize); layoutLayer(_LayerLayoutIds.gesture, offset, renderSize); layoutLayer( _LayerLayoutIds.result, const Offset(10, 50), // H2 : 50 renderSize, ); layoutLayer(_LayerLayoutIds.pause, offset, renderSize); layoutLayer(_LayerLayoutIds.paintAI, offset, renderSize); } void layoutLayer(_LayerLayoutIds layoutId, Offset offset, Size size) { if (hasChild(layoutId)) { layoutChild( layoutId, BoxConstraints.loose(size), ); positionChild(layoutId, offset); } } void calcSize(Size size, IntSize imageSize) { final parentWHRatio = size.width / size.height; final imageWHRatio = imageSize.width / imageSize.height; if (imageWHRatio < parentWHRatio) { // 高度撑满 final layoutWidth = size.height * imageWHRatio; final layoutHeight = size.height; final offsetX = (size.width - layoutWidth) / 2; layoutOffset = Offset(offsetX, 0); layoutSize = Size(layoutWidth, layoutHeight); } else if (imageWHRatio > parentWHRatio) { // 宽度撑满 final layoutWidth = size.width; final layoutHeight = size.width / imageWHRatio; final offsetY = (size.height - layoutHeight) / 2; layoutOffset = Offset(0, offsetY); layoutSize = Size(layoutWidth, layoutHeight); } else { layoutOffset = Offset.zero; layoutSize = size; } } @override bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) { return false; } } enum _LayerLayoutIds { /// 黑色背景层 background, /// 播放器 player, /// 画布放大镜层 canvasMagnifier, /// 测量记录画板 recordsCanvas, /// 活动测量画板 activeMeasure, /// 活动注释画板 activeAnnotation, /// 结果面板 result, /// 手势面板 gesture, /// 暂停画板 后面需要优化命名 pause, /// AI画板 paintAI, /// 参考校准线画板 standardLineCalibration, }