无法在Flutter中截屏

21
在Flutter中尝试截图,但遇到了异常。访问了许多链接,但没有任何作用。

package:flutter/src/rendering/proxy_box.dart':失败的断言:第2813行第12个字符:'!debugNeedsPaint':不为真。

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);
    }
}

1
这个函数在哪里被调用?如果它在StatefulWidgetinitState中被调用,很可能不起作用,因为从_globalKey上下文中检索到的RenderObject尚未被绘制/绘画。 - Ovidiu
6个回答

27
你可以在这里找到官方的toImage示例。但是,似乎在按钮点击和toImage调用之间没有延迟就无法正常工作。
官方存储库中存在一个问题:https://github.com/flutter/flutter/issues/22308 造成这个问题的原因是:您的点击会为按钮初始化动画,并递归地调用RenderObject.markNeedsPaint包括父级,所以您应该等待debugNeedsPaint再次变为false。在这种情况下,toImage函数会抛出断言错误。
  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
        ),
      ),
    );
  }
}

RepaintBoundary( key: globalKey,) - xbadal

5
你的代码已经很好了,在发布模式下不应该遇到任何问题,因为从文档中可以看出:debugNeedsPaint:此渲染对象的绘制信息是否已过期。这仅在调试模式下设置。通常,渲染对象不应该根据其是否脏来决定运行时行为,因为它们只应在被布局和绘制之前被标记为脏。它旨在用于测试和asserts。 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,然后在一秒钟内字节将被打印到控制台。我的意思是,如果 (boundary.debugNeedsPaint) 为真,则计时器将在一秒钟后运行 _capturePng() 函数,然后返回 null - exxbrain
希望您能为我的解决方案投票)). 顺便说一下,我检查了您的代码。它按照我说的那样工作。 - exxbrain
@DenisZakharov 抱歉,我现在无法测试您的代码,但我可以看到您使用了我的逻辑。 - CopsOnRoad
await Future.delayed(const Duration(milliseconds: 20)); 是另一种逻辑,它也能正常工作。Flutter问题 https://github.com/flutter/flutter/issues/22308 的开发人员也使用了Timer,但他们的函数没有返回值。所以他们的解决方案也是有效的。但你的不行。很奇怪你没看出来。 - exxbrain
你的函数应该返回Future<Uint8List>,但在调试中返回了null。这就是问题所在。 - exxbrain
显示剩余4条评论

2

我已将返回空值的逻辑更改为再次调用函数_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;
  }
}

2

这是对已接受答案的重要补充。根据debugNeedsPaint文档,检查此标志会在发布模式下导致应用程序崩溃 (在发布版本中,会抛出异常。)

因此,我们只需要在调试模式下检查此标志,使用import 'package:flutter/foundation.dart';中的kDebugMode

    var debugNeedsPaint = false;

    //https://dev59.com/11UL5IYBdhLWcg3w1rFK
    //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();
    }

0
这是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,
    ),
    ]
    ,
    )
    ,
    )
    )
    ]))));
  }
}

0

我遇到了同样的问题

经过长时间的研究

我终于找到了问题所在

如果你想捕获的小部件在ListView中,并且ListView非常长,因此你的小部件超出了屏幕范围,那么捕获将失败。


1
我没有在任何地方使用ListView。 - xbadal
我正在使用堆栈。 - Rana Hyder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接