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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
- 3D Buildings — Basic 3D building setup
- Change Layer Color — Dynamic layer color changes
- Custom Map Styling — Full style customization
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.