Maps, Location & Device Features

Controlling the Map Camera & UI Settings

16 min Lesson 2 of 12

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 LatLng specifying where the camera is pointed
  • zoom — a double between 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 + bearing
  • CameraUpdate.newLatLng(latLng) — move to coordinates, keep current zoom
  • CameraUpdate.newLatLngZoom(latLng, zoom) — move and set zoom simultaneously
  • CameraUpdate.zoomIn() / CameraUpdate.zoomOut() — increment zoom by 1
  • CameraUpdate.zoomTo(zoom) — jump directly to a specific zoom level
  • CameraUpdate.newLatLngBounds(bounds, padding) — fit a bounding box into the viewport
Note: 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),
      ),
    );
  }
}
Tip: On iOS, 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 moving
  • onCameraMoveStarted() — fires when a camera movement begins
  • onCameraIdle() — fires once the camera has come to rest (ideal for loading data for the visible region)
Warning: 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 CameraPosition to describe a camera viewpoint (target, zoom, tilt, bearing).
  • Store the GoogleMapController in a Completer so it is always safely accessible.
  • Use CameraUpdate factory methods with animateCamera or moveCamera to drive the camera.
  • Toggle UI controls like compassEnabled, zoomControlsEnabled, and mapType directly on the GoogleMap widget.
  • React to camera motion via onCameraMove and onCameraIdle, but keep those callbacks lightweight.