カメラの位置を読む
MapConductor のコントローラに getCameraPosition() はありません。3 プラットフォームとも同じです。地図を触っていて最初に探すメソッドなので、代わりに何を使うのか、そしてなぜ用意していないのかをここに書いておきます。
ひとことで言うと、カメラは取りに行くものではなく、届くものだからです。地図 SDK 側がカメラの変化を知っていて、こちらは受け取るだけで済みます。
01 · 3 つの経路
用途によって使い分けます。どれも同じ MapCameraPosition が届き、表示範囲は visibleRegion に載っています。
mapViewState.cameraPosition — いつでも読めます。表示範囲は cameraPosition.visibleRegion.bounds。onCameraMove / onCameraMoveEnd — 移動中も発火します。UI を追従させるならこれ。onCameraChanged を受け取ります。地図コントローラの単一スロットのリスナーを奪い合わずに済みます。val camera = mapViewState.cameraPosition
val bounds = camera.visibleRegion?.bounds
// 変化を追う
MapView(
state = mapViewState,
onCameraMoveEnd = { camera -> reload(camera.visibleRegion?.bounds) },
)let camera = mapViewState.cameraPosition
let bounds = camera.visibleRegion?.bounds
// 変化を追う
MapLibreMapView(state: mapViewState, onCameraMoveEnd: { camera in
reload(camera.visibleRegion?.bounds)
})const camera = mapViewState.cameraPosition;
const bounds = camera.visibleRegion?.bounds;
// 変化を追う
<MapLibreMapView
state={mapViewState}
onCameraMoveEnd={(camera) => reload(camera.visibleRegion?.bounds)}
/>02 · なぜ取得メソッドが無いのか
MapConductor は 3 プラットフォームとも宣言的 UI(Compose / SwiftUI / React)の上に載っています。宣言的 UI では、状態は「今どうなっているか」を保持する場所が 1 つあって、UI はそれを写すだけ、という形にします。カメラも状態なので、置き場所は state です。
コントローラに取得メソッドを足すと、同じ値の出どころが 2 つになります。片方は state、もう片方は SDK 直読み。両者は同じであるべきなのに、ズレる余地だけが増えます。実際にそうなっていて、削除前は同じフレームで 2 回カメラを組み立てていました ― 1 回は SDK のイベント用、もう 1 回は再描画のたびに作り直していた分です。
地図 SDK のカメライベント
↓ プロバイダが SDK の生値を読み、統一ズームへ変換し、visibleRegion を載せる
state.updateCameraPosition(camera)
↓
├─ mapViewState.cameraPosition (いつでも読める)
├─ onCameraMove / onCameraMoveEnd (アプリのコールバック)
└─ 登録済みオーバーレイの onCameraChanged (拡張モジュール)03 · pull のコスト
設計の話だけだと「1 個くらい足しても」となりがちなので、実測を置いておきます。カメラを 1 回組み立てるのは安くありません。
実測値です。MapLibre のマーカーページを Chromium で開き、1 回のドラッグ(約 20 フレーム分のパン)で getCameraPosition() が何回呼ばれるかを数えました。
約 65% 減りました。減った分はすべて作り直しです。SDK のイベントで 1 回組み立てて state へ流したのに、再描画のたびにビューがもう一度同じ値を組み立てていました。呼び出し元は 13 のビューすべてに入っていて、どれも直前に push したばかりの値を捨てていたことになります。
なお静止中は 0 回です。暴走していたわけではなく、操作している間だけ確実に効いてくる種類のコストでした。地図がいちばん重い瞬間に上乗せされるので、体感に出やすいところです。
var camera by remember { mutableStateOf<MapCameraPosition?>(null) }
MapLibreMapView(
state = mapViewState,
modifier = Modifier.weight(1f),
// 動いている間ずっと届く。ここで重い処理をしないこと
onCameraMove = { position -> camera = position },
// 止まったところで 1 回。取得や再描画はこちらに置く
onCameraMoveEnd = { position -> viewModel.onCameraSettled(position) },
)
camera?.let { position ->
Text("lat %.5f".format(position.position.latitude))
Text("lng %.5f".format(position.position.longitude))
Text("zoom %.2f".format(position.zoom))
// 表示範囲は組み立て直さず、届いた値から取る
position.visibleRegion?.bounds?.let { bounds ->
Text("SW ${bounds.southWest} NE ${bounds.northEast}")
}
}04 · 1 フレームの遅れ
mapViewState.cameraPosition は「最後に push された値」なので、理屈のうえでは SDK の現在値より 1 フレーム古いことがあります。ここが取得メソッドを足したくなる唯一の実質的な理由なので、正直に書いておきます。
onCameraMove は移動中も毎フレーム発火するので、state は常に直前のフレームまで追いついています。表示・データ取得・範囲判定のいずれも体感差はありません。実際、取得メソッドを外したあとも全 41 件のブラウザテストがそのまま通っています。05 · それでも生値が要るとき
statе 経由では足りない ― たとえば地図 SDK の描画ループに同期したい ― という場合は、統一 API を回り道せずに MapViewHolder でネイティブの地図インスタンスを取り出してください。プロバイダ固有のコードになりますが、そのぶん一番速く、一番正確です。
const holder = mapViewState.getMapViewHolder(); const map = holder?.map as maplibregl.Map | undefined; const liveZoom = map?.getZoom();