如何在Flutter应用中实现绘图的标记和注释工具?

问题描述 投票:0回答:1

背景:

我们正在 Flutter 应用程序中开发项目文件夹管理功能,特别专注于处理绘图。这包括实现绘图的标记和注释工具,以及添加注释、突出显示区域和绘图形状等功能。此外,我们需要支持版本控制来跟踪更改并维护文件历史记录。

要求:

  1. 注释工具:

    • 注释: 用户应该能够在图纸上添加文本注释。

    • 突出显示: 用户应该能够突出显示绘图的特定区域。

    • 形状: 用户应该能够绘制矩形、圆形和手绘线条等形状。

  2. 版本控制:

    • 文件历史记录:维护对图纸所做更改的历史记录。
    • 恢复更改:允许用户恢复到绘图的先前版本。

问题:

我们如何在 Flutter 应用程序中实现绘图的标记和注释工具?具体来说,我正在寻找以下方面的指导:

  • 在图纸上添加文字注释。
  • 突出显示特定区域 图纸。
  • 在上面绘制形状(矩形、圆形、手绘线条) 图纸。
  • 实施版本控制来维护和恢复文件 历史。

我们希望实现与此链接中描述的功能类似的功能:

https://support.procore.com/products/online/user-guide/project-level/drawings/tutorials/mark-up-a-drawing.

备注:

  • 图纸可以是PDF文件或图像。
  • 绘图可能很大,因此用户可能希望大幅放大。
  • 我们需要将标记和注释保存到文件本身,无论是 PDF 还是图像。
  • 如果我们能够实现与提供的 URL 完全相同的东西,那就完美了。

提前谢谢您。

flutter dart annotations markup flutter-custompainter
1个回答
0
投票

要在 Flutter 应用程序中实现具有类似于 Procore 文档中描述的功能的绘图标记和注释工具,您需要创建一个全面的绘图编辑器组件。以下是实现此功能的示例方法:

import 'package:flutter/material.dart';
import 'package:pdf_render/pdf_render.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:ui' as ui;

class DrawingMarkupEditor extends StatefulWidget {
  final String filePath;

  DrawingMarkupEditor({required this.filePath});

  @override
  _DrawingMarkupEditorState createState() => _DrawingMarkupEditorState();
}

class _DrawingMarkupEditorState extends State<DrawingMarkupEditor> {
  late Future<ui.Image> _imageFuture;
  List<Markup> _markups = [];
  MarkupTool _currentTool = MarkupTool.select;

  @override
  void initState() {
    super.initState();
    _imageFuture = _loadImage();
  }

  Future<ui.Image> _loadImage() async {
    if (widget.filePath.toLowerCase().endsWith('.pdf')) {
      final document = await PdfDocument.openFile(widget.filePath);
      final page = await document.getPage(1);
      final pageImage = await page.render(width: page.width, height: page.height);
      return pageImage.createImage();
    } else {
      final bytes = await File(widget.filePath).readAsBytes();
      final codec = await ui.instantiateImageCodec(bytes);
      final frame = await codec.getNextFrame();
      return frame.image;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Drawing Markup Editor'),
        actions: [
          IconButton(
            icon: Icon(Icons.save),
            onPressed: _saveMarkups,
          ),
        ],
      ),
      body: FutureBuilder<ui.Image>(
        future: _imageFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            return GestureDetector(
              onPanUpdate: (details) => _handlePanUpdate(details, snapshot.data!),
              child: CustomPaint(
                painter: DrawingPainter(image: snapshot.data!, markups: _markups),
                child: Container(),
              ),
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
      bottomNavigationBar: BottomAppBar(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            IconButton(
              icon: Icon(Icons.pan_tool),
              onPressed: () => setState(() => _currentTool = MarkupTool.select),
            ),
            IconButton(
              icon: Icon(Icons.create),
              onPressed: () => setState(() => _currentTool = MarkupTool.pen),
            ),
            IconButton(
              icon: Icon(Icons.highlight),
              onPressed: () => setState(() => _currentTool = MarkupTool.highlight),
            ),
            IconButton(
              icon: Icon(Icons.crop_square),
              onPressed: () => setState(() => _currentTool = MarkupTool.rectangle),
            ),
            IconButton(
              icon: Icon(Icons.circle_outlined),
              onPressed: () => setState(() => _currentTool = MarkupTool.ellipse),
            ),
            IconButton(
              icon: Icon(Icons.text_fields),
              onPressed: () => _addTextMarkup(),
            ),
          ],
        ),
      ),
    );
  }

  void _handlePanUpdate(DragUpdateDetails details, ui.Image image) {
    final RenderBox renderBox = context.findRenderObject() as RenderBox;
    final position = renderBox.globalToLocal(details.globalPosition);

    setState(() {
      switch (_currentTool) {
        case MarkupTool.pen:
          _addPenMarkup(position);
          break;
        case MarkupTool.highlight:
          _addHighlightMarkup(position);
          break;
        case MarkupTool.rectangle:
          _addRectangleMarkup(position);
          break;
        case MarkupTool.ellipse:
          _addEllipseMarkup(position);
          break;
        default:
          break;
      }
    });
  }

  void _addPenMarkup(Offset position) {
    if (_markups.isNotEmpty && _markups.last is PenMarkup) {
      (_markups.last as PenMarkup).addPoint(position);
    } else {
      _markups.add(PenMarkup(color: Colors.black, points: [position]));
    }
  }

  void _addHighlightMarkup(Offset position) {
    if (_markups.isNotEmpty && _markups.last is HighlightMarkup) {
      (_markups.last as HighlightMarkup).addPoint(position);
    } else {
      _markups.add(HighlightMarkup(color: Colors.yellow.withOpacity(0.5), points: [position]));
    }
  }

  void _addRectangleMarkup(Offset position) {
    if (_markups.isNotEmpty && _markups.last is RectangleMarkup) {
      (_markups.last as RectangleMarkup).updateEndPoint(position);
    } else {
      _markups.add(RectangleMarkup(color: Colors.red, start: position, end: position));
    }
  }

  void _addEllipseMarkup(Offset position) {
    if (_markups.isNotEmpty && _markups.last is EllipseMarkup) {
      (_markups.last as EllipseMarkup).updateEndPoint(position);
    } else {
      _markups.add(EllipseMarkup(color: Colors.blue, start: position, end: position));
    }
  }

  void _addTextMarkup() {
    showDialog(
      context: context,
      builder: (context) {
        String text = '';
        return AlertDialog(
          title: Text('Add Text Markup'),
          content: TextField(
            onChanged: (value) => text = value,
            decoration: InputDecoration(hintText: 'Enter text'),
          ),
          actions: [
            TextButton(
              child: Text('Cancel'),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text('Add'),
              onPressed: () {
                setState(() {
                  _markups.add(TextMarkup(text: text, position: Offset(100, 100), color: Colors.black));
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _saveMarkups() async {
    // Implementation for saving markups
  }
}

class DrawingPainter extends CustomPainter {
  final ui.Image image;
  final List<Markup> markups;

  DrawingPainter({required this.image, required this.markups});

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawImage(image, Offset.zero, Paint());

    for (var markup in markups) {
      markup.draw(canvas);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

abstract class Markup {
  void draw(Canvas canvas);
}

class PenMarkup extends Markup {
  final Color color;
  final List<Offset> points;

  PenMarkup({required this.color, required this.points});

  void addPoint(Offset point) {
    points.add(point);
  }

  @override
  void draw(Canvas canvas) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = 2
      ..strokeCap = StrokeCap.round;

    for (int i = 0; i < points.length - 1; i++) {
      canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
}

class HighlightMarkup extends PenMarkup {
  HighlightMarkup({required Color color, required List<Offset> points})
      : super(color: color, points: points);

  @override
  void draw(Canvas canvas) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = 20
      ..strokeCap = StrokeCap.round
      ..blendMode = BlendMode.multiply;

    for (int i = 0; i < points.length - 1; i++) {
      canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
}

class RectangleMarkup extends Markup {
  final Color color;
  Offset start;
  Offset end;

  RectangleMarkup({required this.color, required this.start, required this.end});

  void updateEndPoint(Offset point) {
    end = point;
  }

  @override
  void draw(Canvas canvas) {
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    canvas.drawRect(Rect.fromPoints(start, end), paint);
  }
}

class EllipseMarkup extends Markup {
  final Color color;
  Offset start;
  Offset end;

  EllipseMarkup({required this.color, required this.start, required this.end});

  void updateEndPoint(Offset point) {
    end = point;
  }

  @override
  void draw(Canvas canvas) {
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    canvas.drawOval(Rect.fromPoints(start, end), paint);
  }
}

class TextMarkup extends Markup {
  final String text;
  final Offset position;
  final Color color;

  TextMarkup({required this.text, required this.position, required this.color});

  @override
  void draw(Canvas canvas) {
    final textPainter = TextPainter(
      text: TextSpan(text: text, style: TextStyle(color: color)),
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();
    textPainter.paint(canvas, position);
  }
}

enum MarkupTool { select, pen, highlight, rectangle, ellipse, text }

此实现为在 Flutter 中创建绘图标记编辑器提供了基础,解决了您提到的关键需求。以下是主要组件的详细信息以及它们如何满足您的要求:

注释工具:

Comments:TextMarkup 类允许向绘图添加文本注释。 突出显示:HighlightMarkup 类提供了一种突出显示特定区域的方法。 形状:PenMarkup、RectangleMarkup 和 EllipseMarkup 类允许绘制各种形状。

绘图工具:

底部应用栏提供了用于选择不同标记工具的按钮。 _handlePanUpdate 方法处理用户输入的绘图。

渲染:

DrawingPainter 类处理渲染图像和所有标记。

文件处理:

_loadImage方法支持加载PDF和图像文件。

要完全实现版本控制并保存标记,您需要扩展此代码。这里有一些建议:

版本控制:

创建一个 DrawingVersion 类来表示绘图的特定版本及其标记。 实现从本地存储或服务器保存和加载版本的方法。 添加 UI 元素以查看并恢复到以前的版本。

保存标记:

实现 _saveMarkups 方法将标记数据序列化并与原始文件一起保存。 对于 PDF,您可能需要使用 PDF 操作库来添加标记作为注释。 对于图像,您可以将标记渲染到图像上并将结果保存为新文件。

缩放和平移:

实现手势识别器来缩放和平移绘图。 根据缩放级别调整标记位置和大小。

标记编辑:

实现现有标记的选择和修改。 添加选项来更改标记的颜色、线宽和其他属性。

图层管理:

实现类似于 Procore 的个人和已发布图层的图层系统。 添加用于切换图层可见性和发布个人标记的 UI。

© www.soinside.com 2019 - 2024. All rights reserved.