widget_snap 1.2.0 copy "widget_snap: ^1.2.0" to clipboard
widget_snap: ^1.2.0 copied to clipboard

Export any widget to PNG — rendered offscreen, even content larger than the screen. Pure Flutter render pipeline, zero dependencies, all platforms.

example/lib/main.dart

import 'dart:typed_data';

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

void main() => runApp(const DemoApp());

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: DemoScreen(), debugShowCheckedModeBanner: false);
}

/// Captures an Ink-heavy widget offscreen on first frame and shows the result
/// side by side with a live copy, so a blank capture is immediately visible.
class DemoScreen extends StatefulWidget {
  const DemoScreen({super.key});
  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  Uint8List? _bytes; // width-pinned: tall, narrow content
  Uint8List? _wideBytes; // height-pinned: wide, short content
  Uint8List? _transparentBytes; // transparent backgroundColor → alpha channel
  Object? _error;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      try {
        // Pin one axis; the other grows to fit the content.
        //   width + height together → fixed box (content clips if larger)
        //   neither      → defaults to the current view's width
        // pixelRatio (default 2.5) is the raster scale, clamped (never below
        // 1.0) so no output axis exceeds the ~4096px GPU cap. delay: lets
        // async images (network/asset) resolve before capture — prefer
        // `await precacheImage(...)` for a deterministic wait.
        // backgroundColor (default white) fills behind bare content — use
        // Colors.transparent for alpha.

        // Tall + narrow → pin the width, height auto-fits.
        final b = await _content().toPngBytes(context, width: 320);
        if (!mounted) return;
        // Wide + short (a chart) → pin the height, width auto-fits.
        // Tint the canvas so the bare bars read against a card, not white.
        final w = await _wideContent().toPngBytes(
          context,
          height: 140,
          backgroundColor: const Color(0xFFF2F4FF),
        );
        if (!mounted) return;
        // Transparent canvas → PNG with an alpha channel, so whatever it's
        // composited over shows through (proven below by the orange backdrop).
        final t = await _content().toPngBytes(
          context,
          width: 320,
          backgroundColor: Colors.transparent,
        );
        if (!mounted) return;
        setState(() {
          _bytes = b;
          _wideBytes = w;
          _transparentBytes = t;
        });
      } catch (e) {
        if (mounted) setState(() => _error = e);
      }
    });
  }

  /// Wide, short content: a bar chart with no intrinsic width constraint —
  /// the case that motivated pinning height instead of width.
  Widget _wideContent() => Row(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.end,
    children: [
      for (final h in const [50.0, 95.0, 65.0, 110.0, 40.0, 85.0, 70.0, 100.0])
        Container(
          width: 28,
          height: h,
          margin: const EdgeInsets.symmetric(horizontal: 4),
          color: const Color(0xFF2A5BD7),
        ),
    ],
  );

  Widget _content() => Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      InkWell(
        onTap: () {},
        child: Ink(
          width: 280,
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: const Color(0xFF2A5BD7),
            borderRadius: BorderRadius.circular(14),
          ),
          child: const Text(
            'Ink card — blue box, white text',
            style: TextStyle(color: Colors.white),
          ),
        ),
      ),
      const SizedBox(height: 10),
      Container(width: 280, height: 12, color: const Color(0xFF23A047)),
      const SizedBox(height: 10),
      Container(width: 120, height: 40, color: const Color(0xFFCC3344)),
    ],
  );

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('widget_snap demo')),
    body: SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('LIVE:'),
          _content(),
          const Divider(height: 32),
          const Text('CAPTURED (width: 320 — height grows):'),
          if (_error != null) Text('ERROR: $_error'),
          if (_bytes == null && _error == null) const Text('capturing…'),
          if (_bytes != null)
            Container(
              color: const Color(0xFFEEEEEE),
              // Show at the captured logical width for a 1:1 comparison.
              child: Image.memory(_bytes!, width: 320),
            ),
          const Divider(height: 32),
          const Text('CAPTURED (height: 140 — width grows):'),
          if (_wideBytes != null)
            // Wide capture: scroll horizontally; show at captured logical height.
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Container(
                color: const Color(0xFFEEEEEE),
                child: Image.memory(_wideBytes!, height: 140),
              ),
            ),
          const Divider(height: 32),
          const Text('CAPTURED (backgroundColor: transparent):'),
          if (_transparentBytes != null)
            // Orange backdrop shows through the transparent PNG's alpha.
            Container(
              color: const Color(0xFFFF8A34),
              padding: const EdgeInsets.all(12),
              child: Image.memory(_transparentBytes!, width: 320),
            ),
        ],
      ),
    ),
  );
}
0
likes
160
points
13
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Export any widget to PNG — rendered offscreen, even content larger than the screen. Pure Flutter render pipeline, zero dependencies, all platforms.

Repository (GitHub)
View/report issues

Topics

#screenshot #widget-to-image #png #export #capture

License

MIT (license)

Dependencies

flutter

More

Packages that depend on widget_snap