Google Map Custom Icon — Part I.

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

Table of Contents

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

Custom Classes

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

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

LocationClass

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

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

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;
}

Provider & View

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...."),
)
],
),
);
}
}

--

--

Flutter & Node.js Full-Stack Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store