Controlling the Map Camera & UI Settings
Controlling the Map Camera & UI Settings
Once your Google Map is rendered inside a Flutter widget, the next step is learning how to control what the user sees — the position, zoom level, tilt, and bearing of the camera — and how to configure the built-in UI controls that Google Maps provides. These capabilities are essential for building apps that respond to user actions, deep-link to specific locations, and provide a polished, brand-consistent map experience.
The CameraPosition Class
CameraPosition is an immutable snapshot of the map camera's viewpoint. It carries four properties:
- target — a
LatLngspecifying where the camera is pointed - zoom — a
doublebetween 0 (world view) and ~21 (building level) - tilt — degrees of camera tilt from overhead (0 – 90); only works at high zoom levels
- bearing — compass direction the camera faces in degrees (0 = north)
You supply an initialCameraPosition to the GoogleMap widget when the map first loads. Every time you want to programmatically move the camera afterward, you create a CameraUpdate and send it through a GoogleMapController.
Declaring an Initial CameraPosition
import 'package:google_maps_flutter/google_maps_flutter.dart';
const CameraPosition _initialPosition = CameraPosition(
target: LatLng(24.7136, 46.6753), // Riyadh, Saudi Arabia
zoom: 12.0,
tilt: 0,
bearing: 0,
);
GoogleMap(
initialCameraPosition: _initialPosition,
onMapCreated: (GoogleMapController controller) {
_controller = controller;
},
)
Obtaining and Storing GoogleMapController
The GoogleMapController is your handle to the live map instance. It is delivered via the onMapCreated callback once the map tiles have been loaded. Store it in a Completer<GoogleMapController> so that any method that needs it can safely await its availability, even if those methods are called before the map finishes initialising.
Using a Completer to Safely Store the Controller
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapCameraPage extends StatefulWidget {
const MapCameraPage({super.key});
@override
State<MapCameraPage> createState() => _MapCameraPageState();
}
class _MapCameraPageState extends State<MapCameraPage> {
final Completer<GoogleMapController> _controllerCompleter =
Completer<GoogleMapController>();
static const CameraPosition _riyadh = CameraPosition(
target: LatLng(24.7136, 46.6753),
zoom: 12,
);
void _onMapCreated(GoogleMapController controller) {
_controllerCompleter.complete(controller);
}
/// Animate to a new position programmatically
Future<void> _goToMecca() async {
final GoogleMapController controller =
await _controllerCompleter.future;
const CameraPosition mecca = CameraPosition(
target: LatLng(21.3891, 39.8579),
zoom: 14,
tilt: 45,
bearing: 30,
);
await controller.animateCamera(
CameraUpdate.newCameraPosition(mecca),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Map Camera Demo')),
body: GoogleMap(
initialCameraPosition: _riyadh,
onMapCreated: _onMapCreated,
),
floatingActionButton: FloatingActionButton(
onPressed: _goToMecca,
child: const Icon(Icons.location_city),
),
);
}
}
CameraUpdate — The Motion Command
CameraUpdate is a factory class that creates camera movement instructions. You pass a CameraUpdate to either controller.animateCamera() (smooth, animated) or controller.moveCamera() (instant, no animation). The most useful factory constructors are:
CameraUpdate.newCameraPosition(pos)— full control: target + zoom + tilt + bearingCameraUpdate.newLatLng(latLng)— move to coordinates, keep current zoomCameraUpdate.newLatLngZoom(latLng, zoom)— move and set zoom simultaneouslyCameraUpdate.zoomIn()/CameraUpdate.zoomOut()— increment zoom by 1CameraUpdate.zoomTo(zoom)— jump directly to a specific zoom levelCameraUpdate.newLatLngBounds(bounds, padding)— fit a bounding box into the viewport
animateCamera() returns a Future<void> that completes when the animation finishes. Awaiting it lets you chain camera movements or run code only after the camera has settled.Configuring Built-in UI Controls
The GoogleMap widget exposes boolean properties that toggle the native controls rendered by the Google Maps SDK. These controls are drawn by the platform layer (not Flutter), so they always look native on each operating system:
- zoomControlsEnabled — shows the +/– zoom buttons (Android only; default
true) - zoomGesturesEnabled — enables pinch-to-zoom and double-tap-to-zoom (default
true) - compassEnabled — shows a compass badge that appears when the map is rotated (default
true) - myLocationButtonEnabled — shows a "locate me" FAB (requires location permission; default
true) - myLocationEnabled — draws the blue dot at the user's current location
- rotateGesturesEnabled — allows two-finger rotation (default
true) - scrollGesturesEnabled — allows pan/scroll gestures (default
true) - tiltGesturesEnabled — enables two-finger tilt (default
true) - mapToolbarEnabled — shows Android's "open in Maps / Directions" toolbar (Android only; default
true) - mapType — one of
MapType.normal,MapType.satellite,MapType.terrain,MapType.hybrid
Toggling Map Type and Controls at Runtime
class _MapCameraPageState extends State<MapCameraPage> {
MapType _currentMapType = MapType.normal;
bool _showCompass = true;
bool _showZoomControls = true;
void _toggleMapType() {
setState(() {
_currentMapType = _currentMapType == MapType.normal
? MapType.satellite
: MapType.normal;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
initialCameraPosition: _riyadh,
onMapCreated: _onMapCreated,
mapType: _currentMapType,
compassEnabled: _showCompass,
zoomControlsEnabled: _showZoomControls,
zoomGesturesEnabled: true,
myLocationButtonEnabled: false, // hidden — we use our own button
rotateGesturesEnabled: true,
tiltGesturesEnabled: true,
scrollGesturesEnabled: true,
),
floatingActionButton: FloatingActionButton(
onPressed: _toggleMapType,
child: const Icon(Icons.layers),
),
);
}
}
zoomControlsEnabled has no effect — iOS users always zoom with pinch gestures. Set myLocationButtonEnabled: false and provide your own styled button when you need design consistency across both platforms.Listening to Camera Movement Events
The GoogleMap widget fires several callbacks you can hook into to react to camera changes:
onCameraMove(CameraPosition pos)— fires continuously as the camera is movingonCameraMoveStarted()— fires when a camera movement beginsonCameraIdle()— fires once the camera has come to rest (ideal for loading data for the visible region)
onCameraMove can fire dozens of times per second during a pan or pinch. Never perform expensive operations (network calls, heavy computation) inside it. Instead, debounce the handler or use onCameraIdle for data-fetching logic.Summary
You now have the full toolkit for programmatic map camera control in Flutter:
- Use
CameraPositionto describe a camera viewpoint (target, zoom, tilt, bearing). - Store the
GoogleMapControllerin aCompleterso it is always safely accessible. - Use
CameraUpdatefactory methods withanimateCameraormoveCamerato drive the camera. - Toggle UI controls like
compassEnabled,zoomControlsEnabled, andmapTypedirectly on theGoogleMapwidget. - React to camera motion via
onCameraMoveandonCameraIdle, but keep those callbacks lightweight.