Take a ‘Screenshot’ Of a Certain Widget in Flutter
PROLOGUE
It’s been a long time since my last post about Flutter. Now I’m back. I’m happy that one of my articles is mentioned in Flutter’s official docs.
To be honest, it motivates me to start writing about Flutter again in my spare time.
THE IDEA
I’m interested on this comic remake page. It has a template comic where the balloon(s) can be filled with text the user wants to fill. Then the user can download it right away. I challenged myself to re-create that on Flutter. Let’s have some fun!
GET STARTED
From the original website above, I got some informations:
- The picture is square and its dimension is 800 x 800.
- There are some
coordinates
data for the input texts’ position. In above picture, there are 2 editable balloons. So there are two data:
[{"y":30.23333740234375,"x":410.16668701171875,"width":247,"height":106},{"y":472.23333740234375,"x":74.16668701171875,"width":178,"height":49}]
The problem is that the coordinate is following the picture’s original size. So I’ve to perform a downscale and keep the aspect ratio. Here, I prepare a class to hold the coordinates data:
class Datum {
final int height, width, x, y;
Datum({this.height, this.width, this.x, this.y});
}
Then, let’s create the main class. At the first attempt, I thought about using the Canvas
for this case. The idea was to draw the image and the inputted texts to the canvas. Sounds good, doesn’t it? But after more research, I came up with another simpler approach. There is a class in Flutter named RepaintBoundary
. Based on the documentation, it creates a separate display list for its child. We can make use of this to create a ‘duplicate’ of a widget. Here, I use the RepaintBoundary
and set a key to it.
class Remake extends StatefulWidget {
@override
_RemakeState createState() => _RemakeState();
}class _RemakeState extends State<Remake> {
GlobalKey previewContainer = new GlobalKey();
int originalSize = 800;
Image _image = Image.network(
"https://www.tahilalats.com/medias/c9befeb0-18c3-4344-a336-0db4996a76f4-2018-08-06-13-34-36-237895-800.jpeg",
);
Image _image2;
@override
Widget build(BuildContext context) {
double size = MediaQuery.of(context).size.width;
double factor = size / originalSize;
Datum data = Datum(
height: (106 * factor).floor(),
width: (247 * factor).floor(),
x: (410.16668701171875 * factor).floor(),
y: (30.23333740234375 * factor).floor(),
);
Datum data2 = Datum(
height: (49 * factor).floor(),
width: (178 * factor).floor(),
x: (74.16668701171875 * factor).floor(),
y: (472.23333740234375 * factor).floor(),
);
return Container(
width: size,
padding: EdgeInsets.only(top: 40.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
RepaintBoundary(
key: previewContainer,
child: Stack(
children: <Widget>[
_image,
Positioned(
top: data.y.toDouble() - 15,
left: data.x.toDouble(),
child: Container(
width: data.width.toDouble(),
height: data.height.toDouble(),
child: TextField(
maxLines: 3,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Write here..",
),
style: TextStyle(
color: Colors.black,
),
),
),
),
Positioned(
top: data2.y.toDouble() - 15,
left: data2.x.toDouble(),
child: Container(
width: data2.width.toDouble(),
height: data2.height.toDouble(),
child: TextField(
maxLines: 1,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Write here..",
),
style: TextStyle(
color: Colors.black,
),
),
),
),
],
),
),
RaisedButton(
child: Text('Download'),
onPressed: (){
FocusScope.of(context).requestFocus(FocusNode());
takeScreenShot();
},
),
_image2 != null ? _image2 : Container(),
],
),
),
);
}
I get the device’s width:
double size = MediaQuery.of(context).size.width;
Then, I get the scale ratio of the width compared to image’s original size:
double factor = size / originalSize;
Finally I perform the downscale to the coordinates
:
...
Datum data = Datum(
height: (106 * factor).floor(),
width: (247 * factor).floor(),
x: (410.16668701171875 * factor).floor(),
y: (30.23333740234375 * factor).floor(),
);
Datum data2 = Datum(
height: (49 * factor).floor(),
width: (178 * factor).floor(),
x: (74.16668701171875 * factor).floor(),
y: (472.23333740234375 * factor).floor(),
);
...
Later I apply the position to the corresponding widgets:
...
Positioned(
top: data.y.toDouble() - 15,
left: data.x.toDouble(),
child: Container(
width: data.width.toDouble(),
height: data.height.toDouble(),
child: TextField(
maxLines: 3,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Write here..",
),
style: TextStyle(
color: Colors.black,
),
),
),
),
Positioned(
top: data2.y.toDouble() - 15,
left: data2.x.toDouble(),
child: Container(
width: data2.width.toDouble(),
height: data2.height.toDouble(),
child: TextField(
maxLines: 1,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Write here..",
),
style: TextStyle(
color: Colors.black,
),
),
),
),
...
GENERATE AND DOWNLOAD THE IMAGE
I provide a button to generate and download the image. Word generate here means to ‘draw’ the _image
and the texts inputted to a variable named _image2
.
...
RaisedButton(
child: Text('Download'),
onPressed: (){
FocusScope.of(context).requestFocus(FocusNode());
takeScreenShot();
},
)
_image2 != null ? _image2 : Container(),_image2 != null ? _image2 : Container(),
...
Here is the method:
takeScreenShot() async {
RenderRepaintBoundary boundary =
previewContainer.currentContext.findRenderObject();
double pixelRatio = originalSize / MediaQuery.of(context).size.width;
ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
setState(() {
_image2 = Image.memory(pngBytes.buffer.asUint8List());
});
final directory = (await getApplicationDocumentsDirectory()).path;
File imgFile = new File('$directory/screenshot.png');
imgFile.writeAsBytes(pngBytes);
final snackBar = SnackBar(
content: Text('Saved to ${directory}'),
action: SnackBarAction(
label: 'Ok',
onPressed: () {
// Some code
},
),
);
Scaffold.of(context).showSnackBar(snackBar);
}
Thing to mention is that we have to also set the pixel ratio for the image’s quality.
...
double pixelRatio = originalSize / MediaQuery.of(context).size.width;
ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
...
To download the image, I need the path of the directory. This time, I use path_provider
package to help me.
final directory = (await getApplicationDocumentsDirectory()).path;
File imgFile = new File('$directory/screenshot.png');
After you press the button, you should see a new image under it. Plus, it’s automatically downloaded to your device.
NOTES
Since I’m using someone else’s website as an example, I won’t tell you how to get the coordinates
information. I also won’t make the example to be more dynamic. For instance, there are two hardcoded text fields in this example. In its real application, there can be various number of text fields, depend on the coordinates
provided by the application.
Thanks for reading this post. Have a good day!