Skip to content

Change Building Color Based on Zoom Level in Flutter

This tutorial shows how to dynamically change the color of 3D buildings as the user zooms in and out — useful for data visualization, theming, or emphasizing detail at different scales.

Prerequisites

Before you begin, ensure you have:

Zoom-Dependent Building Colors

Change 3D building colors as the user zooms in:

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

class BuildingColorZoomScreen extends StatefulWidget {
  @override
  _BuildingColorZoomScreenState createState() =>
      _BuildingColorZoomScreenState();
}

class _BuildingColorZoomScreenState extends State<BuildingColorZoomScreen> {
  MapMetricsController? mapController;
  double currentZoom = 15.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Building Color by Zoom')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(48.8606, 2.3376), // Louvre area
              zoom: 15.0,
              tilt: 55.0,
              bearing: 30.0,
            ),
            onStyleLoaded: () {
              _add3DBuildings();
            },
            onCameraMove: (CameraPosition position) {
              final newZoom = position.zoom;
              // Update color when zoom changes significantly
              if ((newZoom - currentZoom).abs() > 0.5) {
                setState(() {
                  currentZoom = newZoom;
                });
                _updateBuildingColor(newZoom);
              }
            },
            onCameraIdle: () async {
              final pos = await mapController?.getCameraPosition();
              if (pos != null) {
                setState(() => currentZoom = pos.zoom);
                _updateBuildingColor(pos.zoom);
              }
            },
          ),
          // Zoom level indicator
          Positioned(
            top: 16,
            left: 16,
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              decoration: BoxDecoration(
                color: _getColorForZoom(currentZoom),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                'Zoom: ${currentZoom.toStringAsFixed(1)}',
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
          // Color legend
          Positioned(
            bottom: 16,
            left: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(10),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text('Zoom Levels',
                        style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
                    SizedBox(height: 4),
                    _zoomRow(Colors.grey, '< 14 (far)'),
                    _zoomRow(Colors.blue, '14-15 (district)'),
                    _zoomRow(Colors.teal, '15-16 (neighborhood)'),
                    _zoomRow(Colors.orange, '16-17 (block)'),
                    _zoomRow(Colors.deepOrange, '> 17 (building)'),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _zoomRow(Color color, String label) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 1),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(width: 14, height: 14, color: color),
          SizedBox(width: 6),
          Text(label, style: TextStyle(fontSize: 11)),
        ],
      ),
    );
  }

  Color _getColorForZoom(double zoom) {
    if (zoom < 14) return Colors.grey;
    if (zoom < 15) return Colors.blue;
    if (zoom < 16) return Colors.teal;
    if (zoom < 17) return Colors.orange;
    return Colors.deepOrange;
  }

  String _getHexForZoom(double zoom) {
    if (zoom < 14) return '#9e9e9e';
    if (zoom < 15) return '#2196f3';
    if (zoom < 16) return '#009688';
    if (zoom < 17) return '#ff9800';
    return '#ff5722';
  }

  void _add3DBuildings() {
    mapController?.addFillExtrusionLayer(
      '3d-buildings',
      'composite',
      sourceLayer: 'building',
      fillExtrusionColor: _getHexForZoom(currentZoom),
      fillExtrusionOpacity: 0.7,
      fillExtrusionHeight: ['get', 'height'],
      fillExtrusionBase: ['get', 'min_height'],
      minZoom: 13.0,
    );
  }

  void _updateBuildingColor(double zoom) {
    final hexColor = _getHexForZoom(zoom);
    mapController?.setPaintProperty(
      '3d-buildings',
      'fill-extrusion-color',
      hexColor,
    );
  }
}

Theme-Based Building Colors

Let users switch between color themes for buildings:

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

class BuildingThemeScreen extends StatefulWidget {
  @override
  _BuildingThemeScreenState createState() => _BuildingThemeScreenState();
}

class _BuildingThemeScreenState extends State<BuildingThemeScreen> {
  MapMetricsController? mapController;
  String activeTheme = 'default';

  final Map<String, Map<String, String>> themes = {
    'default': {'color': '#aaaaaa', 'name': 'Default'},
    'night': {'color': '#1a237e', 'name': 'Night Mode'},
    'warm': {'color': '#e65100', 'name': 'Warm Sunset'},
    'forest': {'color': '#1b5e20', 'name': 'Forest'},
    'ice': {'color': '#b3e5fc', 'name': 'Ice Blue'},
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Building Themes')),
      body: Column(
        children: [
          Container(
            padding: EdgeInsets.all(8),
            child: Wrap(
              spacing: 6,
              children: themes.entries.map((entry) {
                final hexColor = entry.value['color']!;
                final color = Color(
                    int.parse(hexColor.replaceFirst('#', '0xFF')));
                return ChoiceChip(
                  avatar: Container(
                    width: 16,
                    height: 16,
                    decoration: BoxDecoration(
                      color: color,
                      shape: BoxShape.circle,
                    ),
                  ),
                  label: Text(entry.value['name']!),
                  selected: activeTheme == entry.key,
                  onSelected: (selected) {
                    if (selected) {
                      setState(() => activeTheme = entry.key);
                      mapController?.setPaintProperty(
                        '3d-buildings',
                        'fill-extrusion-color',
                        hexColor,
                      );
                    }
                  },
                );
              }).toList(),
            ),
          ),
          Expanded(
            child: MapMetrics(
              styleUrl:
                  'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
              onMapCreated: (MapMetricsController controller) {
                mapController = controller;
              },
              initialCameraPosition: CameraPosition(
                target: LatLng(48.8606, 2.3376),
                zoom: 16.0,
                tilt: 55.0,
                bearing: -20.0,
              ),
              onStyleLoaded: () {
                mapController?.addFillExtrusionLayer(
                  '3d-buildings',
                  'composite',
                  sourceLayer: 'building',
                  fillExtrusionColor: themes[activeTheme]!['color']!,
                  fillExtrusionOpacity: 0.7,
                  fillExtrusionHeight: ['get', 'height'],
                  fillExtrusionBase: ['get', 'min_height'],
                  minZoom: 13.0,
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Next Steps


Tip: Use onCameraIdle instead of onCameraMove for paint property updates to avoid excessive calls during zoom animations. The idle callback fires only after the camera stops moving.