Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ android {
compose = true
}

testOptions {
// JVM 単体テストで Compose ランタイムを回すと android.os.Trace を呼ぶ。
// スタブの既定値(何もしない)で通す。InfoBubbleRegistrationTest を参照。
unitTests.isReturnDefaultValues = true
}

publishing {
singleVariant("release") {
withSourcesJar()
Expand Down Expand Up @@ -88,6 +94,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)

testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ androidx-compose-ui-unit = { group = "androidx.compose.ui", name = "ui-unit", ve

# Coroutines
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }

# Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
Expand Down
130 changes: 79 additions & 51 deletions src/main/java/com/mapconductor/compose/info/InfoBubbleCompose.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package com.mapconductor.compose.info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.mapconductor.compose.MapViewScope
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.features.GeoPointInterface
import com.mapconductor.core.marker.DefaultMarkerIcon
import com.mapconductor.core.marker.MarkerIconInterface
import com.mapconductor.core.marker.MarkerState
import java.util.UUID

Expand Down Expand Up @@ -50,27 +53,18 @@ fun MapViewScope.InfoBubble(
)
}

val entry =
InfoBubbleEntry(
id = marker.id,
positionProvider = { marker.position },
// Marker本体もicon未指定時はDefaultMarkerIconを描画するため、InfoBubbleの
// 接続位置計算にも同じアイコン寸法・anchor・infoAnchorを使用する。
icon = marker.icon ?: DefaultMarkerIcon(),
content = wrapped,
)

DisposableEffect(marker) {
val newMap = bubbleFlow.value.toMutableMap()
newMap[entry.id] = entry
bubbleFlow.value = newMap

onDispose {
val newMap = bubbleFlow.value.toMutableMap()
newMap.remove(entry.id)
bubbleFlow.value = newMap
}
}
RegisterInfoBubble(
id = marker.id,
anchorKey = marker,
positionProvider = { marker.position },
// Marker本体もicon未指定時はDefaultMarkerIconを描画するため、InfoBubbleの
// 接続位置計算にも同じアイコン寸法・anchor・infoAnchorを使用する。
// marker.icon はスナップショット状態なので、ここで読めば差し替えで再合成される。
icon = marker.icon,
defaultIcon = true,
tailOffset = Offset(0.5f, 1.0f),
content = wrapped,
)
}

/**
Expand Down Expand Up @@ -110,24 +104,15 @@ fun MapViewScope.InfoBubble(
}

// Re-register when position changes so the bubble moves to the new coordinates.
DisposableEffect(position) {
val entry =
InfoBubbleEntry(
id = id,
positionProvider = { position },
icon = null,
content = wrapped,
)
val newMap = bubbleFlow.value.toMutableMap()
newMap[id] = entry
bubbleFlow.value = newMap

onDispose {
val newMap = bubbleFlow.value.toMutableMap()
newMap.remove(id)
bubbleFlow.value = newMap
}
}
RegisterInfoBubble(
id = id,
anchorKey = position,
positionProvider = { position },
icon = null,
defaultIcon = false,
tailOffset = Offset(0.5f, 1.0f),
content = wrapped,
)
}

/**
Expand All @@ -144,25 +129,68 @@ fun MapViewScope.InfoBubbleCustom(
tailOffset: Offset,
content: @Composable () -> Unit,
) {
RegisterInfoBubble(
id = marker.id,
anchorKey = marker,
positionProvider = { marker.position },
icon = marker.icon,
defaultIcon = true,
tailOffset = tailOffset,
content = content,
)
}

/**
* 吹き出しを [MapViewScope.bubbleFlow] に登録し、中身を常に最新に保つ。
*
* 以前は各関数が `DisposableEffect(marker)` の中で**最初の合成時の** [InfoBubbleEntry] を
* 登録していた。エントリの `content` はそのときのラムダで、色や中身の値を捕まえたまま
* 固まるので、`bubbleColor` やラムダに渡した値、マーカーのアイコンが後から変わっても
* 画面の吹き出しは古いままだった。
*
* ここではエントリを作り直さず、`content` を「最新のラムダを読んで呼ぶ」ラムダにする
* ([rememberUpdatedState])。中身や見た目が変わっても再合成されるのは吹き出しの中だけで、
* `bubbleFlow` は鳴らない。位置・アイコン・接続点のように**どこに出すか**が変わったときだけ
* エントリを差し替える。
*
* @param anchorKey 位置を決めるもの(マーカーまたは座標)。変わったら登録し直す。
* @param icon 接続位置の計算に使うアイコン。[defaultIcon] が true で null なら既定のピン。
*/
@Composable
private fun MapViewScope.RegisterInfoBubble(
id: String,
anchorKey: Any,
positionProvider: () -> GeoPointInterface,
icon: MarkerIconInterface?,
defaultIcon: Boolean,
tailOffset: Offset,
content: @Composable () -> Unit,
) {
val currentContent = rememberUpdatedState(content)
val entry =
InfoBubbleEntry(
id = marker.id,
positionProvider = { marker.position },
icon = marker.icon ?: DefaultMarkerIcon(),
tailOffset = tailOffset,
content = content,
)
remember(id, anchorKey, icon, tailOffset) {
InfoBubbleEntry(
id = id,
positionProvider = positionProvider,
icon = icon ?: if (defaultIcon) DefaultMarkerIcon() else null,
tailOffset = tailOffset,
content = { currentContent.value() },
)
}

// Re-register when marker or tail changes
DisposableEffect(marker, tailOffset) {
DisposableEffect(entry) {
val newMap = bubbleFlow.value.toMutableMap()
newMap[entry.id] = entry
bubbleFlow.value = newMap

onDispose {
val newMap = bubbleFlow.value.toMutableMap()
newMap.remove(entry.id)
bubbleFlow.value = newMap
// 差し替えでは新しいエントリが同じ id で入る。消すのは自分が入れたものだけ。
val current = bubbleFlow.value
if (current[entry.id] === entry) {
val newMap = current.toMutableMap()
newMap.remove(entry.id)
bubbleFlow.value = newMap
}
}
}
}
Loading
Loading