Google Map Custom Icon — Part I.

Flutter: How to Use Your Own Image as Google Map Icon

Soo Kim
5 min readJan 24, 2023

This is the first article in a two-part series on Google Map marker icon. In this article, I’ll be going over how to use your own image as a marker with a very simple app that shows listed houses. Part II will go over how to use nine patch image to create dynamic markers (or just image in general), which is a more advanced option.

Keep in mind that this is a very simplified version of what I have done professionally, and that you can do so much more than what is written here. For example, I’ve completely omitted making the selected marker smaller when you tap another house.

Table of Contents

  1. Class — Location Class, House Class, CustomMarker Class
  2. Converting Image Asset to BitmapDescriptor
  3. Provider & View

Custom Classes

For this simple example, I created three classes: Location, House, and Custom Marker. The House class doesn’t require additional explanation.

class House {
final String houseUid;
final String shortDescription;
final LocationClass latLng;

const House({required this.houseUid, required this.shortDescription, required this.latLng});
}

LocationClass

The LocationClass extends Google Map’s LatLng, which I did so that it would be more convenient, as it could be used in other parts of the app.

import 'package:google_maps_flutter/google_maps_flutter.dart';

class LocationClass extends LatLng {
@override
final double latitude;

@override
final double longitude;

const LocationClass({required this.latitude, required this.longitude})
: super(latitude, longitude);

factory LocationClass.fromJson({required double latitude, required double longitude}) =>
LocationClass(longitude: longitude, latitude: latitude);

factory LocationClass.fromLatLng(LatLng ln)=> LocationClass(
latitude: ln.latitude,
longitude: ln.longitude
);
}

CustomMarker Class

The CustomMarker class extends Google Map’s Marker class because I’d like to have my own onTap function and the info of the house stored in the marker. The anchor, which allows you to set the anchor point of the icon, doesn’t matter in this example.

import 'package:google_maps_flutter/google_maps_flutter.dart';

class CustomMarker extends Marker {
final House house;
final FutureOr<void> Function(House house) onTapHouse;

CustomMarker({required this.house, required super.icon, bool hasBubble = false, required this.onTapHouse})
: super(
markerId: MarkerId(house.houseUid),
position: house.latLng,
consumeTapEvents: true, /// do avoid camera moving
anchor: !hasBubble ? const Offset(0.5, 0.5) : const Offset(0.1, 0.8),
onTap: () async => await onTapHouse(house),
);
}

Image Asset to BitmapDescriptor

Google’s Marker takes a BitmapDescriptor as its icon, and you can create it using a array of bytes (Uint8List in Flutter) in png format. so you can take the image Uint8List and convert it to BitmapDescriptor. A Uint8List is a fixed-length list of 8-bit (8 binary digits) unsigned integers. You can create a Uint8List from the byte buffer.

import 'dart:ui' as ui;
import 'package:flutter/services.dart';

Future<Uint8List> getBytesFromAsset({required String path, required int width}) async {
final ByteData _data = await rootBundle.load(path);
final ui.Codec _codec = await ui.instantiateImageCodec(_data.buffer.asUint8List(), targetWidth: width);
final ui.FrameInfo _fi = await _codec.getNextFrame();
final Uint8List _bytes = (await _fi.image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
return _bytes;
}

(1) Get the ByteData of the asset. This will get the byte data of the image as is. So if you convert this immediately to Uint8List, the size of the icon will be the size of the image itself. In order for us to tweak the size of the icon, we need to create a new ByteData with the appropriate size.

(2) Use instantiateImageCodec to get the image codec and set the target width/height.

(3) Use getNextFrame to get information about the image.

(4) Convert the FrameInfo to image → get Bytedata in png format → convert it to Uint8List.

Note: You can choose to return BitmapDescriptor itself by creating it in getBytesFromAsset. I didn’t because I will be using that method in other places as well.

Provider & View

Here is my map provider. When you tap on the marker, the onTapHouse() method removes the tapped one and adds a bigger marker (_selectedMarker). I chose to save the BitmapDescriptors so that I don’t have to create a new one every time the markers are tapped.

class MapProvider with ChangeNotifier {
final MapService _mapService = MapService();

final List<House> houses = [...SampleData.houses];

House? _selectedHouse;
House? get selectedHouse => this._selectedHouse;
set selectedHouse(House? h) => throw "error";

List<Marker> _markers = [];
Set<Marker> get markers => {...this._markers};
set markers(Set<Marker> s) => throw "error";


BitmapDescriptor? _unselectedIconMarker;
BitmapDescriptor? _selectedIconMarker;

Future<void> init() async {
await this._setBitmapDescriptor();
this._setIconMarkers();
}

Future<void> _setBitmapDescriptor() async {
final Uint8List _unselected = await this._mapService.getBytesFromAsset(width: 100, path: "assets/marker_icon.png");
this._unselectedIconMarker = BitmapDescriptor.fromBytes(_unselected);
final Uint8List _selected = await this._mapService.getBytesFromAsset(width: 140, path: "assets/marker_icon.png");
this._selectedIconMarker = BitmapDescriptor.fromBytes(_selected);
}

void _setIconMarkers() {
if (this._unselectedIconMarker == null)return;
this.houses.forEach((House house) => this._markers.add(CustomMarker(icon: this._unselectedIconMarker!, house: house, onTapHouse: this.onTapHouse)));
this.notifyListeners();
}


void onTapHouse(House house) {
this._selectedHouse = house;
if (this._selectedIconMarker == null) return;
this._markers.removeWhere((Marker marker) => marker.markerId.value == house.houseUid);
this._markers.add(CustomMarker(icon: this._selectedIconMarker!, house: house, onTapHouse: this.onTapHouse));
this.notifyListeners();
}
}

Main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_map_custom_icon/map_provider.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:provider/provider.dart';

import 'class/map_related.dart' show House;

void main() => runApp(const GoogleMapCustomIcon());

class GoogleMapCustomIcon extends StatelessWidget {
const GoogleMapCustomIcon({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MapProvider()..init(),
child: MaterialApp(home: MapScreen()),
);
}
}

class MapScreen extends StatelessWidget {
MapScreen({Key? key}) : super(key: key);
final Completer<GoogleMapController> _mapController = Completer();

@override
Widget build(BuildContext context) {
final double _width = MediaQuery.of(context).size.width;

return Scaffold(
body: Stack(
children: <Widget>[
Builder(
builder: (BuildContext ctx) {
final MapProvider _mapProvider = Provider.of(ctx);
return GoogleMap(
initialCameraPosition: const CameraPosition(target: LatLng(37.5258975, 126.9284261), zoom: 14.0),
markers: _mapProvider.markers,
onMapCreated: (GoogleMapController ct) async {
this._mapController.complete(ct);
final GoogleMapController _ct = await this._mapController.future;
_ct.setMapStyle(
'['
'{"featureType": "poi","elementType": "labels", "stylers": [{"visibility": "off"}]},' //
'{"featureType": "poi.park","elementType": "geometry", "stylers": [{"visibility": "simplified"}]},'
'{"featureType": "road","elementType": "labels","stylers": [{"visibility": "off"}]},'
'{"featureType": "road.highway","elementType": "labels", "stylers": [{"visibility": "on"}]},'
'{"featureType": "road.arterial","elementType": "labels", "stylers": [{"visibility": "on"}]},'
'{"featureType": "road.local","elementType": "labels", "stylers": [{"visibility": "on"}]},'
'{"featureType": "road.highway.controlled_access","elementType": "labels", "stylers": [{"visibility": "on"}]},'
']'
);
},
);
},
),
Builder(
builder: (BuildContext ctx) {
final House? _selectedHouse = ctx.select<MapProvider, House?>((MapProvider p) => p.selectedHouse);
if (_selectedHouse == null) return const SizedBox();
return Positioned(
bottom: 50.0,
child: SizedBox(
width: _width,
height: _width * 0.4,
child: HouseInfo(house: _selectedHouse)
),
);
},
),
],
)
);
}
}

class HouseInfo extends StatelessWidget {
const HouseInfo({Key? key, required this.house}) : super(key: key);
final House house;

@override
Widget build(BuildContext context) {
final double _width = MediaQuery.of(context).size.width;

return Container(
width: _width * 0.8,
height: _width * 0.4,
margin: EdgeInsets.only(left: _width * 0.02, right: _width * 0.03, ),
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.0),
border: Border.all(width: 1.5)
),
child: Column(
children: <Widget>[
Text(this.house.shortDescription, style: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold),),
const Padding(
padding: EdgeInsets.only(top: 20.0, left: 20.0),
child: Text("whatever other details...."),
)
],
),
);
}
}

Stay tuned for Part II! Happy Coding :)

--

--