Map & Camera
Map options
MapOptions configures a MapLibreMap. Every option except initCamera and
fastPforEnabled can change at any time, for example with setState(), and
the map follows:
MapLibreMap(
options: MapOptions(
style: MapStyle.url('https://demotiles.maplibre.org/style.json'),
initCamera: CameraUpdate.position(
center: Geographic(lon: 9.17, lat: 47.68),
zoom: 7,
),
minZoom: 3,
maxZoom: 18,
maxPitch: 60,
maxBounds: GeoBox(west: 5, south: 45, east: 17, north: 55),
movements: MapMovements(pitch: false),
),
)
| Option | Description |
|---|---|
style |
The map style. A new style loads when it changes. |
initCamera |
Where the map starts, see below. Only read once. |
minZoom, maxZoom |
The zoom range the camera stays in. |
minPitch, maxPitch |
The pitch range the camera stays in. |
maxBounds |
The area the camera stays in. |
movements |
What the user may change, see Gestures. |
gestures |
Which gestures move the map, see Gestures. |
fastPforEnabled |
Decode MLT tiles that use FastPFOR, see Sources. Only read once. Always on for web. |
Initial camera
initCamera sets where the map starts. It takes a CameraUpdate, like the
camera methods of the controller:
CameraUpdate.positionstarts at acenter,zoom,bearing,pitchandroll.CameraUpdate.bounds,CameraUpdate.pointsandCameraUpdate.geometryfit a target into the map from its first frame, inset bypadding, with an optionalbearing,pitchandmaxZoom.- Without
initCamera, the map starts at the camera stored in the style.
MapOptions(
initCamera: CameraUpdate.bounds(
GeoBox(west: 9.5, south: 47.4, east: 9.8, north: 47.6),
padding: EdgeInsets.all(32),
),
)
Move the camera
Use the MapController to move the camera after the map was created. Every
method takes a CameraUpdate, and fields of CameraUpdate.position that you
leave out keep their value:
// jump to a position
await controller.moveCamera(
const CameraUpdate.position(center: Geographic(lon: 9.17, lat: 47.68), zoom: 10),
);
// ease to a position, 300 ms by default
await controller.animateCamera(
const CameraUpdate.position(zoom: 12, bearing: 45, pitch: 30),
);
// fly to bounds
final result = await controller.animateCamera(
CameraUpdate.bounds(
GeoBox(west: 9.5, south: 47.4, east: 9.8, north: 47.6),
padding: const EdgeInsets.all(32),
),
animation: const CameraAnimation.fly(duration: Duration(seconds: 2)),
);
// the camera that shows some points, without moving the map
final camera = await controller.cameraFor(CameraUpdate.points(stops));
CameraAnimation.ease interpolates the camera over a duration along a
curve. CameraAnimation.fly zooms out and in again like a flight, which
suits far targets; without a duration, it takes as long as its speed
needs.
animateCamera completes when the animation ends, with
CameraAnimationResult.completed, or with CameraAnimationResult.cancelled
if a gesture, moveCamera or stopCamera() stopped it. A later
animateCamera takes over and completes the earlier one.
Read the camera
MapController.getCamera() returns the current camera, and the
MapController.camera getter returns it too, or null before the map is
ready. MapLibreMap.onCameraMove reports every change. Widgets in MapLibreMap.children can read the camera with
CameraPosition.of(context) and rebuild when it changes.
The controller also converts between coordinates and screen positions with
toScreenLocation, toLngLat and their list variants, returns the visible
area with getVisibleBounds() and the scale with
getMetersPerPixelAtLatitude().
Gestures
Gestures are recognized in Flutter and are the same on every platform. Two options decide what they do, and everything is on by default:
movementstakes aMapMovements: what the user may change, with any gesture:pan,zoom,rotateandpitch.gesturestakes aMapGestures: which gestures work. Each one moves the camera only along the movements that are on.
| Gesture switch | Gesture | Moves |
|---|---|---|
drag |
Drag with one finger or the left mouse button | pan |
fling |
Keep moving after a fast drag or pinch | pan |
pinch |
Move, spread or twist two fingers on the screen or a trackpad | pan, zoom, rotate |
scrollWheel |
Scroll wheel | zoom |
doubleTap |
Double tap or double click | zoom |
doubleTapDrag |
Double tap and drag up or down | zoom |
secondaryDrag |
Drag with the right mouse button or Ctrl + left button | rotate, pitch |
twoFingerDrag |
Drag two fingers side by side up or down | pitch |
MapOptions(
// No tilting and no rotating.
movements: MapMovements(pitch: false, rotate: false),
// No scroll wheel zoom; pinching still zooms.
gestures: MapGestures(scrollWheel: false),
)
// Only pan, by dragging.
MapOptions(
movements: MapMovements.none(pan: true),
gestures: MapGestures.none(drag: true),
)
The movements and gestures only stop the user. To keep the camera within limits for the app
too, use minZoom, maxZoom, minPitch, maxPitch and maxBounds.
Widgets in MapLibreMap.children receive their own taps and drags before the
map does.
Query features
queryRenderedFeatures returns the features drawn at a screen position
(QueryGeometry.point), in a rectangle (.rect) or anywhere in the map
(.viewport), optionally only from some layers or matching a filter:
onClick: (event) async {
final features = await controller.queryRenderedFeatures(
.point(event.screenPoint),
layerIds: ['points'],
filter: .equal(.get('kind'), .literal('cafe')),
);
for (final feature in features) {
print('${feature.layerId}: ${feature.properties}');
}
},
Each RenderedFeature holds its id, properties and geobase geometry, and the
sourceId, sourceLayer and layerId it comes from.
querySourceFeatures returns the features of a source in the tiles it loaded,
also those that a filter hides or that lie outside the visible part of the
map. A source loads tiles only while a visible layer uses it. For a vector
tile source, name the sourceLayer:
final countries = await controller.querySourceFeatures(
'openmaptiles',
sourceLayer: 'boundary',
filter: .equal(.get('admin_level'), const .literal(2)),
);
Paused rendering
A map stops rendering while MapLibreMap.paused is true, and keeps its style,
camera, sources and cached tiles. Changes made while it is paused show once it
resumes.
Maps also pause on their own while they are hidden by an IndexedStack or a
Visibility that keeps their state, or while TickerMode is disabled, as for
a route that another route covers. Set paused for the cases Flutter does not
report, such as a kept-alive page of a PageView or TabBarView while
another page shows:
MapLibreMap(
paused: _tabController.index != 0,
options: MapOptions(/* ... */),
)
MapController.isRenderingPaused and the onRenderingChanged callback tell
whether the map renders. On web, the browser already skips hidden maps, so
paused has no effect there.