.NET MAUI(Android)で Google Maps のクラスタリングを導入すると、次にやりたくなるのが「ピンをタップしたときのカスタム InfoWindow 表示」です。ところが、Xamarin では当たり前に触れた ClusterManager の MarkerCollection が、MAUI で使っているパッケージでは見当たらず、同じ書き方ができないケースがあります。この記事では原因を整理し、現場で取り得る回避策を具体的にまとめます。
.NET MAUI(Android)のクラスタリングで「カスタム InfoWindow」にハマる理由
クラスタリングは、近い位置にある多数のピン(マーカー)をまとめて表示し、ズーム操作に応じて「まとまり」を分解・統合する仕組みです。Google Maps のクラスタリングを扱う場合、Android では Google Maps Android SDK と、その上に乗る Maps Utils(クラスタリング支援) の概念が関わってきます。
ここで問題になるのが「InfoWindow(吹き出し)」です。InfoWindow はマーカーをタップしたときに出るポップアップですが、実務では次のような要件がよく出ます。
- 店舗アイコンをタップしたら、店舗名・営業時間・ステータス・写真などを表示したい
- クラスタ(まとまり)をタップしたら、「◯件あります」など別の表示にしたい
- 表示内容をデータモデルに合わせて柔軟に変えたい
一方で、InfoWindow は Android 側の仕様として「自由度が高そうで、実は制約が強い」UI です。さらに .NET MAUI では、ネイティブ API を どのパッケージ(ラッパー)経由で触っているか によって、書けるコードが大きく変わります。
前提整理:InfoWindow は「実ビュー」ではない
Android の Google Maps における InfoWindow は、画面上に普通の View が乗るのではなく、一度描画(スナップショット)された見た目として表示されます。これにより、次のような特性が出ます。
- InfoWindow 内のボタンやリンクを直接タップしても、基本的に個別クリックは取れない(クリックは「InfoWindow 全体」扱い)
- 画像を非同期ロードしても、そのままでは表示が更新されない(再表示や invalidate 的な工夫が必要)
- カスタム表示は InfoWindowAdapter(IInfoWindowAdapter)で行う
つまり「レイアウトを自由に組めば完璧」というより、仕様を理解した上で、最適な UI(InfoWindow / BottomSheet / 画面遷移)を選ぶのが重要です。
症状:Xamarin の書き方が MAUI で通用しない
質問の状況を噛み砕くと、困っているのは次の一点です。
- Xamarin では clustermanager.MarkerCollection.SetOnInfoWindowAdapter(…) のように、クラスタ配下のマーカー群に対して InfoWindowAdapter を設定できた
- .NET MAUI(Android)で利用している GoogleMapsUtils.Android.Maui (1.0.2) の ClusterManager には MarkerCollection が公開されていない
- そのため、Xamarin と同じ実装方針が取れず、カスタム InfoWindow 表示の入り口が塞がっている
原因:MAUI そのものではなく「利用しているラッパーの API 不足」
ここを誤解しやすいのですが、今回の論点は「MAUI だからできない」ではありません。多くのケースで本質は次のどちらかです。
- そもそも使っているパッケージが、ネイティブ側の API を 十分に公開していない
- 公開はしているが、ドキュメントやサンプルが少なく、到達経路が見えない
今回のように ClusterManager に MarkerCollection が無い場合、Xamarin 時代の手順(MarkerCollection に対して adapter をセット)が再現できません。これはアプリ側で頑張っても「プロパティが存在しない」という壁なので、基本的には ライブラリ側の対応範囲の問題です。
まず確認したいこと:自分が触っているのは「どの ClusterManager」か
同じ “ClusterManager” という名前でも、以下のような事情で別物になりえます。
- 公式の Maps Utils をそのままバインドしているのか
- MAUI 向けに独自ラップされ、API が省略されているのか
- Generics の扱い、イベントの露出、Renderer の差し替え口があるか
| 観点 | Xamarin 時代に多い構成 | .NET MAUI + サードパーティ構成で起きがち | 影響 |
|---|---|---|---|
| クラスタリングの API | Maps Utils の API を比較的そのまま利用 | 必要最低限だけ公開(省略あり) | MarkerCollection / Renderer などが触れない |
| InfoWindow 設定箇所 | MarkerCollection に adapter を設定できる | 設定口が無い/Map 側しか触れない | マーカー種別ごとの出し分けが難しい |
| 拡張ポイント | DefaultClusterRenderer の差し替えがしやすい | Renderer が internal 扱いなどで差し替え不能 | marker.Tag 付与などの定石が使えない |
この表の通り、どこまでラップされているかで取れる戦略が変わります。今回のケースは「MarkerCollection が見えない」ため、Xamarin の定石が使えない側です。
解決策の方向性は大きく 2 つ
今回の状況に対して、現実的な打ち手は次の 2 つに集約されます。
- 解決策1:利用しているパッケージ(GoogleMapsUtils.Android.Maui)に機能追加・不具合として依頼する
- 解決策2:より API が揃っている別パッケージ(例:Xamarin.Google.Maps.Utils など)へ切り替える
以降では、それぞれを「実務で動かす」前提で、手順と注意点を掘り下げます。
解決策1:プラグイン側に Issue を起票して対応を依頼する
MarkerCollection が存在しない問題は、アプリコードの工夫でどうこうできる範囲を超えています。したがって、正攻法は 当該プラグインの公式リポジトリに Issue を起票し、API を公開してもらうことです。
Issue 起票時に入れると解決が早い情報
- 利用バージョン(例:GoogleMapsUtils.Android.Maui 1.0.2)
- .NET / MAUI / Android のバージョン、ターゲット API レベル
- 期待する API(MarkerCollection / ClusterMarkerCollection を公開してほしい、あるいは SetOnInfoWindowAdapter 相当のメソッドが欲しい)
- 再現手順(最小コード、最小プロジェクト)
- Xamarin で動いていたコード例(比較として)
要求仕様を「API の形」で具体化する
Issue を出す際に有効なのは、「やりたいこと」だけでなく「どういう API があれば実装できるか」を提示することです。例えば次のような形です。
- ClusterManager に MarkerCollection を公開する
- ClusterManager に ClusterMarkerCollection(クラスタ側)も公開する
- あるいは、MarkerCollection を直接触らずに済む SetInfoWindowAdapter のヘルパーを提供する
プラグイン作者側が「どこを公開すべきか」を判断しやすくなり、修正も早くなりやすいです。
社内・商用で急ぐなら「フォークして自前で追加」も選択肢
もし待てない場合、リポジトリが公開されている前提ですが、プラグインをフォークして MarkerCollection を公開する PR を作り、社内ビルドで一時的に運用する手もあります。
ただし、これはチーム運用の負債にもなるため、次の観点で判断しましょう。
- アプリのリリース期限が近いか
- クラスタリング+InfoWindow が必須機能か
- 今後も Maps 周りで改修が続くか
- Google Play services / AndroidX の追従が必要か
解決策2:より API が揃っている別パッケージへ切り替える
スレッド内で提示されている案として、「追従が遅い/必要な API が揃っていない」場合は、別のパッケージへ切り替えるのが現実的、という判断があります。代表例として挙がっているのが Xamarin.Google.Maps.Utils 系です。
切り替えの狙い
切り替えによって得たいのは、主に次の 2 点です。
- MarkerCollection にアクセスできる(Xamarin でやっていた adapter 設定が復活する可能性)
- Renderer やイベントなど、Maps Utils の拡張ポイントを使える
切り替え前に知っておくべき注意点
一方で、Xamarin 系の資産を MAUI 側で使う場合、依存関係が絡みます。特に Android は次がポイントです。
- Google Play services(Maps 関連)の依存整合
- AndroidX 互換の状況
- 既存の地図コントロール(Microsoft.Maui.Maps / 別プラグイン)との相性
実務では、いきなり本体に組み込むより、最小構成の検証プロジェクトを作って「地図表示→クラスタリング→タップ→InfoWindow 表示」まで通るか確認してから移植するのが安全です。
| 選択肢 | 工数 | 安定性 | 将来性 | 向いているケース | 注意点 |
|---|---|---|---|---|---|
| プラグインに Issue 起票 | 小 | 作者対応次第 | 対応されれば高い | 時間に余裕がある、OSS に貢献したい | 修正がいつ入るか読めない |
| プラグインをフォークして自前修正 | 中〜大 | 短期は高い | 運用負債になりやすい | リリースが迫っている、機能が必須 | アップデート追従が必要 |
| 別パッケージへ切り替え | 中 | 実績次第 | 選定次第 | 必要 API が揃うことが最優先 | 依存関係の整合が課題 |
| InfoWindow を捨てて別 UI(BottomSheet 等) | 中 | 高い | 高い | 情報量が多い、操作が複雑 | UI 設計の見直しが必要 |
(補助策)MarkerCollection が触れない時に検討できる設計変更
ここからは「本筋はプラグイン対応 or 切り替え」だと理解したうえで、現場でよく採用される補助策を紹介します。状況によっては、MarkerCollection に触れなくても目的を達成できることがあります。
補助策A:InfoWindow ではなく BottomSheet / ポップアップに寄せる
InfoWindow は見た目は便利ですが、制約が多く、UI がリッチになるほど苦しくなります。たとえば次の条件に当てはまるなら、InfoWindow を捨てて BottomSheet などに寄せた方が結果的に堅牢です。
- 写真・複数行・ボタンなど、情報量が多い
- 「お気に入り」「経路」など、InfoWindow 内で操作させたい
- 非同期で表示内容が変わる(在庫、混雑度、到着予測など)
地図上のタップは「どのピンが選ばれたか」を決めるだけにして、詳細は別 UI に任せる設計は、MAUI との相性も良いです。
補助策B:Map 全体に InfoWindowAdapter を設定して出し分ける
理屈の上では、InfoWindowAdapter はマーカー単位ではなく Map に対して設定する手もあります。MarkerCollection に対して adapter を付けられない場合でも、Map に adapter を設定できるなら、次のような設計が可能です。
- すべてのマーカー(クラスタ含む)に共通の InfoWindowAdapter を設定
- marker.Title / marker.Snippet / marker.Tag などを見て内容を切り替える
ただし、クラスタリングライブラリが内部でマーカーを生成するため、marker.Tag を自由にセットできるかや、マーカー生成のタイミングを制御できるかが鍵になります。プラグインが Renderer 差し替えを提供していないと、この補助策は成立しにくいです。
参考として、Android 側で一般的な InfoWindowAdapter の形は次のようになります。
using Android.Content;
using Android.Views;
using Android.Widget;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
public class CustomInfoWindowAdapter : Java.Lang.Object, GoogleMap.IInfoWindowAdapter
{
private readonly LayoutInflater _inflater;
public CustomInfoWindowAdapter(Context context)
{
_inflater = LayoutInflater.From(context);
}
// 枠ごと差し替える場合は GetInfoWindow を返す
public View GetInfoWindow(Marker marker) => null;
// 中身だけ差し替える場合は GetInfoContents を返す
public View GetInfoContents(Marker marker)
{
var view = _inflater.Inflate(Resource.Layout.info_window, null);
view.FindViewById<TextView>(Resource.Id.title)!.Text = marker.Title ?? string.Empty;
view.FindViewById<TextView>(Resource.Id.snippet)!.Text = marker.Snippet ?? string.Empty;
return view;
}
}
この方式の利点は、MarkerCollection を触れなくても「表示」自体は組める可能性がある点です。一方で、クラスタリング側のマーカーにタイトルやタグが入っていないと、出し分けロジックを作れません。
補助策C:Renderer を差し替えて marker.Tag を付与する
Maps Utils の定石は、Renderer を差し替えて「クラスタアイテムのマーカーが生成された直後」に情報を埋め込み、InfoWindowAdapter はそれを参照して描画する設計です。
例えば、クラスタアイテムが描画されたタイミングで marker.Tag にモデルを詰めるイメージです。
// ※実際の型名や名前空間は利用ライブラリにより異なります(概念例)
public class MyClusterRenderer : DefaultClusterRenderer<MyItem>
{
public MyClusterRenderer(Context context, GoogleMap map, ClusterManager<MyItem> clusterManager)
: base(context, map, clusterManager)
{
}
protected override void OnClusterItemRendered(MyItem item, Marker marker)
{
base.OnClusterItemRendered(item, marker);
// marker.Tag にモデルを保持して InfoWindowAdapter で参照する、という定石
marker.Tag = item;
marker.Title = item.Title;
marker.Snippet = item.Subtitle;
}
}
ただし今回の前提では、GoogleMapsUtils.Android.Maui のラップが薄く、Renderer 差し替えや MarkerCollection が提供されていない可能性が高いです。つまり補助策B/C は「理論上は可能でも、プラグイン API が開いていないと実装できない」側になります。
実務でよくある落とし穴と対処
画像を入れたら更新されない
InfoWindow はスナップショット表示のため、画像の非同期ロードが完了しても表示が更新されないことがあります。一般的には次の対応を組み合わせます。
- 画像ロード完了後に一度 InfoWindow を閉じて再表示する(hide → show)
- キャッシュが効くようにし、InfoWindow 表示前に画像が揃う状態を作る
InfoWindow 内のボタンが押せない
基本的に InfoWindow 内部のビューイベントは取れません。必要なら UI を InfoWindow に詰め込まず、BottomSheet や別画面に寄せるのが安全です。どうしても必要な場合は、InfoWindow 全体クリックをトリガーにして「擬似的に操作」へ繋げます。
クラスタをタップした時の挙動が中途半端になる
クラスタは通常「タップでズームインして分解」が期待されます。ここに InfoWindow を混ぜると UX が崩れる場合があるため、次のような設計が無難です。
- クラスタタップはズームに専念(InfoWindow を出さない)
- アイテム(単体ピン)だけ InfoWindow や詳細 UI を出す
トラブルシューティング:最小チェックリスト
| チェック項目 | 確認ポイント | よくある症状 | 対処 |
|---|---|---|---|
| 利用しているクラスタリング実装 | パッケージ名とバージョン | サンプル通りに進まない | Maps Utils を直接使っているか、MAUI 用ラップかを切り分ける |
| MarkerCollection / Renderer の公開有無 | API が見えるか | InfoWindow の設定口が無い | Issue 起票/フォーク/別パッケージへ切り替えを検討 |
| InfoWindow の責務 | 表示だけか、操作も含むか | クリックできない、更新されない | BottomSheet など別 UI に移す |
| 表示内容の参照元 | Title/Snippet/Tag を使えるか | 出し分けできない | Renderer で Tag を埋めるか、設計を変更する |
| 依存関係(Play services / AndroidX) | ビルド・実機で動くか | 参照が競合する、実機で落ちる | 最小プロジェクトで検証し、依存整合を取ってから本体に移植 |
結局どれを選ぶべきか:現実的な判断基準
今回のポイントは明確で、MarkerCollection を使った Xamarin のやり方が「書けない」のは、MAUI の問題ではなく、利用中プラグインの API 不足です。したがって、短期・中期の現実解は次の通りです。
- 短期で確実に進めたい:別パッケージへ切り替える(もしくはフォークで API を追加)
- 中期で改善したい:プラグインに Issue を起票して公式対応を狙う(可能なら PR も出す)
- UX を含めて最適化したい:InfoWindow 前提を見直し、BottomSheet 等へ寄せる
「クラスタ配下のマーカーにカスタム InfoWindow を出したい」場合、技術的には MarkerCollection や Renderer を触れることが重要です。そこが閉じているなら、戦い方(採用ライブラリ or UI 設計)を変えるのが最も費用対効果が高い選択になります。
まとめ:詰まりどころを“自分のコード”と“ライブラリの限界”で切り分ける
.NET MAUI(Android)で Google Maps のクラスタリングを使い、カスタム InfoWindow を出したいのに ClusterManager に MarkerCollection が無い——この状況は、アプリ側の工夫だけで解決しづらいタイプの問題です。まずは「今のプラグインが必要な拡張ポイントを公開しているか」を見極め、公開されていないなら プラグイン対応の依頼か パッケージ切り替えに舵を切るのが近道です。あわせて、InfoWindow の制約を踏まえ、より堅牢な UI(BottomSheet 等)へ寄せる選択肢も持っておくと、地図機能全体の品質が安定します。

コメント