Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Build the widget off screen

Tags:

flutter

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.

like image 778
JoKr Avatar asked Oct 15 '22 12:10

JoKr


2 Answers

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
like image 132
JoKr Avatar answered Nov 12 '22 20:11

JoKr


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);
}
like image 24
Simon Avatar answered Nov 12 '22 19:11

Simon