custom_interactive_viewer 0.1.0 copy "custom_interactive_viewer: ^0.1.0" to clipboard
custom_interactive_viewer: ^0.1.0 copied to clipboard

A highly customizable alternative for Flutter's InteractiveViewer with advanced keyboard navigation, gesture controls, rotation support, and programmatic transformations.

example/lib/main.dart

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

void main() => runApp(const MaterialApp(home: CameraPlayground()));

class CameraPlayground extends StatefulWidget {
  const CameraPlayground({super.key});
  @override
  State<CameraPlayground> createState() => _CameraPlaygroundState();
}

class _CameraPlaygroundState extends State<CameraPlayground> {
  final camera = CustomInteractiveViewerController(
    initialOffset: const Offset(350, 240),
  );
  bool bounded = false, rtl = false, rotate = false;
  ViewerViewport? viewport;
  static const world = Rect.fromLTWH(-800, -600, 1600, 1200);
  @override
  void dispose() {
    camera.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Directionality(
    textDirection: rtl ? TextDirection.rtl : TextDirection.ltr,
    child: Scaffold(
      appBar: AppBar(title: const Text('Camera playground')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Wrap(
              spacing: 12,
              runSpacing: 8,
              children: [
                FilterChip(
                  label: const Text('Content bounds'),
                  selected: bounded,
                  onSelected: (v) => setState(() => bounded = v),
                ),
                FilterChip(
                  label: const Text('Rotation'),
                  selected: rotate,
                  onSelected: (v) => setState(() => rotate = v),
                ),
                FilterChip(
                  label: const Text('RTL text'),
                  selected: rtl,
                  onSelected: (v) => setState(() => rtl = v),
                ),
                OutlinedButton(
                  onPressed: () {
                    if (viewport != null) {
                      camera.zoomToRegion(world, viewport!.size);
                    }
                  },
                  child: const Text('Fit world'),
                ),
                OutlinedButton(
                  onPressed: () => camera.reset(),
                  child: const Text('Reset camera'),
                ),
                OutlinedButton(
                  onPressed: () => camera.centerOnRect(
                    const Rect.fromLTWH(-450, -200, 180, 90),
                  ),
                  child: const Text('Reveal negative region'),
                ),
              ],
            ),
          ),
          const Padding(
            padding: EdgeInsets.all(12),
            child: Text(
              'Pan in any direction. Pinch or Ctrl-scroll to zoom. Negative coordinates are ordinary world positions. RTL changes text, not camera geometry.',
            ),
          ),
          Expanded(
            child: CustomInteractiveViewer.world(
              controller: camera,
              zoomConfig: const ZoomConfig(minScale: .1, maxScale: 8),
              interactionConfig: InteractionConfig(
                enableRotation: rotate,
                boundsBehavior: bounded
                    ? const WorldBoundsBehavior(world, margin: 100)
                    : null,
              ),
              viewportBuilder: (context, view) {
                viewport = view;
                final point = view.worldToViewport(const Offset(-450, -200));
                return Stack(
                  children: [
                    Positioned.fill(
                      child: CustomPaint(painter: WorldPainter(view)),
                    ),
                    Positioned(
                      left: point.dx,
                      top: point.dy,
                      child: Transform.scale(
                        alignment: Alignment.topLeft,
                        scale: view.camera.scale,
                        child: Card(
                          child: InkWell(
                            onTap: () =>
                                ScaffoldMessenger.of(context).showSnackBar(
                                  const SnackBar(
                                    content: Text(
                                      'Negative-coordinate region selected',
                                    ),
                                  ),
                                ),
                            child: const SizedBox(
                              width: 180,
                              height: 90,
                              child: Center(child: Text('World: −450, −200')),
                            ),
                          ),
                        ),
                      ),
                    ),
                    Positioned(
                      left: 16,
                      bottom: 16,
                      child: Text(
                        'Zoom: ${view.camera.scale.toStringAsFixed(2)} · Camera: ${view.camera.offset}',
                      ),
                    ),
                  ],
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

class WorldPainter extends CustomPainter {
  final ViewerViewport viewport;
  WorldPainter(this.viewport);
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawColor(const Color(0xfff5f7f8), BlendMode.src);
    canvas.save();
    canvas.transform(viewport.worldToViewportMatrix.storage);
    final bounds = viewport.visibleWorldBounds;
    final step = viewport.camera.scale < .3 ? 200.0 : 50.0;
    final grid = Paint()
      ..color = const Color(0xffd8dfe3)
      ..strokeWidth = 1 / viewport.camera.scale;
    for (
      double x = (bounds.left / step).floor() * step;
      x <= bounds.right;
      x += step
    ) {
      canvas.drawLine(Offset(x, bounds.top), Offset(x, bounds.bottom), grid);
    }
    for (
      double y = (bounds.top / step).floor() * step;
      y <= bounds.bottom;
      y += step
    ) {
      canvas.drawLine(Offset(bounds.left, y), Offset(bounds.right, y), grid);
    }
    final axes = Paint()
      ..color = Colors.teal
      ..strokeWidth = 2 / viewport.camera.scale;
    canvas.drawLine(Offset(0, bounds.top), Offset(0, bounds.bottom), axes);
    canvas.drawLine(Offset(bounds.left, 0), Offset(bounds.right, 0), axes);
    canvas.restore();
  }

  @override
  bool shouldRepaint(WorldPainter old) => old.viewport != viewport;
}
9
likes
150
points
1.7k
downloads

Documentation

API reference

Publisher

verified publisherali-hnaineh.dev

Weekly Downloads

A highly customizable alternative for Flutter's InteractiveViewer with advanced keyboard navigation, gesture controls, rotation support, and programmatic transformations.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, vector_math

More

Packages that depend on custom_interactive_viewer