interactive_viewer_vector 0.1.3 copy "interactive_viewer_vector: ^0.1.3" to clipboard
interactive_viewer_vector: ^0.1.3 copied to clipboard

Fork of the Flutter SDK InteractiveViewer that updates the RenderTransform directly (markNeedsPaint) instead of calling setState on every pan/zoom frame. No widget tree rebuild during pan/zoom interactions.

example/lib/main.dart

import "package:flutter/material.dart";
import "package:interactive_viewer_vector/interactive_viewer_vector.dart";

void main() {
  runApp(const VectorDemoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "InteractiveViewerVector Demo",
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(useMaterial3: true),
      home: const VectorDemoPage(),
    );
  }
}

/// Simple node placed on the demo canvas.
class DemoNode {
  const DemoNode({required this.position, required this.size, required this.color, required this.label});

  final Offset position;
  final Size size;
  final Color color;
  final String label;
}

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

  @override
  State<VectorDemoPage> createState() => _VectorDemoPageState();
}

class _VectorDemoPageState extends State<VectorDemoPage> {
  final TransformationControllerVector _controller = TransformationControllerVector();

  /// Number of times the canvas content is built. With the stock
  /// InteractiveViewer this counter explodes during pan/zoom (setState on
  /// every frame). With InteractiveViewerVector it stays flat: the
  /// transformation is pushed straight to the RenderTransform.
  int _buildCount = 0;

  static const double _canvasSize = 2000;

  static const List<DemoNode> _nodes = [
    DemoNode(position: Offset(300, 250), size: Size(180, 100), color: Colors.teal, label: "Node A"),
    DemoNode(position: Offset(900, 200), size: Size(220, 120), color: Colors.deepOrange, label: "Node B"),
    DemoNode(position: Offset(600, 700), size: Size(200, 140), color: Colors.indigo, label: "Node C"),
    DemoNode(position: Offset(1300, 500), size: Size(160, 160), color: Colors.purple, label: "Node D"),
    DemoNode(position: Offset(1100, 1100), size: Size(240, 100), color: Colors.green, label: "Node E"),
    DemoNode(position: Offset(250, 1200), size: Size(200, 180), color: Colors.redAccent, label: "Node F"),
    DemoNode(position: Offset(1500, 1400), size: Size(180, 140), color: Colors.amber, label: "Node G"),
  ];

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _resetTransform() {
    _controller.value = Matrix4.identity();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("InteractiveViewerVector"),
        actions: [
          Center(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Text("canvas builds: $_buildCount"),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.center_focus_strong),
            tooltip: "Reset transform",
            onPressed: _resetTransform,
          ),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          return InteractiveViewerVector(
            transformationController: _controller,
            constrained: false,
            boundaryMargin: const EdgeInsets.all(_canvasSize),
            minScale: 0.1,
            maxScale: 3,
            onInteractionEnd: (_) => setState(() {}),
            child: _buildCanvasContent(),
          );
        },
      ),
      bottomNavigationBar: const Padding(
        padding: EdgeInsets.all(12),
        child: Text(
          "Drag to pan — pinch / scroll to zoom — double-tap the app bar icon to reset.\n"
          "The 'canvas builds' counter stays flat during pan/zoom: no setState, only markNeedsPaint.",
          textAlign: TextAlign.center,
          style: TextStyle(fontSize: 12),
        ),
      ),
    );
  }

  Widget _buildCanvasContent() {
    _buildCount++;
    return SizedBox(
      width: _canvasSize,
      height: _canvasSize,
      child: Stack(
        children: [
          const Positioned.fill(child: CustomPaint(painter: _GridPainter())),
          const CustomPaint(size: Size(_canvasSize, _canvasSize), painter: _LinksPainter(_nodes)),
          for (final node in _nodes)
            Positioned(
              left: node.position.dx,
              top: node.position.dy,
              child: _DemoNodeWidget(node: node),
            ),
        ],
      ),
    );
  }
}

class _DemoNodeWidget extends StatelessWidget {
  const _DemoNodeWidget({required this.node});

  final DemoNode node;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        color: node.color.withValues(alpha: 0.85),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.white38, width: 2),
      ),
      alignment: Alignment.center,
      child: Text(node.label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
    );
  }
}

class _GridPainter extends CustomPainter {
  const _GridPainter();

  static const double _step = 100;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.white12
      ..strokeWidth = 1;
    for (double x = 0; x <= size.width; x += _step) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
    }
    for (double y = 0; y <= size.height; y += _step) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
    }
  }

  @override
  bool shouldRepaint(_GridPainter oldDelegate) => false;
}

class _LinksPainter extends CustomPainter {
  const _LinksPainter(this.nodes);

  final List<DemoNode> nodes;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.white54
      ..strokeWidth = 3
      ..style = PaintingStyle.stroke;
    for (var i = 0; i < nodes.length - 1; i++) {
      final a = nodes[i].position + nodes[i].size.center(Offset.zero);
      final b = nodes[i + 1].position + nodes[i + 1].size.center(Offset.zero);
      canvas.drawLine(a, b, paint);
    }
  }

  @override
  bool shouldRepaint(_LinksPainter oldDelegate) => false;
}
0
likes
145
points
138
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Fork of the Flutter SDK InteractiveViewer that updates the RenderTransform directly (markNeedsPaint) instead of calling setState on every pan/zoom frame. No widget tree rebuild during pan/zoom interactions.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter, vector_math

More

Packages that depend on interactive_viewer_vector