[Flutter]GoogleMapピンをカスタマイズしてみた備忘録(svgのカラー変更など)
はじめに
Google Mapsを使ったアプリ開発で、SVG画像をマーカーとして表示し、さらにそのSVGアイコンのカラーを動的に変更したいという要件に直面しました。
最初は従来のcreateBitmapメソッドを使ってCanvas描画でマーカーを作成しようとしたのですが、思った以上に複雑で多くの壁にぶつかりました:
- 複数画像の重ねの時のポジションどりが複雑: 複数のSVGや画像を重ねる際の位置調整が非常に困難
- テキストの配置も難しい: Canvas上でのテキストレンダリングと位置調整が煩雑
- SVGの動的カラー変更ができない: Picture型として表示させているため、Paint()を指定することができず、色の変更に苦戦
複数の要素を組み合わせたマーカーを作成する場合、Canvas描画ではさらに複雑な実装が必要になります。
そんな中、widget_to_markerパッケージを発見しました。このパッケージを使えば、普段Flutterで作成しているWidgetをそのままGoogleMapのマーカーとして使用できるため、上記の課題を一気に解決できます。
widget_to_markerパッケージとは
widget_to_markerは、任意のFlutter WidgetをBitmapDescriptorに変換し、google_maps_flutterのマーカーアイコンとして使用できるようにするパッケージです。
主な特徴
- 任意のWidgetをマーカーとして使用可能
- 複雑なレイアウトやテキストを含むマーカーの簡単作成
- 動的なデータに基づくマーカーのカスタマイズが容易
- ネットワーク画像やアセット画像の読み込み待機機能
実装方法
1. パッケージのインストール
まず、pubspec.yaml
にパッケージを追加します:
dependencies:
widget_to_marker: ^1.0.6
google_maps_flutter: ^2.2.8
2. カスタムマーカーWidgetの作成
テキストと画像を組み合わせたカスタムマーカーを作成してみましょう:
import 'package:widget_to_marker/widget_to_marker.dart';
class CustomMarkerWidget extends StatelessWidget {
const CustomMarkerWidget({
Key? key,
required this.title,
required this.subtitle,
this.backgroundColor = Colors.white,
}) : super(key: key);
final String title;
final String subtitle;
final Color backgroundColor;
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on,
color: Colors.red,
size: 30,
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
);
}
}
3. GoogleMapでの使用
作成したWidgetをマーカーとして使用します(一部省略):
// マーカー作成関数
Future<void> _createMarkers() async {
// カスタムマーカーの作成
final customMarker = Marker(
markerId: const MarkerId("custom_marker_1"),
position: const LatLng(35.6812, 139.7671), // 東京
icon: await CustomMarkerWidget(
title: "東京駅",
subtitle: "中央区",
backgroundColor: Colors.blue.shade50,
).toBitmapDescriptor(
logicalSize: const Size(120, 100),
imageSize: const Size(120, 100),
),
);
setState(() {
markers = {customMarker};
});
}
// UI部分
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('カスタムマーカーサンプル')),
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: const CameraPosition(
target: LatLng(35.6812, 139.7671),
zoom: 12,
),
markers: markers,
),
);
}
従来のcreateBitmapメソッドとの比較
従来のcreateBitmapを使った実装では、以下のような複雑なCanvas操作が必要でした:
Future<BitmapDescriptor> createBitmap() async {
// サイズの決定ロジック
Size finalSize;
if (iconSize != null) {
finalSize = Size(iconSize, iconSize);
} else {
finalSize = Size(size.width, size.height);
}
final basePictureInfo =
await vg.loadPicture(SvgAssetLoader(baseAssetName), null);
// 全てのオーバーレイSVGを事前に読み込み
final overlayPictureInfos = <PictureInfo>[];
for (final overlay in overlays) {
final pictureInfo =
await vg.loadPicture(SvgAssetLoader(overlay.assetName), null);
overlayPictureInfos.add(pictureInfo);
}
double devicePixelRatio =
ui.PlatformDispatcher.instance.views.first.devicePixelRatio;
int width = (finalSize.width * devicePixelRatio).toInt();
int height = (finalSize.height * devicePixelRatio).toInt();
int canvasWidth = (finalSize.width * devicePixelRatio).toInt() * 2;
int canvasHeight = (finalSize.height * devicePixelRatio).toInt() * 2;
final baseScaleFactor = min(
width / basePictureInfo.size.width,
height / basePictureInfo.size.height,
);
double offsetX = (canvasWidth - finalSize.width * devicePixelRatio) / 2;
double offsetY = (canvasHeight - finalSize.height * devicePixelRatio) / 2;
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
// オーバーレイの位置情報を保存するリスト
final overlayPositions = <Map<String, dynamic>>[];
canvas.restore();
// ベースSVGを描画
canvas.save();
canvas.translate(offsetX, offsetY);
canvas.scale(baseScaleFactor);
canvas.drawPicture(basePictureInfo.picture);
canvas.restore();
// ... さらに複雑な描画処理が続く
}
この実装方式では、複数の画像を重ねる場合や、テキストを追加する場合にさらに複雑になり、保守性が大幅に低下します。
パフォーマンスと注意点
waitToRender属性の活用
ネットワーク画像や大きなアセット画像を含むマーカーを作成する場合は、waitToRender
パラメータを使用して適切な待機時間を設定できます:
icon: await CustomMarkerWidget(
title: "タイトル",
subtitle: "サブタイトル",
).toBitmapDescriptor(
logicalSize: const Size(120, 100),
imageSize: const Size(120, 100),
waitToRender: const Duration(milliseconds: 500), // 画像読み込み待機
),
サイズの設定
logicalSize
とimageSize
を適切に設定することで、マーカーの表示品質とパフォーマンスを調整できます:
-
logicalSize
: Widgetのレイアウトサイズ -
imageSize
: 実際に生成される画像のサイズ
まとめ
widget_to_markerパッケージの導入により、GoogleMapのマーカーカスタマイズが劇的に簡単になりました。
従来の方法との比較
従来の方法(createBitmap使用):
// 50行以上の複雑なCanvas描画コード
Future<BitmapDescriptor> createBitmap() async {
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
// ... 複雑な描画処理、位置調整、サイズ計算
final picture = recorder.endRecording();
final image = await picture.toImage(width, height);
// ... バイト変換処理
}
widget_to_marker使用:
// わずか数行でカスタムマーカー完成
icon: await CustomMarkerWidget(
title: "タイトル",
subtitle: "サブタイトル",
).toBitmapDescriptor(
logicalSize: const Size(120, 100),
imageSize: const Size(120, 100),
),
主なメリット
- コードの簡略化: Canvas描画に比べて90%以上のコード削減
- 保守性の向上: 通常のWidgetと同様にマーカーを管理可能
- 再利用性: 一度作成したマーカーWidgetを様々な場面で再利用
- 動的対応: リアルタイムでマーカーの内容を変更可能
- デザインの自由度: Flutterの豊富なWidgetを活用したリッチなマーカー作成
GoogleMapを使ったアプリ開発において、widget_to_markerは必須のパッケージと言えるでしょう。ぜひ次のプロジェクトで活用してみてください!
Discussion