flutter-infinite-canvas

Flutter Infinite Canvas

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "flutter-infinite-canvas" with this command: npx skills add rodydavis/skills/rodydavis-skills-flutter-infinite-canvas

Flutter Infinite Canvas

Overview 

The following is an example of how to build an infinite canvas with InteractiveViewer and CustomMultiChildLayout.

Blog post: Create a multi touch canvas in Flutter

import 'package:flutter/material.dart'; import 'package:vector_math/vector_math_64.dart' hide Colors;

void main() { final controller = WidgetCanvasController([ WidgetCanvasChild( key: UniqueKey(), offset: Offset.zero, size: const Size(400, 800), child: Scaffold( appBar: AppBar( title: const Text('Scaffold 1'), ), body: const Placeholder(), ), ), WidgetCanvasChild( key: UniqueKey(), offset: const Offset(200, 200), size: const Size(400, 800), child: Scaffold( appBar: AppBar( title: const Text('Scaffold 2'), ), body: const Placeholder(), ), ), ]); runApp(MaterialApp(home: WidgetCanvas(controller: controller))); }

class WidgetCanvas extends StatefulWidget { const WidgetCanvas({super.key, required this.controller});

final WidgetCanvasController controller;

@override State<WidgetCanvas> createState() => WidgetCanvasState(); }

class WidgetCanvasState extends State<WidgetCanvas> { @override void initState() { super.initState(); controller.addListener(onUpdate); }

@override void dispose() { controller.removeListener(onUpdate); super.dispose(); }

void onUpdate() { if (mounted) setState(() {}); }

static const Size _gridSize = Size.square(50); WidgetCanvasController get controller => widget.controller;

Rect axisAlignedBoundingBox(Quad quad) { double xMin = quad.point0.x; double xMax = quad.point0.x; double yMin = quad.point0.y; double yMax = quad.point0.y; for (final Vector3 point in <Vector3>[ quad.point1, quad.point2, quad.point3, ]) { if (point.x < xMin) { xMin = point.x; } else if (point.x > xMax) { xMax = point.x; }

  if (point.y &#x3C; yMin) {
    yMin = point.y;
  } else if (point.y > yMax) {
    yMax = point.y;
  }
}

return Rect.fromLTRB(xMin, yMin, xMax, yMax);

}

@override Widget build(BuildContext context) { const inset = 2.0; return Listener( onPointerDown: (details) { controller.mouseDown = true; controller.checkSelection(details.localPosition); }, onPointerUp: (details) { controller.mouseDown = false; }, onPointerCancel: (details) { controller.mouseDown = false; }, onPointerMove: (details) {}, child: LayoutBuilder( builder: (context, constraints) => InteractiveViewer.builder( transformationController: controller.transform, panEnabled: controller.canvasMoveEnabled, scaleEnabled: controller.canvasMoveEnabled, onInteractionStart: (details) { controller.mousePosition = details.focalPoint; }, onInteractionUpdate: (details) { if (!controller.mouseDown) { controller.scale = details.scale; } else { controller.moveSelection(details.focalPoint); } controller.mousePosition = details.focalPoint; }, onInteractionEnd: (details) {}, minScale: 0.4, maxScale: 4, boundaryMargin: const EdgeInsets.all(double.infinity), builder: (context, viewport) { return SizedBox( width: 1, height: 1, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: GridBackgroundBuilder( cellWidth: _gridSize.width, cellHeight: _gridSize.height, viewport: axisAlignedBoundingBox(viewport), ), ), Positioned.fill( child: CustomMultiChildLayout( delegate: WidgetCanvasDelegate(controller), children: controller.children.map((e) { return LayoutId( id: e, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Material( elevation: 4, child: SizedBox.fromSize( size: e.size, child: e.child, ), ), ), if (controller.isSelected(e.key!)) Positioned.fill( top: -inset, left: -inset, right: -inset, bottom: -inset, child: Container( decoration: BoxDecoration( border: Border.all( color: Colors.blue, width: 1, ), ), ), ), ], )); }).toList(), ), ), ], ), ); }, ), ), ); } }

class GridBackgroundBuilder extends StatelessWidget { const GridBackgroundBuilder({ super.key, required this.cellWidth, required this.cellHeight, required this.viewport, });

final double cellWidth; final double cellHeight; final Rect viewport;

@override Widget build(BuildContext context) { final int firstRow = (viewport.top / cellHeight).floor(); final int lastRow = (viewport.bottom / cellHeight).ceil(); final int firstCol = (viewport.left / cellWidth).floor(); final int lastCol = (viewport.right / cellWidth).ceil();

return Stack(
  clipBehavior: Clip.none,
  children: &#x3C;Widget>[
    for (int row = firstRow; row &#x3C; lastRow; row++)
      for (int col = firstCol; col &#x3C; lastCol; col++)
        Positioned(
          left: col * cellWidth,
          top: row * cellHeight,
          child: Container(
            height: cellHeight,
            width: cellWidth,
            decoration: BoxDecoration(
              border: Border.all(
                color: Colors.grey.withOpacity(0.1),
                width: 1,
              ),
            ),
          ),
        ),
  ],
);

} }

class WidgetCanvasDelegate extends MultiChildLayoutDelegate { WidgetCanvasDelegate(this.controller); final WidgetCanvasController controller; List<WidgetCanvasChild> get children => controller.children;

Size backgroundSize = const Size(100000, 100000); late Offset backgroundOffset = Offset( -backgroundSize.width / 2, -backgroundSize.height / 2, );

@override void performLayout(Size size) { // Then draw the screens. for (final widget in children) { layoutChild(widget, BoxConstraints.tight(widget.size)); positionChild(widget, widget.offset); } }

@override bool shouldRelayout(WidgetCanvasDelegate oldDelegate) => true; }

class WidgetCanvasChild extends StatelessWidget { const WidgetCanvasChild({ required Key key, required this.size, required this.offset, required this.child, }) : super(key: key);

final Size size; final Offset offset; final Widget child;

Rect get rect => offset & size;

WidgetCanvasChild copyWith({ Size? size, Offset? offset, Widget? child, }) { return WidgetCanvasChild( key: key!, size: size ?? this.size, offset: offset ?? this.offset, child: child ?? this.child, ); }

@override Widget build(BuildContext context) { return child; } }

class WidgetCanvasController extends ChangeNotifier { WidgetCanvasController(this.children);

final List<WidgetCanvasChild> children; final Set<Key> _selected = {}; late final transform = TransformationController(); Matrix4 get matrix => transform.value; double scale = 1; Offset mousePosition = Offset.zero;

bool _mouseDown = false; bool get mouseDown => _mouseDown; set mouseDown(bool value) { _mouseDown = value; notifyListeners(); }

bool isSelected(Key key) => _selected.contains(key);

bool get hasSelection => _selected.isNotEmpty;

bool get canvasMoveEnabled => !mouseDown;

Offset toLocal(Offset global) { return transform.toScene(global); }

void checkSelection(Offset localPosition) { final offset = toLocal(localPosition); final selection = <Key>[]; for (final child in children) { final rect = child.rect; if (rect.contains(offset)) { selection.add(child.key!); } } if (selection.isNotEmpty) { setSelection({selection.last}); } else { deselectAll(); } }

void moveSelection(Offset position) { final delta = toLocal(position) - toLocal(mousePosition); for (final key in _selected) { final index = children.indexWhere((e) => e.key == key); if (index == -1) continue; final current = children[index]; children[index] = current.copyWith( offset: current.offset + delta, ); } mousePosition = position; notifyListeners(); }

void select(Key key) { _selected.add(key); notifyListeners(); }

void setSelection(Set<Key> keys) { _selected.clear(); _selected.addAll(keys); notifyListeners(); }

void deselect(Key key) { _selected.remove(key); notifyListeners(); }

void deselectAll() { _selected.clear(); notifyListeners(); }

void add(WidgetCanvasChild child) { children.add(child); notifyListeners(); }

void remove(Key key) { children.removeWhere((e) => e.key == key); notifyListeners(); } }

Demo

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

flutter-control-and-screenshot

No summary provided by upstream source.

Repository SourceNeeds Review
General

install-flutter-from-git

No summary provided by upstream source.

Repository SourceNeeds Review
General

how-to-build-a-native-cross-platform-project-with-flutter

No summary provided by upstream source.

Repository SourceNeeds Review
General

how-to-build-a-webrtc-signal-server-with-pocketbase

No summary provided by upstream source.

Repository SourceNeeds Review