infinite_canvas 0.0.2 copy "infinite_canvas: ^0.0.2" to clipboard
infinite_canvas: ^0.0.2 copied to clipboard

InteractiveViewer + CustomMultiChildLayout

Infinite Canvas #

infinite_canvas

Flutter infinite canvas that can be zoomed and panned.

There is also a menu for common actions and marquee for multiple selection.

Example #

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const Home(),
      theme: ThemeData.light(useMaterial3: true),
      darkTheme: ThemeData.dark(useMaterial3: true),
      themeMode: ThemeMode.system,
    );
  }
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  late InfiniteCanvasController controller;
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    final nodes = [
      InfiniteCanvasNode(
        label: 'Flutter Counter',
        key: UniqueKey(),
        offset: Offset.zero,
        size: const Size(400, 800),
        builder: (context) => StatefulBuilder(
          builder: (context, setState) => Scaffold(
            appBar: AppBar(
              title: const Text('Flutter Demo Home Page'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  const Text(
                    'You have pushed the button this many times:',
                  ),
                  Text(
                    '$_counter',
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                if (mounted) {
                  setState(() {
                    _counter++;
                  });
                }
              },
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
          ),
        ),
      ),
      InfiniteCanvasNode(
        key: UniqueKey(),
        offset: const Offset(600, 100),
        size: const Size(400, 800),
        builder: (context) => Scaffold(
          appBar: AppBar(
            title: const Text('Gradient'),
          ),
          body: Container(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Theme.of(context).colorScheme.primary,
                  Theme.of(context).colorScheme.secondary,
                  Theme.of(context).colorScheme.tertiary,
                ],
              ),
            ),
          ),
        ),
      ),
      InfiniteCanvasNode(
        key: UniqueKey(),
        label: 'Rectangle',
        offset: const Offset(400, 300),
        size: const Size(200, 200),
        builder: (context) => CustomPaint(
          isComplex: true,
          willChange: true,
          painter: InlineCustomPainter(
            brush: Paint(),
            builder: (brush, canvas, rect) {
              // Draw rect
              brush.color = Theme.of(context).colorScheme.secondary;
              canvas.drawRect(rect, brush);
            },
          ),
        ),
      ),
      InfiniteCanvasNode(
        key: UniqueKey(),
        label: 'Triangle',
        offset: const Offset(550, 300),
        size: const Size(200, 200),
        builder: (context) => CustomPaint(
          painter: InlineCustomPainter(
            brush: Paint(),
            builder: (brush, canvas, rect) {
              // Draw triangle
              brush.color = Theme.of(context).colorScheme.secondaryContainer;
              final path = Path();
              path.addPolygon([
                rect.topCenter,
                rect.bottomLeft,
                rect.bottomRight,
              ], true);
              canvas.drawPath(path, brush);
            },
          ),
        ),
      ),
      InfiniteCanvasNode(
        key: UniqueKey(),
        label: 'Circle',
        offset: const Offset(500, 450),
        size: const Size(200, 200),
        builder: (context) => CustomPaint(
          painter: InlineCustomPainter(
            brush: Paint(),
            builder: (brush, canvas, rect) {
              // Draw circle
              brush.color = Theme.of(context).colorScheme.tertiary;
              canvas.drawCircle(rect.center, rect.width / 2, brush);
            },
          ),
        ),
      ),
    ];
    controller = InfiniteCanvasController(nodes: nodes, edges: [
      InfiniteCanvasEdge(
        from: nodes[2].key!,
        to: nodes[3].key!,
        label: 'Edge 1',
      ),
      InfiniteCanvasEdge(
        from: nodes[2].key!,
        to: nodes[4].key!,
        label: 'Edge 1',
      ),
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Canvas Example'),
        centerTitle: false,
      ),
      body: InfiniteCanvas(
        controller: controller,
      ),
    );
  }
}

class InlineCustomPainter extends CustomPainter {
  const InlineCustomPainter({
    required this.brush,
    required this.builder,
    this.isAntiAlias = true,
  });
  final Paint brush;
  final bool isAntiAlias;
  final void Function(Paint paint, Canvas canvas, Rect rect) builder;

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Offset.zero & size;
    brush.isAntiAlias = isAntiAlias;
    canvas.save();
    builder(brush, canvas, rect);
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}