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

Offset the Vanishing Point Using Padding in Flutter

This tutorial shows how to offset the map's center point so the map's focal point shifts to accommodate UI overlays like side panels, bottom sheets, or info cards without covering the area of interest.

Prerequisites

Before you begin, ensure you have:

How padding works in this SDK

There is no contentPadding property on MapMetricsView/MapOptions in this SDK. The real way to shift the visual center is to convert your target coordinate to a screen Offset with MapController.toScreenLocation, nudge that Offset by the pixel amount you want to compensate for, convert it back to a Position with MapController.toLngLat, and re-center the camera on that adjusted position with animateCamera. Both conversions are real MapController methods.

dart
/// Re-center [target] so it appears offset by [dx]/[dy] screen pixels
/// from the map's true center — useful for keeping a point of interest
/// visible above a bottom sheet or beside a side panel.
Future<void> recenterWithOffset(
  MapController controller,
  Position target, {
  double dx = 0,
  double dy = 0,
}) async {
  final targetScreen = await controller.toScreenLocation(target);
  final shifted = Offset(targetScreen.dx + dx, targetScreen.dy + dy);
  final newCenter = await controller.toLngLat(shifted);
  await controller.animateCamera(center: newCenter);
}

Bottom Sheet with Map Padding

Shift the map center up when a bottom panel is shown, by moving the target's screen position down before converting back to a geographic coordinate:

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

class MapPaddingScreen extends StatefulWidget {
  @override
  _MapPaddingScreenState createState() => _MapPaddingScreenState();
}

class _MapPaddingScreenState extends State<MapPaddingScreen> {
  MapController? mapController;
  bool showPanel = false;
  final double panelHeight = 200.0;

  final Position target = Position(2.2945, 48.8584); // Eiffel Tower (lng, lat)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Map Padding')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: target,
              initZoom: 15.0,
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            mapChildren: [
              WidgetLayer(
                markers: [
                  Marker(
                    point: target,
                    size: const Size(32, 32),
                    alignment: Alignment.bottomCenter,
                    child: const Icon(Icons.location_on, color: Colors.red, size: 32),
                  ),
                ],
              ),
            ],
          ),
          // Bottom panel
          if (showPanel)
            Positioned(
              bottom: 0,
              left: 0,
              right: 0,
              child: Container(
                height: panelHeight,
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius:
                      BorderRadius.vertical(top: Radius.circular(16)),
                  boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8)],
                ),
                child: Padding(
                  padding: EdgeInsets.all(20),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text('Eiffel Tower',
                                style: TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold)),
                          ),
                          IconButton(
                            icon: Icon(Icons.close),
                            onPressed: () => setState(() => showPanel = false),
                          ),
                        ],
                      ),
                      Text('Champ de Mars, 5 Av. Anatole France',
                          style: TextStyle(color: Colors.grey[600])),
                      SizedBox(height: 12),
                      Row(
                        children: [
                          Icon(Icons.star, color: Colors.amber, size: 18),
                          Text(' 4.7 (200K reviews)'),
                        ],
                      ),
                      Spacer(),
                      SizedBox(
                        width: double.infinity,
                        child: ElevatedButton(
                          onPressed: () {},
                          child: Text('Get Directions'),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
      floatingActionButton: showPanel
          ? null
          : FloatingActionButton(
              onPressed: () async {
                setState(() => showPanel = true);
                // Shift the target up by half the panel height so it
                // stays visible in the space above the sheet.
                final controller = mapController;
                if (controller != null) {
                  final targetScreen = await controller.toScreenLocation(target);
                  final shifted = Offset(
                    targetScreen.dx,
                    targetScreen.dy + panelHeight / 2,
                  );
                  final newCenter = await controller.toLngLat(shifted);
                  await controller.animateCamera(center: newCenter);
                }
              },
              child: Icon(Icons.info),
            ),
    );
  }
}

Side Panel Padding

Shift the map center to the right when a left panel is open, by moving the target's screen position left before converting back:

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

class SidePanelPaddingScreen extends StatefulWidget {
  @override
  _SidePanelPaddingScreenState createState() =>
      _SidePanelPaddingScreenState();
}

class _SidePanelPaddingScreenState extends State<SidePanelPaddingScreen> {
  MapController? mapController;
  bool showSidePanel = false;
  final double panelWidth = 250.0;

  final List<Map<String, dynamic>> places = [
    {'name': 'Eiffel Tower', 'position': Position(2.2945, 48.8584)},
    {'name': 'Louvre Museum', 'position': Position(2.3376, 48.8606)},
    {'name': 'Notre-Dame', 'position': Position(2.3499, 48.8530)},
    {'name': 'Sacre-Coeur', 'position': Position(2.3431, 48.8867)},
    {'name': 'Arc de Triomphe', 'position': Position(2.2950, 48.8738)},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Side Panel Padding'),
        leading: IconButton(
          icon: Icon(showSidePanel ? Icons.menu_open : Icons.menu),
          onPressed: () => setState(() => showSidePanel = !showSidePanel),
        ),
      ),
      body: Row(
        children: [
          // Side panel
          if (showSidePanel)
            Container(
              width: panelWidth,
              color: Colors.white,
              child: ListView.builder(
                itemCount: places.length,
                itemBuilder: (context, i) {
                  final place = places[i];
                  return ListTile(
                    leading: Icon(Icons.location_on, color: Colors.blue),
                    title: Text(place['name']),
                    onTap: () async {
                      final controller = mapController;
                      if (controller == null) return;
                      final target = place['position'] as Position;
                      await controller.animateCamera(center: target, zoom: 15.0);
                      // Compensate for the panel covering the left side.
                      final targetScreen = await controller.toScreenLocation(target);
                      final shifted = Offset(
                        targetScreen.dx - panelWidth / 2,
                        targetScreen.dy,
                      );
                      final newCenter = await controller.toLngLat(shifted);
                      await controller.animateCamera(center: newCenter);
                    },
                  );
                },
              ),
            ),
          // Map
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initStyle:
                    'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
                initCenter: Position(2.320, 48.860), // lng, lat
                initZoom: 13.0,
              ),
              onMapCreated: (MapController controller) {
                mapController = controller;
              },
              mapChildren: [
                WidgetLayer(
                  markers: places.map((p) {
                    return Marker(
                      point: p['position'] as Position,
                      size: const Size(28, 28),
                      alignment: Alignment.bottomCenter,
                      child: const Icon(Icons.location_on, color: Colors.blue, size: 28),
                    );
                  }).toList(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Padding Options

DirectionScreen offset applied before toLngLatUse Case
Bottom sheetdy: +panelHeight / 2Bottom sheets, info panels
Top bardy: -barHeight / 2Top search bars
Left drawerdx: -panelWidth / 2Left sidebars, drawers
Right paneldx: +panelWidth / 2Right panels

Next Steps


Tip: MapController.fitBounds also accepts an offset: Offset and padding: EdgeInsets parameter, which is the more direct real-API tool when you're fitting the camera to a bounding box (rather than re-centering on a single point) and want the fitted content to avoid a UI overlay.