🗺️

[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), // 画像読み込み待機
),

サイズの設定

logicalSizeimageSizeを適切に設定することで、マーカーの表示品質とパフォーマンスを調整できます:

  • 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),
),

主なメリット

  1. コードの簡略化: Canvas描画に比べて90%以上のコード削減
  2. 保守性の向上: 通常のWidgetと同様にマーカーを管理可能
  3. 再利用性: 一度作成したマーカーWidgetを様々な場面で再利用
  4. 動的対応: リアルタイムでマーカーの内容を変更可能
  5. デザインの自由度: Flutterの豊富なWidgetを活用したリッチなマーカー作成

GoogleMapを使ったアプリ開発において、widget_to_markerは必須のパッケージと言えるでしょう。ぜひ次のプロジェクトで活用してみてください!

Discussion