How to take screenshot of widget beyond the screen in flutter?
Asked Answered
A

4

15

I am using RepaintBoundary to take the screenshot of the current widget which is a listView. But it only captures the content which is visible on the screen at the time.

RepaintBoundary(
                key: src,
                child: ListView(padding: EdgeInsets.only(left: 10.0),
                  scrollDirection: Axis.horizontal,
                  children: <Widget>[
                    Align(
                        alignment: Alignment(-0.8, -0.2),
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: listLabel(orientation),
                        )
                    ),

                    Padding(padding: EdgeInsets.all(5.0)),

                    Align(
                        alignment: FractionalOffset(0.3, 0.5),
                        child: Container(
                            height: orientation == Orientation.portrait? 430.0: 430.0*0.7,
                            decoration: BoxDecoration(
                                border: Border(left: BorderSide(color: Colors.black))
                            ),
                            //width: 300.0,
                            child:
                            Wrap(
                              direction: Axis.vertical,
                              //runSpacing: 10.0,
                              children: colWidget(orientation),
                            )
                        )
                    ),
                    Padding(padding: EdgeInsets.all(5.0)),
                    Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: listLabel(orientation),
                    )
                  ],
                ),
              );

screenshot function:

Future screenshot() async {
    RenderRepaintBoundary boundary = src.currentContext.findRenderObject();
    ui.Image image = await boundary.toImage();
    ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    Uint8List pngBytes = byteData.buffer.asUint8List();
    print(pngBytes);
    final directory = (await getExternalStorageDirectory()).path;
File imgFile =new File('$directory/layout2.pdf');
imgFile.writeAsBytes(pngBytes);
  }

Is there any way, so that I can capture the whole listView, i.e., not only the content which is not visible on the screen but the scrollable content also. Or maybe if the whole widget is too large to fit in a picture, it can be captured in multiple images.

Ataxia answered 6/12, 2018 at 7:40 Comment(2)
I don't think this is possible with the current implementation of ListView. ListView does a ton of optimization under the hood so that it only draws the objects currently on the screen as drawing out the entire list would be a huge waste of resources and could cause frame drops. I don't know for sure if it would work, but if you really need to do this you could try using a SingleChildScrollView with a RepaintBoundary and Column as that might actually draw out the entire list... but I'm still not sure it would.Drava
Yes rmtmckenzie, SingleChildScrollView(child: RepaintBoundary(child: Column(...),),), does draw the entire list.Sigler
D
12

This made me curious whether it was possible so I made a quick mock-up that shows it does work. But please be aware that by doing this you're essentially intentionally breaking the things flutter does to optimize, so you really shouldn't use it beyond where you absolutely have to.

Anyways, here's the code:

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class UiImagePainter extends CustomPainter {
  final ui.Image image;

  UiImagePainter(this.image);

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // simple aspect fit for the image
    var hr = size.height / image.height;
    var wr = size.width / image.width;

    double ratio;
    double translateX;
    double translateY;
    if (hr < wr) {
      ratio = hr;
      translateX = (size.width - (ratio * image.width)) / 2;
      translateY = 0.0;
    } else {
      ratio = wr;
      translateX = 0.0;
      translateY = (size.height - (ratio * image.height)) / 2;
    }

    canvas.translate(translateX, translateY);
    canvas.scale(ratio, ratio);
    canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
  }

  @override
  bool shouldRepaint(UiImagePainter other) {
    return other.image != image;
  }
}

class UiImageDrawer extends StatelessWidget {
  final ui.Image image;

  const UiImageDrawer({Key key, this.image}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.infinite,
      painter: UiImagePainter(image),
    );
  }
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  GlobalKey<OverRepaintBoundaryState> globalKey = GlobalKey();

  ui.Image image;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: image == null
            ? Capturer(
                overRepaintKey: globalKey,
              )
            : UiImageDrawer(image: image),
        floatingActionButton: image == null
            ? FloatingActionButton(
                child: Icon(Icons.camera),
                onPressed: () async {
                  var renderObject = globalKey.currentContext.findRenderObject();

                  RenderRepaintBoundary boundary = renderObject;
                  ui.Image captureImage = await boundary.toImage();
                  setState(() => image = captureImage);
                },
              )
            : FloatingActionButton(
                onPressed: () => setState(() => image = null),
                child: Icon(Icons.remove),
              ),
      ),
    );
  }
}

class Capturer extends StatelessWidget {
  static final Random random = Random();

  final GlobalKey<OverRepaintBoundaryState> overRepaintKey;

  const Capturer({Key key, this.overRepaintKey}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: OverRepaintBoundary(
        key: overRepaintKey,
        child: RepaintBoundary(
          child: Column(
            children: List.generate(
              30,
              (i) => Container(
                    color: Color.fromRGBO(random.nextInt(256), random.nextInt(256), random.nextInt(256), 1.0),
                    height: 100,
                  ),
            ),
          ),
        ),
      ),
    );
  }
}

class OverRepaintBoundary extends StatefulWidget {
  final Widget child;

  const OverRepaintBoundary({Key key, this.child}) : super(key: key);

  @override
  OverRepaintBoundaryState createState() => OverRepaintBoundaryState();
}

class OverRepaintBoundaryState extends State<OverRepaintBoundary> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

What it's doing is making a scroll view that encapsulates the list (column), and making sure the repaintBoundary is around the column. With your code where you use a list, there's no way it can ever capture all the children as the list is essentially a repaintBoundary in and of itself.

Note in particular the 'overRepaintKey' and OverRepaintBoundary. You might be able to get away without using it by iterating through render children, but it makes it a lot easier.

Drava answered 6/12, 2018 at 19:18 Comment(1)
Hey @rmtmckenzie, found your answer while still looking for a solution for my problem (described here) and hoped you could give me a tipp..? :) Every child of a ListView seems to be its own RepaintBoundary - could that be correct? I'm trying to imagefilter.blur parts of the children of a Listview.builder "togeter"...Moulin
P
15

I achieve the solution of this problem using this package: Screenshot, that takes a screenshot of the entire widget. It's easy and simple, follow the steps on the PubDev or GitHub and you can make it work.

OBS: To take a full screenshot of the widget make sure that your widget is fully scrollable, and not just a part of it.

(In my case, i had a ListView inside a Container, and the package doesn't take the screenshot of all ListView because i have many itens on it, SO i have wrap my Container inside a SingleChildScrollView and add the NeverScrollableScrollPhysics physics in the ListView and it works! :D). Screenshot of my screen

description

More details in this issue

Perpetrate answered 26/9, 2019 at 15:29 Comment(7)
Thank you very much for the last minute save. Although, I didn't really understood that the ScrollView should be the parent of the ScreenShot widget because in my case it was already the child but that Github issue helped me.Baese
What great news! Please, if you are having any trouble in this, feel free to comment bellow.Perpetrate
Any way to get it to capture screenshot of only 1 item of the ListView rather than the complete ListView?Korman
@Korman Yes, but you may change a little bit of the logic mentioned here. If you wanna take a screenshot of a Card in a list, for example, you may add the Widget in every single item on your list, and add some logic to control which Card will be screenshoted.. but i think in terms of performance this not sound the best.Perpetrate
Is it possible to create a multi page pdf with this big screenshot?Mucky
@Mucky That's a good question, i haven't tried this before.Perpetrate
How can I achieve this using CustomScrollView With SliverList as a child?Hypercatalectic
D
12

This made me curious whether it was possible so I made a quick mock-up that shows it does work. But please be aware that by doing this you're essentially intentionally breaking the things flutter does to optimize, so you really shouldn't use it beyond where you absolutely have to.

Anyways, here's the code:

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class UiImagePainter extends CustomPainter {
  final ui.Image image;

  UiImagePainter(this.image);

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // simple aspect fit for the image
    var hr = size.height / image.height;
    var wr = size.width / image.width;

    double ratio;
    double translateX;
    double translateY;
    if (hr < wr) {
      ratio = hr;
      translateX = (size.width - (ratio * image.width)) / 2;
      translateY = 0.0;
    } else {
      ratio = wr;
      translateX = 0.0;
      translateY = (size.height - (ratio * image.height)) / 2;
    }

    canvas.translate(translateX, translateY);
    canvas.scale(ratio, ratio);
    canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
  }

  @override
  bool shouldRepaint(UiImagePainter other) {
    return other.image != image;
  }
}

class UiImageDrawer extends StatelessWidget {
  final ui.Image image;

  const UiImageDrawer({Key key, this.image}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.infinite,
      painter: UiImagePainter(image),
    );
  }
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  GlobalKey<OverRepaintBoundaryState> globalKey = GlobalKey();

  ui.Image image;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: image == null
            ? Capturer(
                overRepaintKey: globalKey,
              )
            : UiImageDrawer(image: image),
        floatingActionButton: image == null
            ? FloatingActionButton(
                child: Icon(Icons.camera),
                onPressed: () async {
                  var renderObject = globalKey.currentContext.findRenderObject();

                  RenderRepaintBoundary boundary = renderObject;
                  ui.Image captureImage = await boundary.toImage();
                  setState(() => image = captureImage);
                },
              )
            : FloatingActionButton(
                onPressed: () => setState(() => image = null),
                child: Icon(Icons.remove),
              ),
      ),
    );
  }
}

class Capturer extends StatelessWidget {
  static final Random random = Random();

  final GlobalKey<OverRepaintBoundaryState> overRepaintKey;

  const Capturer({Key key, this.overRepaintKey}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: OverRepaintBoundary(
        key: overRepaintKey,
        child: RepaintBoundary(
          child: Column(
            children: List.generate(
              30,
              (i) => Container(
                    color: Color.fromRGBO(random.nextInt(256), random.nextInt(256), random.nextInt(256), 1.0),
                    height: 100,
                  ),
            ),
          ),
        ),
      ),
    );
  }
}

class OverRepaintBoundary extends StatefulWidget {
  final Widget child;

  const OverRepaintBoundary({Key key, this.child}) : super(key: key);

  @override
  OverRepaintBoundaryState createState() => OverRepaintBoundaryState();
}

class OverRepaintBoundaryState extends State<OverRepaintBoundary> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

What it's doing is making a scroll view that encapsulates the list (column), and making sure the repaintBoundary is around the column. With your code where you use a list, there's no way it can ever capture all the children as the list is essentially a repaintBoundary in and of itself.

Note in particular the 'overRepaintKey' and OverRepaintBoundary. You might be able to get away without using it by iterating through render children, but it makes it a lot easier.

Drava answered 6/12, 2018 at 19:18 Comment(1)
Hey @rmtmckenzie, found your answer while still looking for a solution for my problem (described here) and hoped you could give me a tipp..? :) Every child of a ListView seems to be its own RepaintBoundary - could that be correct? I'm trying to imagefilter.blur parts of the children of a Listview.builder "togeter"...Moulin
A
9

There is a simple way You need wrap SingleChildScrollView Widget to RepaintBoundary. just wrap your Scrollable widget (or his father) with SingleChildScrollView

SingleChildScrollView(
  child: RepaintBoundary(
     key: _globalKey

   )
)
Answerable answered 15/10, 2020 at 15:7 Comment(1)
I think this is the easiest and most effective wayIrreducible
H
0

I Got this solution for taking horizontal Screenshot using:- "Screenshot" Pub libraby and use this code for taking screenshot:-

 body: Screenshot(
    controller: _screenshotController,
    child: SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: RepaintBoundary(
        key: _screenshotKey,
        child: SizedBox(
          width: 810.0, // Set a fixed width or use a sized container
          child: ListView.builder(
            //physics: NeverScrollableScrollPhysics(),
            shrinkWrap: true,
            itemCount: reportData.length,
            itemBuilder: (context, reportIndex) {
Hatchery answered 18/12, 2023 at 10:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.