ドキュメント / 基本 / Info Bubble

情報バブル(InfoBubble)

マーカーに紐づく吹き出しを、各プラットフォームの UI コードでそのまま書けます。中身は Compose の Composable、SwiftUI の View、React の要素。位置合わせとしっぽの描画だけを SDK が担当します。

ANDROID
com.mapconductor.compose.info
iOS
MapConductorCore
REACT
@mapconductor/js-sdk-react

01 · しくみ

3プラットフォーム共通で「選択中のマーカーを状態として持ち、選択されている間だけバブルを描く」という組み立てです。バブルの開閉は SDK ではなくアプリ側の state が決めます。

STEP 01
選択状態を持つ

選択中のマーカー(または ID)を state として保持します。複数同時表示なら Set を使います。

STEP 02
地図の子として置く

Marker と InfoBubble を地図コンテナの子として並べ、選択中のときだけ InfoBubble を描画します。

STEP 03
地図タップで閉じる

地図の onMapClick で選択を解除します。マーカーの onClick が選択をセットします。

サンプル · マーカーを選択してバブルを開いたところ

02 · 4つのパターン

サンプルアプリの infobubble 配下にある4ページが、そのまま4つの実装パターンになっています。パターンとプラットフォームを選ぶと、対応するサンプルのコードが表示されます。

プラットフォーム
テキスト1行

最小構成。marker の extra に入れた文字列を1行で表示します。枠・しっぽ・余白は既定値のままなので、書くのは中身のテキストだけです。

SimpleTextBubblePage.kt
var selectedMarker by remember { mutableStateOf<MarkerState?>(null) }
val markerState = remember {
    MarkerState(
        position = GeoPoint.fromLatLong(37.7749, -122.4194),
        icon = DefaultMarkerIcon(fillColor = Color.Blue, label = "SF"),
        extra = "San Francisco - The Golden Gate City",
        onClick = { selectedMarker = it },
    )
}

MapViewContainer(
    modifier = Modifier.fillMaxSize(),
    state = mapViewState,
    onMapClick = { selectedMarker = null },
    onMapLoaded = { selectedMarker = markerState },
) {
    Marker(markerState)

    selectedMarker?.let { marker ->
        InfoBubble(marker = marker) {
            Text(
                text = marker.extra as? String ?: "No information",
                color = MaterialTheme.colorScheme.primary,
                modifier = Modifier.padding(4.dp),
            )
        }
    }
}
見え方
San Francisco - The Golden Gate City
SF

既定のしっぽはマーカーの真上(tailOffset x:0.5 / y:1.0)に固定されます。

スタイル変更

枠のスタイルを変えるパターン。色や角丸を変えるだけなら Android・React は個別の引数、iOS は InfoBubbleStyle で渡します。枠としっぽごと自前で描くなら 3 プラットフォームとも InfoBubbleCustom を使います(Compose は Canvas、SwiftUI は Shape、React は CSS の ::before / ::after)。

StyledInfoBubblePage.kt
val markerState1 by remember {
    mutableStateOf(
        MarkerState(
            id = "marker1",
            position = GeoPoint.fromLatLong(37.7749, -122.4194),
            icon = DefaultMarkerIcon(
                fillColor = Color.Blue,
                infoAnchor = Offset(0.5f, 0.25f),
                label = "1",
            ),
            draggable = true,
            onClick = onMarkerClick,
        ),
    )
}

selectedMarker?.let { marker ->
    val text = GeoPoint.from(marker.position).toUrlValue(6)
    InfoBubbleCustom(
        marker = marker,
        tailOffset = Offset(0f, 0.5f), // 左中央で接続
    ) {
        RightTailInfoBubble(
            bubbleColor = Color.White,
            borderColor = Color.Black,
        ) {
            Text(text = text, color = MaterialTheme.colorScheme.primary)
        }
    }
}
StyledInfoBubblePage.kt · RightTailInfoBubble
@Composable
private fun RightTailInfoBubble(
    bubbleColor: Color,
    borderColor: Color,
    contentPadding: Dp = 8.dp,
    cornerRadius: Dp = 4.dp,
    tailSize: Dp = 8.dp,
    content: @Composable () -> Unit,
) {
    Box(modifier = Modifier.wrapContentSize()) {
        Canvas(modifier = Modifier.matchParentSize()) {
            val path = Path().apply {
                // 角丸の矩形を描き、左辺の中央に三角のしっぽを足す
                lineTo(tail, height / 2 + tail / 2)
                lineTo(0f, height / 2)
                lineTo(tail, height / 2 - tail / 2)
                close()
            }
            drawPath(path, color = bubbleColor, style = Fill)
            drawPath(path, color = borderColor, style = Stroke(width = 2f))
        }
        Box(modifier = Modifier.padding(start = contentPadding + tailSize)) {
            content()
        }
    }
}
見え方
1
37.7749,-122.419404

接続点は tailOffset(バブル側)とアイコンの infoAnchor(マーカー側)で決まります。

リッチコンテンツ

extra にオブジェクトを入れ、見出し・説明・評価を含むレイアウトを埋め込みます。枠は bubbleColor / borderColor / contentPadding / cornerRadius で調整でき、中身のコードは内容のレイアウトだけに集中できます。

RichContentBubblePage.kt
data class LocationInfo(
    val name: String,
    val description: String,
    val rating: Float,
) : Serializable

selectedMarker?.let { marker ->
    val info = marker.extra as? LocationInfo ?: return@let
    InfoBubble(
        marker = marker,
        bubbleColor = if (isDarkTheme) Color.Black else Color.White,
        borderColor = if (isDarkTheme) Color.Gray else Color.Black,
        contentPadding = 16.dp,
        cornerRadius = 12.dp,
    ) {
        Column(modifier = Modifier.width(200.dp)) {
            Text(info.name, style = MaterialTheme.typography.headlineSmall,
                fontWeight = FontWeight.Bold)
            Spacer(Modifier.height(8.dp))
            Text(info.description, style = MaterialTheme.typography.bodyMedium)
            Spacer(Modifier.height(8.dp))
            Row(verticalAlignment = Alignment.CenterVertically) {
                repeat(5) { index ->
                    Icon(Icons.Default.Star, contentDescription = null,
                        tint = if (index < info.rating.toInt()) Color.Yellow else Color.Gray,
                        modifier = Modifier.size(16.dp))
                }
                Text(" ${info.rating}/5", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}
見え方
Golden Gate Park

A large urban park with gardens, museums, and recreational areas.

★★★★4.5/5
🌳

ダークテーマ時に枠と背景色を差し替える例も、Android / iOS のサンプルに含まれています。

複数同時表示

選択を Set で持つと、複数のバブルを同時に開いたままにできます。開いているマーカーだけ InfoBubble を描き、中身にタップ操作を付ければバブル自身で閉じられます。

MultipleBubblesPage.kt
var selectedMarkers by remember { mutableStateOf(setOf<String>()) }

val onMarkerClick: OnMarkerEventHandler = { markerState ->
    selectedMarkers =
        if (selectedMarkers.contains(markerState.id)) {
            selectedMarkers - markerState.id
        } else {
            selectedMarkers + markerState.id
        }
}

MapViewContainer(
    state = mapViewState,
    onMapClick = { selectedMarkers = emptySet() },
) {
    markerStates.forEach { markerState ->
        Marker(markerState)

        if (selectedMarkers.contains(markerState.id)) {
            InfoBubble(
                marker = markerState,
                bubbleColor = Color.White,
                borderColor = Color.Black,
            ) {
                Column(modifier = Modifier.clickable(true, onClick = {
                    selectedMarkers = selectedMarkers - markerState.id
                })) {
                    Text(markerState.extra as String, fontWeight = FontWeight.Bold)
                    Text("Tap to close", color = Color.Gray)
                }
            }
        }
    }
}
見え方
Restaurant ATap to close
1
Hotel BTap to close
2
Shop CTap to close
3

地図タップで全部閉じる(空の Set に戻す)動作も各サンプルに含まれています。

03 · API

InfoBubble はしっぽ付きの枠を描く高レベルなコンポーネント、InfoBubbleCustom は位置合わせだけを行う低レベルなコンポーネントです。iOS はスタイル引数を InfoBubbleStyle にまとめて渡します。

PARAM
ANDROID
REACT
説明
marker
必須
必須
バブルを紐づけるマーカーの状態。位置とアイコンサイズから接続点を計算します。
bubbleColor
Color.White
'#ffffff'
バブルの背景色。
borderColor
Color.Black
'#000000'
枠線の色。
contentPadding
8.dp
8
中身の内側余白。
cornerRadius
4.dp
4
角丸の半径。
tailSize
8.dp
8
しっぽの大きさ。
content / children
必須
必須
バブルの中身。各プラットフォームの UI コードをそのまま渡します。
InfoBubbleStyle · iOS

iOS は個別の引数ではなく、この構造体をまとめて style に渡します。

InfoBubbleStyle(
  bubbleColor: Color,
  borderColor: Color,
  contentPadding: CGFloat,
  cornerRadius: CGFloat,
  tailSize: CGFloat
)
InfoBubbleCustom

枠もしっぽも自前で描くパターン。SDK は位置合わせだけを担当します。

marker: MarkerState
tailOffset: Offset   // ios: CGPoint
content / children
InfoBubble (position)

マーカーではなく座標に直接バブルを出します。3プラットフォームとも InfoBubble に position を渡す形(Compose・iOS では InfoBubble(position:) のオーバーロード)で統一されています。

position: GeoPoint
// react: <InfoBubble position=… />
// compose / ios: InfoBubble(position:)

04 · 注意点

アイコン未指定でも重ならない

icon を渡さないマーカーは既定のピン(48px・下端が基準)として扱われ、その実サイズ分だけバブルがずれます。マーカーと重なることはありません。

infoAnchor でずらす

アイコン側の接続点は infoAnchor(Compose は Offset、React は x/y)で変えられます。ドラッグ中もバブルは追随します。

バブルの中身はイベントを受け取れる

Compose の clickable、SwiftUI のジェスチャ、React の onClick をそのまま置けます。地図の onMapClick とは独立に動くので、閉じる操作を中身側に持たせられます。

関連ページ