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
| Prop | Type | Default / behavior |
|---|---|---|
child | Widget | Required scene |
transformationController | TransformationController | Created internally when omitted |
constrained | boolean | true; use false for a child larger than the viewport |
minScale, maxScale | number | 0.8, 2.5; finite and positive |
boundaryMargin | EdgeInsets | Zero; scene-space margin available for panning |
panEnabled, scaleEnabled | boolean | true; controls user input only |
clipBehavior | 'hardEdge' | 'none' | 'hardEdge' |
alignment | Alignment | Alignment.topLeft; positions a scaled scene smaller than the viewport |
wheelScale | (event: WheelEvent, viewportHeight: number) => number | Scale 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 |
panOnScroll | boolean | false; when true, a plain wheel pans and a wheel with Ctrl or Meta held (trackpad pinch) still zooms |
panOnScrollSpeed | number | 0.5; multiplier applied to wheel deltas when panning |
onInteractionStart | (details: ScaleStartDetails) => void | Global and local focal point |
onInteractionUpdate | (details: ScaleUpdateDetails) => void | Focal points, scale ratio, and movement delta |
onInteractionEnd | (details: ScaleEndDetails) => void | Zero velocity for this non-inertial implementation |
key | any | Widget 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
- SingleChildScrollView for one-axis scrolling.
- Transform for direct matrix transforms.
- GestureDetector for custom interactions.