diff --git a/api/android-for-googlemaps.api b/api/android-for-googlemaps.api index 7e5265a..1d47c50 100644 --- a/api/android-for-googlemaps.api +++ b/api/android-for-googlemaps.api @@ -329,6 +329,10 @@ public final class com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverla } final class com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer$Companion { } +final class com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer$DetachableTileProvider implements com.google.android.gms.maps.model.TileProvider { + public com.google.android.gms.maps.model.Tile getTile(int, int, int); + public final void detach(); +} final class com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer$TileSpec { public final java.lang.String getTemplate(); public final int getTileSize(); diff --git a/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index d24b239..22f98e1 100644 --- a/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -43,7 +43,6 @@ import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import com.mapconductor.googlemaps.raster.GoogleMapRasterLayerController import com.mapconductor.googlemaps.raster.GoogleMapRasterLayerOverlayRenderer import okhttp3.OkHttpClient -import android.view.ViewGroup import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine @@ -142,7 +141,6 @@ fun GoogleMapView( // Specific Google Maps DisposableEffect logic val lifecycle = LocalLifecycleOwner.current.lifecycle // Get lifecycle here DisposableEffect(lifecycle) { - val stateId = state.id val observer = object : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { @@ -152,22 +150,6 @@ fun GoogleMapView( override fun onPause(owner: LifecycleOwner) { holderRef.value?.mapView?.onPause() } - - override fun onDestroy(owner: LifecycleOwner) { - val activity = context.findActivity() - if (activity?.isChangingConfigurations == true) { - holderRef.value?.mapView?.let { - (it.parent as? ViewGroup)?.removeView(it) - it.onDestroy() - } - } else { - // Real teardown (not a configuration change): free the - // controller's overlay controllers (tile-server routes, - // marker managers) and cancel its coroutine scope. - GoogleMapViewControllerStore.get(stateId)?.destroy() - GoogleMapViewControllerStore.remove(stateId) - } - } } lifecycle.addObserver(observer) onDispose { @@ -175,6 +157,10 @@ fun GoogleMapView( } } }, + // The MapView keeps an SDK thread whose listeners reach the controller, + // so it has to be destroyed when the map leaves -- a provider switch as + // much as an Activity going away. + onDestroyMapView = { mapView -> mapView.onDestroy() }, sdkInitialize = { sdkInitialize?.invoke(context) ?: true }, diff --git a/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt b/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index 6183fe2..5df8c95 100644 --- a/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt +++ b/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -37,10 +37,13 @@ class GoogleMapViewState( this.controller?.setMapDesignType(value) } get() = _mapDesignType - private var controller: GoogleMapViewControllerInterface? = null + + // Read through the base so that letting go of the controller there (MapViewBase + // does it when the map is torn down) leaves nothing behind here. + private val controller: GoogleMapViewControllerInterface? + get() = attachedMapController as? GoogleMapViewControllerInterface internal fun setController(controller: GoogleMapViewControllerInterface) { - this.controller = controller // コントローラが覚えているデザインを state のものにしておく。既定の None のままだと、 // 変更リスナーを繋いだ時点で None が state へ返ってきて、復元したデザインを潰す。 // 地図は state の mapType で作られているので、地図の見た目は変わらない。 diff --git a/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerController.kt b/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerController.kt index c004bd7..9f73fef 100644 --- a/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerController.kt +++ b/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerController.kt @@ -32,7 +32,7 @@ internal class GoogleMapRasterLayerController( internal fun onCameraIdle() { tileRenderer.refreshLocalTilesAfterZoom() } override fun destroy() { - tileRenderer.cancelLocalRequests() + tileRenderer.removeAllLayers() super.destroy() } } diff --git a/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerOverlayRenderer.kt index f5dd859..c561856 100644 --- a/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/googlemaps/raster/GoogleMapRasterLayerOverlayRenderer.kt @@ -45,6 +45,13 @@ class GoogleMapRasterLayerOverlayRenderer( /** 差し替え済みで、新しい方のタイルが揃うのを待って外す古いオーバーレイ。 */ private val retiring = mutableSetOf() + /** + * 各オーバーレイに渡したタイルプロバイダ。SDK はオーバーレイを外しても、地図を + * 破棄しても、渡されたプロバイダをプロセス共有の表に残す(実測)。そこから + * このレンダラ・ホルダー・MapView まで辿れてしまうので、外すときに中身を切り離す。 + */ + private val providers = mutableMapOf() + internal fun updateLocalZoom(zoom: Double) { localRequests.values.forEach { it.updateZoom(zoom) } } @@ -62,6 +69,27 @@ class GoogleMapRasterLayerOverlayRenderer( localRequests.clear() } + /** + * Takes every overlay this renderer added off the map. Called when the map + * goes away: the SDK keeps a tile overlay's provider in a process-wide + * table until the overlay is removed, and the provider reaches this + * renderer and through it the map view. Must run on the main thread, before + * the MapView is destroyed. + */ + internal fun removeAllLayers() { + cancelLocalRequests() + (tileActivity.keys + retiring).forEach { overlay -> runCatching { overlay.remove() } } + tileActivity.clear() + retiring.clear() + providers.values.forEach { it.detach() } + providers.clear() + } + + private fun forget(overlay: TileOverlay) { + tileActivity.remove(overlay) + providers.remove(overlay)?.detach() + } + private fun isMarkerTileRaster(state: RasterLayerState): Boolean = state.id.startsWith(MARKER_TILE_RASTER_ID_PREFIX) private fun resolveOverlayZIndex(state: RasterLayerState): Float = @@ -104,10 +132,13 @@ class GoogleMapRasterLayerOverlayRenderer( withContext(coroutine.coroutineContext) { data.forEach { entity -> localRequests.remove(entity.layer)?.close() - tileActivity.remove(entity.layer) + forget(entity.layer) entity.layer.remove() } - retiring.forEach { it.remove() } + retiring.forEach { + forget(it) + it.remove() + } retiring.clear() } } @@ -136,7 +167,7 @@ class GoogleMapRasterLayerOverlayRenderer( delay(RETIRE_POLL_MS) } if (retiring.remove(old)) { - tileActivity.remove(old) + forget(old) old.remove() } } @@ -256,14 +287,18 @@ class GoogleMapRasterLayerOverlayRenderer( } } } + val detachable = DetachableTileProvider(provider) val options = TileOverlayOptions() - .tileProvider(provider) + .tileProvider(detachable) .zIndex(resolveOverlayZIndex(state)) .transparency(opacityToTransparency(state.opacity)) .visible(state.visible) val overlay = holder.map.addTileOverlay(options) - if (overlay != null) tileActivity[overlay] = activity + if (overlay != null) { + tileActivity[overlay] = activity + providers[overlay] = detachable + } if (requests != null) { if (overlay != null) localRequests[overlay] = requests else requests.close() } @@ -304,6 +339,21 @@ class GoogleMapRasterLayerOverlayRenderer( private fun opacityToTransparency(opacity: Float): Float = (1.0f - opacity.coerceIn(0.0f, 1.0f)).coerceIn(0.0f, 1.0f) + /** SDK に渡すのはこれ。[detach] したあとは何も返さず、元のプロバイダも掴まない。 */ + private class DetachableTileProvider( + @Volatile private var delegate: TileProvider?, + ) : TileProvider { + override fun getTile( + x: Int, + y: Int, + zoom: Int, + ): Tile? = delegate?.getTile(x, y, zoom) ?: NO_TILE + + fun detach() { + delegate = null + } + } + private data class TileSpec( val template: String, val tileSize: Int,