InteractiveViewer

Overview

InteractiveViewer adds zooming and panning around a child. The wheel and pinch gestures zoom around the pointer, and dragging with a mouse or one finger pans. It clips the result to a viewport and applies scale and translation boundaries. It follows the design of Flutter's InteractiveViewer.

When to use it?

  • Explore a chart with densely packed marks.
  • Pan across a large diagram.
  • Inspect an image at different zoom levels.
  • Build a map-like grid or editing canvas.
  • Provide reset and zoom controls for a scene.

Basic Usage

import {
  Column, Container, Expanded, GestureDetector, InteractiveViewer,
  Matrix4, State, StatefulWidget, Text, TransformationController,
} from 'flitter-ui';
 
class ViewerExample extends StatefulWidget {
  createState() { return new ViewerExampleState(); }
}
 
class ViewerExampleState extends State<ViewerExample> {
  controller = new TransformationController();
 
  dispose() {
    this.controller.dispose();
    super.dispose();
  }
 
  build() {
    return Container({ width: 400, height: 300, child: Column({ children: [
      GestureDetector({
        onClick: () => { this.controller.value = Matrix4.identity(); },
        child: Text('Reset view'),
      }),
      Expanded({ child: InteractiveViewer({
        transformationController: this.controller,
        constrained: false,
        minScale: 0.5,
        maxScale: 4,
        child: Container({ width: 800, height: 600, color: '#dbeafe',
          child: Text('Wheel to zoom; drag to pan'),
        }),
      }) }),
    ] }) });
  }
}
 
export default function ViewerExampleWidget() { return new ViewerExample(); }

Props

PropTypeDefault / behavior
childWidgetRequired scene
transformationControllerTransformationControllerCreated internally when omitted
constrainedbooleantrue; use false for a child larger than the viewport
minScale, maxScalenumber0.8, 2.5; finite and positive
boundaryMarginEdgeInsetsZero; scene-space margin available for panning
panEnabled, scaleEnabledbooleantrue; controls user input only
clipBehavior'hardEdge' | 'none''hardEdge'
alignmentAlignmentAlignment.topLeft; positions a scaled scene smaller than the viewport
wheelScale(event: WheelEvent, viewportHeight: number) => numberScale ratio for one wheel event. Defaults to exp(-pixels / 200), or to the event's pinchScale for a touch pinch; return 2 ** (-event.deltaY * 0.002) for the d3-zoom curve React Flow uses
panOnScrollbooleanfalse; when true, a plain wheel pans and a wheel with Ctrl or Meta held (trackpad pinch) still zooms
panOnScrollSpeednumber0.5; multiplier applied to wheel deltas when panning
onInteractionStart(details: ScaleStartDetails) => voidGlobal and local focal point
onInteractionUpdate(details: ScaleUpdateDetails) => voidFocal points, scale ratio, and movement delta
onInteractionEnd(details: ScaleEndDetails) => voidZero velocity for this non-inertial implementation
keyanyWidget reconciliation key

Practical Examples

Set boundaryMargin: EdgeInsets.all(100) to allow an extra 100 scene pixels around the child, or EdgeInsets.all(Infinity) for unrestricted panning. Use panEnabled: false for zoom-only interaction.

For design-tool style navigation, set panOnScroll: true: the wheel and two-finger trackpad scrolling pan the scene, and pinching zooms. Wheel panning respects panEnabled. Pass wheelScale to change how fast the wheel zooms; the result is a ratio applied to the current scale, so 1 ignores the event.

Touch

Touch works without extra props. One finger drags to pan. Two fingers pinch to zoom around their midpoint, and moving them together pans. A touch that starts on the viewer is captured, so the page does not scroll from there; touches elsewhere scroll the page as usual.

Flitter delivers each pinch step as a wheel event with Ctrl held and a pinchScale property, the zoom ratio of that step. Without wheelScale, the viewer applies pinchScale directly, so a pinch follows the fingers exactly. With panOnScroll, a pinch still zooms because Ctrl is held. A custom wheelScale receives the pinch events too and has to read pinchScale itself:

import { Container, InteractiveViewer, type PinchWheelEvent } from 'flitter-ui';
 
InteractiveViewer({
  minScale: 0.5,
  maxScale: 4,
  wheelScale: (event) => {
    const pinch = (event as PinchWheelEvent).pinchScale;
    if (pinch != null) return pinch; // two-finger pinch: follow the fingers
    return 2 ** (-event.deltaY * 0.002); // mouse wheel: the d3-zoom curve
  },
  child: Container({ width: 800, height: 600, color: '#dbeafe' }),
});

Controller

Assign controller.value = Matrix4.diagonal3Values(2, 2, 1) to zoom to 2×. controller.toScene(new Offset({ x, y })) maps a viewport point into scene coordinates; toViewport performs the reverse mapping. These methods include translation and scaling. A singular matrix cannot be inverted and toScene throws.

To change a matrix, assign a new instance or a modified clone to value; mutating the current matrix in place does not notify listeners. addListener and removeListener can update associated controls.

Important Notes

Both viewport dimensions must be bounded. With constrained: false, the child must still choose a finite size. Each wheel event emits one start/update/end sequence; a drag emits one start, a series of updates, and one end. Mouse gesture scaling preserves the scene point under the cursor unless a boundary prevents it. Scale values in updates are relative to that update, and drag updates have a scale of one.

Programmatic matrices are applied as supplied, allowing custom reset and navigation behavior. Limits apply to user gestures. The alignment option aligns undersized scenes; the controller matrix always uses a top-left origin, so coordinate conversion stays consistent.

Mouse, trackpad and touch input are supported; each pinch step emits one start/update/end sequence like a wheel event. Automatic inertial movement and perspective-aware pan boundaries are not implemented. Disabling clipping allows painting outside the viewport, but input remains limited to its original bounds. Dispose controllers that your State owns.

Related Widgets