I need to build a widget just to get its bitmap. I don't care about widget being on the screen.
So my question is: can I somehow build the widget "on the side" without using the screen view hierarchy?
I didn't find a way to do it. So, if that's not possible can I build the widget on the screen, but not actually show it.
I've tried Visibility
but that will make RenderObject
null. With Offstage
it would fail when calling toImage()
on assertion: Failed assertion: line 2752 pos 12: ‘!debugNeedsPaint’: is not true.
EDIT: It looks like this broke in a recent version of Flutter. Not sure why, but I guess that Flutter will now avoid drawing when it determines that overlay will not be visible at all. This method can still be used, but it needs to combine with translate to move it offscreen: https://gist.github.com/itsJoKr/ce5ec57bd6dedf74d1737c1f39481913
Some people recommended using OverlayEntry
and it looks like the best solution.
You can put OverlayEntry
below the current screen so it's not visible and with maintainState: true
it will be built.
A big advantage is that it's easier to implement as it doesn't mix with the current widget tree.
OverlayState overlayState = Overlay.of(context);
OverlayEntry entry = OverlayEntry(builder: (context) {
return RepaintBoundary(key: key, child: yourWidget,); // Using RepaintBoundary to get RenderObject and convert to image
}, maintainState: true);
overlayState.insert(entry);
// doesn't work anymore
// overlayState.rearrange([entry], above: entry); // Didn't find how to insert it at the bottom of current overlays, so this should rearrange it so that our entry is at the bottom
This should do the job. We create an area the size of the screen.. but (off the screen) so it is still able to be captured as part of the tree.
Can also be found here: https://gist.github.com/slightfoot/8eeadd8028c373df87f3a47bd4a35e36
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
accentColor: Colors.pinkAccent,
),
home: ExampleScreen(),
),
);
}
class ExampleScreen extends StatefulWidget {
@override
_ExampleScreenState createState() => new _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
final _captureKey = GlobalKey<CaptureWidgetState>();
Future<CaptureResult> _image;
void _onCapturePressed() {
setState(() {
_image = _captureKey.currentState.captureImage();
});
}
@override
Widget build(BuildContext context) {
return CaptureWidget(
key: _captureKey,
capture: Material(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'These widgets are not visible on the screen yet can still be captured by a RepaintBoundary.',
),
SizedBox(height: 12.0),
Container(
width: 25.0,
height: 25.0,
color: Colors.red,
),
],
),
),
),
child: Scaffold(
appBar: AppBar(
title: Text('Widget To Image Demo'),
),
body: FutureBuilder<CaptureResult>(
future: _image,
builder: (BuildContext context, AsyncSnapshot<CaptureResult> snapshot) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: RaisedButton(
child: Text('Capture Image'),
onPressed: _onCapturePressed,
),
),
if (snapshot.connectionState == ConnectionState.waiting)
Center(
child: CircularProgressIndicator(),
)
else if (snapshot.hasData) ...[
Text(
'${snapshot.data.width} x ${snapshot.data.height}',
textAlign: TextAlign.center,
),
Container(
margin: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 2.0),
),
child: Image.memory(
snapshot.data.data,
scale: MediaQuery.of(context).devicePixelRatio,
),
),
],
],
),
);
},
),
),
);
}
}
class CaptureWidget extends StatefulWidget {
final Widget child;
final Widget capture;
const CaptureWidget({
Key key,
this.capture,
this.child,
}) : super(key: key);
@override
CaptureWidgetState createState() => CaptureWidgetState();
}
class CaptureWidgetState extends State<CaptureWidget> {
final _boundaryKey = GlobalKey();
Future<CaptureResult> captureImage() async {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final boundary = _boundaryKey.currentContext.findRenderObject() as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: pixelRatio);
final data = await image.toByteData(format: ui.ImageByteFormat.png);
return CaptureResult(data.buffer.asUint8List(), image.width, image.height);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final height = constraints.maxHeight * 2;
return OverflowBox(
alignment: Alignment.topLeft,
minHeight: height,
maxHeight: height,
child: Column(
children: <Widget>[
Expanded(
child: widget.child,
),
Expanded(
child: Center(
child: RepaintBoundary(
key: _boundaryKey,
child: widget.capture,
),
),
),
],
),
);
},
);
}
}
class CaptureResult {
final Uint8List data;
final int width;
final int height;
const CaptureResult(this.data, this.width, this.height);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With