【问题标题】:Saving picture of Canvas with PictureRecorder results in empty image使用 PictureRecorder 保存 Canvas 的图片会导致图像为空
【发布时间】:2018-09-30 11:35:21
【问题描述】:

首先,该程序的目标是允许用户使用手机或平板电脑签署官方文件。 程序必须将图像保存为 png。

我使用 Flutter(和 dart)和 VS Code 来开发这个应用程序。

什么有效:

-The user can draw on the canvas.

什么不起作用:

-the image can't be saved as a png

我发现了什么:

-The **Picture** get by ending the **PictureRecoder** of the canvas is empty (i tried to display it but no success)
-I tried to save it as a PNG using **PictureRecorder.EndRecording().toImage(100.0,100.0).toByteDate(EncodingFormat.png())** but the size is really small, and it can't be displayed.

如果你们中的一些人能给我一些关于问题出在哪里的提示,那就太好了。

仅供参考:Flutter 在开发频道上是最新版本

这是完整的代码:

import 'dart:ui' as ui;
import 'package:flutter/material.dart';

///This class is use just to check if the Image returned by
///the PictureRecorder of the first Canvas is not empty.
///FYI : The image is not displayed.
class CanvasImageDraw extends CustomPainter {

    ui.Picture picture;

    CanvasImageDraw(this.picture);

    @override
    void paint(ui.Canvas canvas, ui.Size size) {
      canvas.drawPicture(picture);
    }

    @override
    bool shouldRepaint(CustomPainter oldDelegate) {
      return true;
    }
}

///Class used to display the second canvas
class SecondCanvasView extends StatelessWidget {

    ui.Picture picture;

    var canvas;
    var pictureRecorder= new ui.PictureRecorder();

    SecondCanvasView(this.picture) {
      canvas = new Canvas(pictureRecorder);
    }

    @override
    Widget build(BuildContext context) {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text('Image Debug'),
        ),
        body: new Column(
          children: <Widget>[
            new Text('Top'),
            CustomPaint(
              painter: new CanvasImageDraw(picture),
            ),
            new Text('Bottom'),
          ],
        ));
    }
}

///This is the CustomPainter of the first Canvas which is used
///to draw to display the users draw/sign.
class SignaturePainter extends CustomPainter {
  final List<Offset> points;
  Canvas canvas;
  ui.PictureRecorder pictureRecorder;

  SignaturePainter(this.points, this.canvas, this.pictureRecorder);

  void paint(canvas, Size size) {
    Paint paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

///Classes used to get the user draw/sign, send it to the first canvas, 
///then display it.
///'points' list of position returned by the GestureDetector
class Signature extends StatefulWidget {
  ui.PictureRecorder pictureRecorder = new ui.PictureRecorder();
  Canvas canvas;
  List<Offset> points = [];

  Signature() {
    canvas = new Canvas(pictureRecorder);
  }

  SignatureState createState() => new SignatureState(canvas, points);
}

class SignatureState extends State<Signature> {
  List<Offset> points = <Offset>[];
  Canvas canvas;

  SignatureState(this.canvas, this.points);

  Widget build(BuildContext context) {
    return new Stack(
      children: [
        GestureDetector(
          onPanUpdate: (DragUpdateDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);

            setState(() {
              points = new List.from(points)..add(localPosition);
            });
          },
          onPanEnd: (DragEndDetails details) => points.add(null),
        ),
        new Text('data'),
        CustomPaint(
            painter:
                new SignaturePainter(points, canvas, widget.pictureRecorder)),
        new Text('data')
      ],
    );
  }
}


///The main class which display the first Canvas, Drawing/Signig area
///
///Floating action button used to stop the PictureRecorder's recording,
///then send the Picture to the next view/Canvas which should display it
class DemoApp extends StatelessWidget {
  Signature sign = new Signature();
  Widget build(BuildContext context) => new Scaffold(
        body: sign,
        floatingActionButton: new FloatingActionButton(
          child: new Icon(Icons.save),
          onPressed: () async {
            ui.Picture picture = sign.pictureRecorder.endRecording();
            Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondCanvasView(picture)));
          },
        ),
  );
}

void main() => runApp(new MaterialApp(home: new DemoApp()));

【问题讨论】:

    标签: image canvas dart png flutter


    【解决方案1】:

    首先,感谢您提出有趣的问题和独立的答案。这令人耳目一新,而且更容易提供帮助!

    您的代码存在一些问题。首先是您不应该将画布和点从 Signature 传递到 SignatureState;这是 Flutter 中的反模式。

    这里的代码有效。我已经做了一些对答案来说不必要的小事情来清理它,对此感到抱歉=D。

    import 'dart:ui' as ui;
    
    import 'package:flutter/material.dart';
    
    ///This class is use just to check if the Image returned by
    ///the PictureRecorder of the first Canvas is not empty.
    class CanvasImageDraw extends CustomPainter {
      ui.Image image;
    
      CanvasImageDraw(this.image);
    
      @override
      void paint(ui.Canvas canvas, ui.Size size) {
        // simple aspect fit for the image
        var hr = size.height / image.height;
        var wr = size.width / image.width;
    
        double ratio;
        double translateX;
        double translateY;
        if (hr < wr) {
          ratio = hr;
          translateX = (size.width - (ratio * image.width)) / 2;
          translateY = 0.0;
        } else {
          ratio = wr;
          translateX = 0.0;
          translateY = (size.height - (ratio * image.height)) / 2;
        }
    
        canvas.translate(translateX, translateY);
        canvas.scale(ratio, ratio);
        canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
      }
    
      @override
      bool shouldRepaint(CanvasImageDraw oldDelegate) {
        return image != oldDelegate.image;
      }
    }
    
    ///Class used to display the second canvas
    class SecondView extends StatelessWidget {
      ui.Image image;
    
      var pictureRecorder = new ui.PictureRecorder();
    
      SecondView({this.image});
    
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
            appBar: new AppBar(
              title: new Text('Image Debug'),
            ),
            body: new Column(
              children: <Widget>[
                new Text('Top'),
                CustomPaint(
                  painter: new CanvasImageDraw(image),
                  size: new Size(200.0, 200.0),
                ),
                new Text('Bottom'),
              ],
            ));
      }
    }
    
    ///This is the CustomPainter of the first Canvas which is used
    ///to draw to display the users draw/sign.
    class SignaturePainter extends CustomPainter {
      final List<Offset> points;
      final int revision;
    
      SignaturePainter(this.points, [this.revision = 0]);
    
      void paint(canvas, Size size) {
        if (points.length < 2) return;
    
        Paint paint = new Paint()
          ..color = Colors.black
          ..strokeCap = StrokeCap.round
          ..strokeWidth = 5.0;
    
        for (int i = 0; i < points.length - 1; i++) {
          if (points[i] != null && points[i + 1] != null)
            canvas.drawLine(points[i], points[i + 1], paint);
        }
      }
    
      // simplified this, but if you ever modify points instead of changing length you'll
      // have to revise.
      bool shouldRepaint(SignaturePainter other) => other.revision != revision;
    }
    
    ///Classes used to get the user draw/sign, send it to the first canvas,
    ///then display it.
    ///'points' list of position returned by the GestureDetector
    class Signature extends StatefulWidget {
      Signature({Key key}): super(key: key);
    
      @override
      State<StatefulWidget> createState()=> new SignatureState();
    }
    
    class SignatureState extends State<Signature> {
      List<Offset> _points = <Offset>[];
      int _revision = 0;
    
      ui.Image get rendered {
        var pictureRecorder = new ui.PictureRecorder();
        Canvas canvas = new Canvas(pictureRecorder);
        SignaturePainter painter = new SignaturePainter(_points);
    
        var size = context.size;
        // if you pass a smaller size here, it cuts off the lines
        painter.paint(canvas, size);
        // if you use a smaller size for toImage, it also cuts off the lines - so I've
        // done that in here as well, as this is the only place it's easy to get the width & height.
        return pictureRecorder.endRecording().toImage(size.width.floor(), size.height.floor());
      }
    
      void _addToPoints(Offset position) {
        _revision++;
        _points.add(position);
      }
    
      Widget build(BuildContext context) {
        return new Stack(
          children: [
            GestureDetector(
              onPanStart: (DragStartDetails details) {
                RenderBox referenceBox = context.findRenderObject();
                Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
                setState(() {
                  _addToPoints(localPosition);
                });
              },
              onPanUpdate: (DragUpdateDetails details) {
                RenderBox referenceBox = context.findRenderObject();
                Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
                setState(() {
                  _addToPoints(localPosition);
                });
              },
              onPanEnd: (DragEndDetails details) => setState(() => _addToPoints(null)),
            ),
            CustomPaint(painter: new SignaturePainter(_points, _revision)),
          ],
        );
      }
    }
    
    ///The main class which display the first Canvas, Drawing/Signing area
    ///
    ///Floating action button used to stop the PictureRecorder's recording,
    ///then send the Picture to the next view/Canvas which should display it
    class DemoApp extends StatelessWidget {
      GlobalKey<SignatureState> signatureKey = new GlobalKey();
    
      Widget build(BuildContext context) => new Scaffold(
            body: new Signature(key: signatureKey),
            floatingActionButton: new FloatingActionButton(
              child: new Icon(Icons.save),
              onPressed: () async {
                var image = signatureKey.currentState.rendered;
                Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondView(image: image)));
              },
            ),
          );
    }
    
    void main() => runApp(new MaterialApp(home: new DemoApp()));
    

    您遇到的最大问题是您将自己的 canvas 对象到处都是 - 这不是您真正应该做的事情。 CustomPainter 提供了自己的画布。当你在画画的时候,我不认为它实际上是在正确的地方画画。

    此外,每次添加点时,您都​​会重新创建列表。我将假设这是因为other.points != points 导致您的画布无法绘制。我已将其更改为采用一个修订 int,每次将某些内容添加到列表中时它都会递增。更好的是使用您自己的列表子类,它自己执行此操作,但是对于此示例 =D 来说有点多。如果您确定永远不会修改列表中的任何元素,您也可以只使用 other.points.length != points.length

    考虑到图像的大小,还有一些事情要做。我走了最简单的路线,使画布与其父级的大小相同,因此渲染更容易,因为它可以再次使用相同的大小(因此渲染一个手机屏幕大小的图像)。如果您不想这样做而是渲染自己的大小,则可以这样做。您必须向 SignaturePainter 传递一些内容,以便它知道如何缩放点以使其适合新大小(如果您愿意,可以在 CanvasImageDraw 中调整方面适合代码)。

    如果您对我所做的事情有任何其他问题,请随时提出。

    【讨论】:

    • 感谢您,我知道问题出在哪里。我没有进一步的问题,现在一切都清楚了。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2018-06-01
    • 1970-01-01
    • 2012-09-08
    • 1970-01-01
    • 1970-01-01
    • 2013-11-03
    • 2021-08-01
    • 1970-01-01
    相关资源
    最近更新 更多