xbadal
xbadal

Reputation: 1375

Unable to take screenshot in flutter

package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 2813 pos 12: '!debugNeedsPaint': is not true.

I am trying to take screenshot in flutter, but I am getting exception. I visited many links but nothing worked.

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);
    }
}

Upvotes: 23

Views: 12156

Answers (6)

sanchit
sanchit

Reputation: 179

Here's the solution for Flutter 2.0+ method for screenshot and share it on social media.

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,
    ),
    ]
    ,
    )
    ,
    )
    )
    ]))));
  }
}

Upvotes: 0

adrianvintu
adrianvintu

Reputation: 1425

This is an important addition to the accepted answer. As per documentation of debugNeedsPaint, checking this flag in release mode will crash the app (In release builds, this throws.)

So we need to only check this flag in debug mode, using kDebugMode from import 'package:flutter/foundation.dart';

    var debugNeedsPaint = false;

    //https://stackoverflow.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();
    }

Upvotes: 4

user3044484
user3044484

Reputation: 513

I have face the same issue

After a long period of research

I finally find the problem

if the widget you want to capture is in a ListView,and the listview is very long,so your widget is out of screen,the capture will faild.

Upvotes: 1

Adam Rabbani
Adam Rabbani

Reputation: 21

I have changed logic of return from null to function _capturePng() again.

Reff: @CopsOnRoad

Check the below code

    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;
  }
}

Upvotes: 2

exxbrain
exxbrain

Reputation: 636

You can find official toImage example here. But it looks like it doesn't work without delay between button tap and toImage call.

There is an issue in the official repository: https://github.com/flutter/flutter/issues/22308

The cause for this: your tap initializes animation for a button and RenderObject.markNeedsPaint gets called recursively including parents, so you should wait while debugNeedsPaint will be false again. toImage function just throws assertion error in this case:

  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

Actually assert function is used only in development, so as you can see you will not have the trouble with the error in production. But I don't know what kind of troubles you can get instead, probably no ).

The next code is not great but it works:

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
        ),
      ),
    );
  }
}

Upvotes: 29

CopsOnRoad
CopsOnRoad

Reputation: 268394

Your code is already fine, you shouldn't face any problem in release mode because from docs:

debugNeedsPaint: Whether this render object's paint information is dirty.

This is only set in debug mode. In general, render objects should not need to condition their runtime behavior on whether they are dirty or not, since they should only be marked dirty immediately prior to being laid out and painted.

It is intended to be used by tests and asserts.

It is possible (and indeed, quite common) for debugNeedsPaint to be false and debugNeedsLayout to be true. The render object will still be repainted in the next frame when this is the case, because the markNeedsPaint method is implicitly called by the framework after a render object is laid out, prior to the paint phase.

However, if you still need a solution, you can try this:

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;
  }
}

Upvotes: 5

Related Questions