无法在颤动中截取屏幕截图
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);
【问题讨论】:
这个函数在哪里调用?如果在StatefulWidget
的 initState
内调用它很可能不起作用,因为您从 _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), () => _capturePng()); return null;
@DenisZakharov 不会这样的,试一试,告诉我。
你的Timer
异步调度,回调会在_capturePng
返回null后一秒执行。请检查我的解决方案,我相信你会明白我的意思。
我希望你会投票支持我的解决方案))。顺便说一句,我检查了您的代码。正如我所说的那样工作。
@DenisZakharov 抱歉,我目前无法测试您的代码,但我可以看到您使用了我的逻辑【参考方案2】:
您可以找到官方toImage
示例here。但看起来它在按钮点击和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
),
),
);
【讨论】:
重绘边界(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,
),
]
,
)
,
)
)
]))));
【讨论】:
以上是关于无法在颤动中截取屏幕截图的主要内容,如果未能解决你的问题,请参考以下文章