Draw a Gradient Line in Flutter
This tutorial shows how to draw a line with a color gradient along its length — useful for showing elevation, speed, or progress along a route.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Gradient Line Using Multiple Segments
The real MapMetrics Flutter API's PolylineLayer takes a single color for all the lines it draws, just like the fictional API this page used to describe — so the technique is the same: simulate a gradient by breaking the route into short segments and giving each segment its own PolylineLayer with a progressively changing color. layers: accepts a List<Layer>, so you pass one PolylineLayer per segment:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class GradientLineScreen extends StatefulWidget {
@override
_GradientLineScreenState createState() => _GradientLineScreenState();
}
class _GradientLineScreenState extends State<GradientLineScreen> {
MapController? mapController;
// Route through European capitals — Position(lng, lat), GeoJSON order
final List<Position> routePoints = [
Position(-3.7038, 40.4168), // Madrid
Position(2.3522, 48.8566), // Paris
Position(13.405, 52.52), // Berlin
Position(16.3738, 48.2082), // Vienna
Position(28.9784, 41.0082), // Istanbul
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Gradient Line')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(12.0, 47.0),
initZoom: 4,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: _buildGradientLayers(),
),
);
}
/// Build one PolylineLayer per segment so each can have its own color
List<Layer> _buildGradientLayers() {
final layers = <Layer>[];
final segmentCount = routePoints.length - 1;
for (int i = 0; i < segmentCount; i++) {
// Calculate color at this position (blue -> purple -> red)
final t = i / segmentCount;
final color = Color.lerp(Colors.blue, Colors.red, t)!;
layers.add(
PolylineLayer(
polylines: [
LineString(coordinates: [routePoints[i], routePoints[i + 1]]),
],
color: color,
width: 5,
),
);
}
return layers;
}
}Smooth Gradient with Interpolated Points
For a smoother gradient, interpolate extra points between waypoints. This produces many more segments — each still its own PolylineLayer — so keep the step count reasonable (see the performance note below):
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class SmoothGradientLineScreen extends StatefulWidget {
@override
_SmoothGradientLineScreenState createState() =>
_SmoothGradientLineScreenState();
}
class _SmoothGradientLineScreenState extends State<SmoothGradientLineScreen> {
MapController? mapController;
final List<Position> waypoints = [
Position(-3.7038, 40.4168), // Madrid
Position(2.3522, 48.8566), // Paris
Position(13.405, 52.52), // Berlin
Position(16.3738, 48.2082), // Vienna
Position(28.9784, 41.0082), // Istanbul
];
/// Interpolate extra points between waypoints for a smoother gradient
List<Position> _interpolate(List<Position> points, int stepsPerSegment) {
final result = <Position>[];
for (int i = 0; i < points.length - 1; i++) {
final from = points[i];
final to = points[i + 1];
for (int s = 0; s < stepsPerSegment; s++) {
final t = s / stepsPerSegment;
result.add(Position(
from.lng + (to.lng - from.lng) * t,
from.lat + (to.lat - from.lat) * t,
));
}
}
result.add(points.last);
return result;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Smooth Gradient Line')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(12.0, 47.0),
initZoom: 4,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: _buildSmoothGradient(),
),
);
}
List<Layer> _buildSmoothGradient() {
final smoothPoints = _interpolate(waypoints, 20);
final layers = <Layer>[];
for (int i = 0; i < smoothPoints.length - 1; i++) {
final t = i / (smoothPoints.length - 1);
final color = Color.lerp(Colors.green, Colors.red, t)!;
layers.add(
PolylineLayer(
polylines: [
LineString(
coordinates: [smoothPoints[i], smoothPoints[i + 1]],
),
],
color: color,
width: 5,
),
);
}
return layers;
}
}Elevation-Based Gradient
Color the line based on simulated elevation data:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ElevationGradientScreen extends StatefulWidget {
@override
_ElevationGradientScreenState createState() =>
_ElevationGradientScreenState();
}
class _ElevationGradientScreenState extends State<ElevationGradientScreen> {
MapController? mapController;
// Points with simulated elevation data (meters) — lng, lat, elevation
final List<Map<String, dynamic>> routeWithElevation = [
{'lng': 2.2945, 'lat': 48.8584, 'elevation': 30}, // Eiffel Tower
{'lng': 2.3100, 'lat': 48.8620, 'elevation': 45},
{'lng': 2.3200, 'lat': 48.8650, 'elevation': 80},
{'lng': 2.3300, 'lat': 48.8700, 'elevation': 60},
{'lng': 2.3350, 'lat': 48.8750, 'elevation': 100},
{'lng': 2.3400, 'lat': 48.8800, 'elevation': 130}, // Montmartre hill
{'lng': 2.3431, 'lat': 48.8867, 'elevation': 130}, // Sacre-Coeur
];
/// Map elevation to a color (green = low, yellow = medium, red = high)
Color _elevationColor(int elevation) {
final minElev = 30.0;
final maxElev = 130.0;
final t = ((elevation - minElev) / (maxElev - minElev)).clamp(0.0, 1.0);
if (t < 0.5) {
return Color.lerp(Colors.green, Colors.yellow, t * 2)!;
} else {
return Color.lerp(Colors.yellow, Colors.red, (t - 0.5) * 2)!;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Elevation Gradient')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.3200, 48.8700),
initZoom: 14,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: _buildElevationLayers(),
),
// Legend
Positioned(
bottom: 16,
left: 16,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Elevation',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
_legendRow(Colors.green, 'Low (30m)'),
_legendRow(Colors.yellow, 'Medium (80m)'),
_legendRow(Colors.red, 'High (130m)'),
],
),
),
),
],
),
);
}
Widget _legendRow(Color color, String label) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 20, height: 4, color: color),
SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 12)),
],
),
);
}
List<Layer> _buildElevationLayers() {
final layers = <Layer>[];
for (int i = 0; i < routeWithElevation.length - 1; i++) {
final from = routeWithElevation[i];
final to = routeWithElevation[i + 1];
final avgElevation = ((from['elevation'] + to['elevation']) / 2).round();
layers.add(
PolylineLayer(
polylines: [
LineString(
coordinates: [
Position(from['lng'], from['lat']),
Position(to['lng'], to['lat']),
],
),
],
color: _elevationColor(avgElevation),
width: 6,
),
);
}
return layers;
}
}Gradient Techniques Comparison
| Technique | Segments | Smoothness | Performance |
|---|---|---|---|
| Waypoint segments | Few (4-10) | Visible steps | Best |
| Interpolated (20/seg) | Many (80-200) | Smooth | Good |
| Interpolated (50/seg) | Very many (200+) | Very smooth | Moderate |
Each segment above is its own PolylineLayer, and each Layer in layers: becomes a separate native GeoJSON source + style layer under the hood — hundreds of segments means hundreds of layers, which is why heavier interpolation costs more than it would with a single native gradient line. If you need thousands of smoothly-colored segments, consider driving the line via StyleController.addSource/addLayer with a raw line-gradient paint expression on a single LineStyleLayer instead (see the StyleController examples in the SDK's example app), which pushes the gradient calculation to the native renderer.
Next Steps
- Map Interactions — Handle taps and gestures
- Markers and Annotations — Point markers and other layer types
- Jump to Locations — Animate the camera between waypoints
Tip: Use Color.lerp() to blend between any two colors. For multi-stop gradients (e.g., green -> yellow -> red), split the t value into ranges and lerp between adjacent colors.