Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Navigation Controls in Flutter

This tutorial shows how to add custom map navigation controls like zoom buttons, compass, and scale indicators to your MapMetrics Flutter map.

Prerequisites

Before you begin, ensure you have:

Basic Zoom Controls

Add simple zoom in/out buttons as a floating overlay:

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

class NavigationControlsScreen extends StatefulWidget {
  @override
  _NavigationControlsScreenState createState() => _NavigationControlsScreenState();
}

class _NavigationControlsScreenState extends State<NavigationControlsScreen> {
  MapController? mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Navigation Controls')),
      body: Stack(
        children: [
          // Map
          MapMetricsView(
            options: MapOptions(
              initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: Position(2.3522, 48.8566), // Paris (lng, lat)
              initZoom: 12.0,
            ),
            onMapCreated: (controller) => mapController = controller,
          ),
          // Zoom controls (top-right)
          Positioned(
            top: 16,
            right: 16,
            child: Column(
              children: [
                _controlButton(Icons.add, _zoomIn),
                SizedBox(height: 4),
                _controlButton(Icons.remove, _zoomOut),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _controlButton(IconData icon, VoidCallback onPressed) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(4),
        boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
      ),
      child: IconButton(
        icon: Icon(icon, color: Colors.black87),
        onPressed: onPressed,
        constraints: BoxConstraints(minWidth: 40, minHeight: 40),
        padding: EdgeInsets.zero,
      ),
    );
  }

  void _zoomIn() {
    final zoom = mapController?.getCamera().zoom ?? 12.0;
    mapController?.animateCamera(zoom: zoom + 1);
  }

  void _zoomOut() {
    final zoom = mapController?.getCamera().zoom ?? 12.0;
    mapController?.animateCamera(zoom: zoom - 1);
  }
}

mapmetrics also ships a ready-made zoom control — MapControlButtons(showZoomInOutButton: true) — dropped straight into mapChildren. The hand-rolled version above is useful when you want full control over placement and styling.

Complete Navigation Panel

A full set of controls — zoom, compass, location, and reset — built on the real MapController API. Camera state (used to rotate the compass icon and show the zoom readout) is tracked through onEvent, since there is no onCameraMove callback on the widget itself:

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

class FullNavigationScreen extends StatefulWidget {
  @override
  _FullNavigationScreenState createState() => _FullNavigationScreenState();
}

class _FullNavigationScreenState extends State<FullNavigationScreen> {
  MapController? mapController;
  MapCamera? currentCamera;

  static final Position parisCenter = Position(2.3522, 48.8566); // lng, lat

  @override
  Widget build(BuildContext context) {
    final bearing = currentCamera?.bearing ?? 0.0;

    return Scaffold(
      body: Stack(
        children: [
          // Map
          MapMetricsView(
            options: MapOptions(
              initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: parisCenter,
              initZoom: 12.0,
            ),
            onMapCreated: (controller) => mapController = controller,
            onEvent: (event) {
              if (event is MapEventMoveCamera) {
                setState(() => currentCamera = event.camera);
              }
            },
          ),
          // Navigation panel (top-right)
          Positioned(
            top: 60,
            right: 16,
            child: Container(
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(8),
                boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 6)],
              ),
              child: Column(
                children: [
                  // Compass (rotates with bearing)
                  IconButton(
                    icon: Transform.rotate(
                      angle: -bearing * 3.14159 / 180,
                      child: Icon(Icons.navigation, color: Colors.red),
                    ),
                    onPressed: _resetNorth,
                    tooltip: 'Reset North',
                  ),
                  Divider(height: 1),
                  // Zoom in
                  IconButton(
                    icon: Icon(Icons.add),
                    onPressed: _zoomIn,
                    tooltip: 'Zoom In',
                  ),
                  Divider(height: 1),
                  // Zoom out
                  IconButton(
                    icon: Icon(Icons.remove),
                    onPressed: _zoomOut,
                    tooltip: 'Zoom Out',
                  ),
                  Divider(height: 1),
                  // My location
                  IconButton(
                    icon: Icon(Icons.my_location, color: Colors.blue),
                    onPressed: _goToMyLocation,
                    tooltip: 'My Location',
                  ),
                  Divider(height: 1),
                  // Reset view
                  IconButton(
                    icon: Icon(Icons.refresh),
                    onPressed: _resetView,
                    tooltip: 'Reset View',
                  ),
                ],
              ),
            ),
          ),
          // Zoom level indicator (bottom-left)
          if (currentCamera != null)
            Positioned(
              bottom: 24,
              left: 16,
              child: Container(
                padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.9),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  'Zoom: ${currentCamera!.zoom.toStringAsFixed(1)}',
                  style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
                ),
              ),
            ),
        ],
      ),
    );
  }

  void _zoomIn() {
    final zoom = mapController?.getCamera().zoom ?? 12.0;
    mapController?.animateCamera(zoom: zoom + 1);
  }

  void _zoomOut() {
    final zoom = mapController?.getCamera().zoom ?? 12.0;
    mapController?.animateCamera(zoom: zoom - 1);
  }

  void _resetNorth() {
    mapController?.animateCamera(bearing: 0, pitch: 0);
  }

  void _goToMyLocation() async {
    await mapController?.enableLocation();
    await mapController?.trackLocation();
  }

  void _resetView() {
    mapController?.animateCamera(
      center: parisCenter,
      zoom: 12.0,
      bearing: 0,
      pitch: 0,
    );
  }
}

mapmetrics also ships a real compass widget, MapCompass (drop into mapChildren), which rotates and resets to north on tap automatically — you don't have to build the rotation logic yourself unless you want a fully custom look.

Control Positioning

PositionUse Case
Top-rightZoom controls, compass (most common)
Top-leftSearch bar, back button
Bottom-rightAttribution, scale bar
Bottom-leftZoom level, coordinates

Next Steps


Tip: Wrap your controls in a Container with a white background and boxShadow to match the look of standard map control panels — or use the SDK's own MapControlButtons, MapCompass, and MapScalebar widgets in mapChildren for a ready-made look.