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

11,773

Solution 1

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.

Solution 2

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

Solution 3

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

   )
)
Share:
11,773

Related videos on Youtube

Keshav
Author by

Keshav

Updated on June 04, 2022

Comments

  • Keshav
    Keshav almost 2 years

    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.

    • rmtmckenzie
      rmtmckenzie over 5 years
      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.
    • Zvi Karp
      Zvi Karp about 5 years
      Yes rmtmckenzie, SingleChildScrollView(child: RepaintBoundary(child: Column(...),),), does draw the entire list.
  • Lalit Fauzdar
    Lalit Fauzdar over 3 years
    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.
  • gvNN
    gvNN over 3 years
    What great news! Please, if you are having any trouble in this, feel free to comment bellow.
  • Arpit
    Arpit over 3 years
    Any way to get it to capture screenshot of only 1 item of the ListView rather than the complete ListView?
  • gvNN
    gvNN over 3 years
    @Arpit 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.
  • djalmafreestyler
    djalmafreestyler almost 3 years
    Is it possible to create a multi page pdf with this big screenshot?
  • gvNN
    gvNN almost 3 years
    @djalmafreestyler That's a good question, i haven't tried this before.
  • Mohit Arora
    Mohit Arora almost 3 years
    How can I achieve this using CustomScrollView With SliverList as a child?
  • user3249027
    user3249027 over 2 years
    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"...
  • Zero
    Zero over 2 years
    I think this is the easiest and most effective way