无法在颤动中截取屏幕截图

Posted

技术标签:

【中文标题】无法在颤动中截取屏幕截图【英文标题】:Unable to take screenshot in flutter 【发布时间】:2019-12-29 21:14:57 【问题描述】:

package:flutter/src/rendering/proxy_box.dart': 断言失败: line 2813 pos 12: '!debugNeedsPaint': is not true.

我试图在颤动中截取屏幕截图,但我遇到了异常。我访问了许多链接,但没有任何效果。

Future<Uint8List> _capturePng() async 
    try 
        print('inside');
        RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
        ui.Image image = await boundary.toImage(pixelRatio: 3.0);
        ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
        var pngBytes = byteData.buffer.asUint8List();
        var bs64 = base64Encode(pngBytes);
        print(pngBytes);
        print(bs64);
        setState(() );
        return pngBytes;
     catch (e) 
        print(e);
    

【问题讨论】:

这个函数在哪里调用?如果在 StatefulWidgetinitState 内调用它很可能不起作用,因为您从 _globalKey 的上下文中检索的 RenderObject 尚未绘制/绘制。 【参考方案1】:

你的代码已经很好了,在发布模式下你应该不会遇到任何问题,因为来自docs:

debugNeedsPaint:此渲染对象的绘制信息是否脏。

这仅在调试模式下设置。一般来说,渲染对象不需要根据它们是否脏来决定它们的运行时行为,因为它们只应在布局和绘制之前立即标记为脏。

它旨在供测试和断言使用。

debugNeedsPaint 为 false 并且 debugNeedsLayout 为 true 是可能的(实际上,很常见)。在这种情况下,渲染对象仍将在下一帧重新绘制,因为在绘制阶段之前,框架会在布局渲染对象之后隐式调用 markNeedsPaint 方法。

但是,如果您仍然需要解决方案,您可以试试这个:

Future<Uint8List> _capturePng() async 
  try 
    print('inside');
    RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
    
    // if it needs repaint, we paint it.
    if (boundary.debugNeedsPaint) 
      Timer(Duration(seconds: 1), () => _capturePng());
      return null;
    
    
    ui.Image image = await boundary.toImage(pixelRatio: 3.0);
    ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    var pngBytes = byteData.buffer.asUint8List();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
    setState(() );
    return pngBytes;
   catch (e) 
    print(e);
    return null;
  

【讨论】:

我认为这段代码行不通,因为_capturePng 将返回null,然后在一秒钟内将在控制台中打印字节。我的意思是if (boundary.debugNeedsPaint) Timer(Duration(seconds: 1), () =&gt; _capturePng()); return null; @DenisZakharov 不会这样的,试一试,告诉我。 你的Timer异步调度,回调会在_capturePng返回null后一秒执行。请检查我的解决方案,我相信你会明白我的意思。 我希望你会投票支持我的解决方案))。顺便说一句,我检查了您的代码。正如我所说的那样工作。 @DenisZakharov 抱歉,我目前无法测试您的代码,但我可以看到您使用了我的逻辑【参考方案2】:

您可以找到官方toImage 示例here。但看起来它在按钮点击和toImage 调用之间没有延迟。

官方仓库有问题:https://github.com/flutter/flutter/issues/22308

原因:您的点击初始化按钮的动画,RenderObject.markNeedsPaint 被包括父母在内的递归调用,所以您应该等待debugNeedsPaint 将再次成为falsetoImage 函数在这种情况下只会抛出断言错误:

  Future<ui.Image> toImage( double pixelRatio = 1.0 ) 
    assert(!debugNeedsPaint);
    final OffsetLayer offsetLayer = layer;
    return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
  

https://github.com/flutter/flutter/blob/f0553ba58e6455aa63fafcdca16100b81ff5c3ce/packages/flutter/lib/src/rendering/proxy_box.dart#L2857

  bool get debugNeedsPaint 
    bool result;
    assert(() 
      result = _needsPaint;
      return true;
    ());
    return result;
  

https://github.com/flutter/flutter/blob/0ca5e71f281cd549f1b5284e339523ad93544c60/packages/flutter/lib/src/rendering/object.dart#L2011

其实assert函数只在开发中使用,所以你可以看到你在生产中不会遇到错误的麻烦。但我不知道你会遇到什么样的麻烦,可能不会)。

下一个代码不是很好,但它可以工作:

class _MyHomePageState extends State<MyHomePage> 
  GlobalKey globalKey = GlobalKey();

  Future<Uint8List> _capturePng() async 
    RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();

    if (boundary.debugNeedsPaint) 
      print("Waiting for boundary to be painted.");
      await Future.delayed(const Duration(milliseconds: 20));
      return _capturePng();
    

    var image = await boundary.toImage();
    var byteData = await image.toByteData(format: ImageByteFormat.png);
    return byteData.buffer.asUint8List();
  

  void _printPngBytes() async 
    var pngBytes = await _capturePng();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
  

  @override
  Widget build(BuildContext context) 
    return RepaintBoundary(
      key: globalKey,
      child: Center(
        child: FlatButton(
          color: Color.fromARGB(255, 255, 255, 255),
          child: Text('Capture Png', textDirection: TextDirection.ltr),
          onPressed: _printPngBytes
        ),
      ),
    );
  

【讨论】:

重绘边界(key: globalKey,)【参考方案3】:

我再次将返回逻辑从 null 更改为函数 _capturePng()。

参考:@CopsOnRoad

检查以下代码

    Future<Uint8List> _capturePng() async 
  try 
    print('inside');
    RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
    
    // if it needs repaint, we paint it.
    if (boundary.debugNeedsPaint) 

//if debugNeedsPaint return to function again.. and loop again until boundary have paint.
return _capturePng()); 
    
    
    ui.Image image = await boundary.toImage(pixelRatio: 3.0);
    ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    var pngBytes = byteData.buffer.asUint8List();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
    setState(() );
    return pngBytes;
   catch (e) 
    print(e);
    return null;
  

【讨论】:

【参考方案4】:

我也遇到了同样的问题

经过长时间的研究

我终于找到问题了

如果你要捕捉的widget在ListView中,而listview很长,所以你的widget不在屏幕上,捕捉会失败。

【讨论】:

我没有在任何地方使用 Listview 我正在使用堆栈【参考方案5】:

这是对已接受答案的重要补充。根据debugNeedsPaint 的文档,在发布模式下检查此标志将使应用程序崩溃(In release builds, this throws.)

所以我们只需要在调试模式下检查这个标志,使用来自import 'package:flutter/foundation.dart';kDebugMode

    var debugNeedsPaint = false;

    //https://***.com/questions/49707028/how-to-check-flutter-application-is-running-in-debug
    //In release builds, this throws (boundary.debugNeedsPaint)
    if (kDebugMode) debugNeedsPaint = boundary.debugNeedsPaint;

    if (debugNeedsPaint) 
      print("Waiting for boundary to be painted.");
      await Future.delayed(const Duration(milliseconds: 20));
      return _capturePng();
    

【讨论】:

【参考方案6】:

这里是 Flutter 2.0+ 方法截图并分享到社交媒体的解决方案。

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';

class selClass extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      home: sel(),
    );
  


class sel extends StatefulWidget 
  @override
  _selState createState() => _selState();


class _selState extends State<sel> 
  String _date = "Not set";

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: SafeArea(child:SingleChildScrollView(
    child: Column(
    children: <Widget>[
    Container(
    color: Colors.grey,
      width: MediaQuery
          .of(context)
          .size
          .width,
      height: MediaQuery
          .of(context)
          .size
          .height / 10,
      child: Center(child: Text("Attendance",
        style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),),),
    ),
    SizedBox(height: 30.0,),
    Padding(
    padding: const EdgeInsets.all(16.0),
    child: Container(
    child: Column(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    RaisedButton(
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(5.0)),
    elevation: 4.0,
    onPressed: () 
    DatePicker.showDatePicker(context,
    theme: DatePickerTheme(
    containerHeight: 210.0,
    ),
    showTitleActions: true,
    minTime: DateTime(2000, 1, 1),
    maxTime: DateTime(2022, 12, 31),
    onConfirm: (date) 
    print('confirm $date');
    _date = '$date.year - $date.month - $date.day';
    setState(() );
    ,
    currentTime: DateTime.now(),
    locale: LocaleType.en);
    ,
    child: Container(
    alignment: Alignment.center,
    height: 50.0,
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: <Widget>[
    Row(
    children: <Widget>[
    Container(
    child: Row(
    children: <Widget>[
    Icon(
    Icons.date_range,
    size: 18.0,
    color: Colors.teal,
    ),
    Text(
    " $_date",
    style: TextStyle(
    color: Colors.teal,
    fontWeight: FontWeight.bold,
    fontSize: 18.0),
    ),
    ],
    ),
    )
    ],
    ),
    Text(
    "  Change",
    style: TextStyle(
    color: Colors.teal,
    fontWeight: FontWeight.bold,
    fontSize: 18.0),
    ),
    ],
    ),
    ),
    color: Colors.white,
    ),
    SizedBox(
    height: 20.0,
    ),
    ]
    ,
    )
    ,
    )
    )
    ]))));
  

【讨论】:

以上是关于无法在颤动中截取屏幕截图的主要内容,如果未能解决你的问题,请参考以下文章

颤动:从其兄弟中截取小部件的屏幕截图

如何使用路径提供程序包将颤动屏幕截图保存到我的桌面

为啥我无法截取 MPMoviePlayerController 的屏幕截图?

如何使用嵌入视频从 WKWebView 截取屏幕截图?

如何以编程方式截取可扩展的uitableview的屏幕截图

如何截取完整滚动文件的屏幕截图