From eed610a108c6d2b8d11b436ef6a5bcaaa54a4479 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 19:25:13 -0800 Subject: [PATCH] Keep the map design across configuration changes - The saver kept only the id and restored through Create(id), so Custom designs lost their attribution rules (and unknown ids became None). It now records whether the design was a preset. - The controller started with None and reported it on listener attach, and onMapLoaded reported a preset rebuilt from mapType. The state now seeds the controller and ignores a report with the same mapType, so a Custom design keeps its rules. - Device round-trip test. --- .../googlemaps/GoogleMapViewSaverTest.kt | 86 +++++++++++++++++++ .../googlemaps/GoogleMapViewStateImpl.kt | 41 ++++++--- 2 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 src/androidTest/java/com/mapconductor/googlemaps/GoogleMapViewSaverTest.kt diff --git a/src/androidTest/java/com/mapconductor/googlemaps/GoogleMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/googlemaps/GoogleMapViewSaverTest.kt new file mode 100644 index 0000000..f183fe7 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/googlemaps/GoogleMapViewSaverTest.kt @@ -0,0 +1,86 @@ +package com.mapconductor.googlemaps + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.android.gms.maps.GoogleMap +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.AttributionRule +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapUISettings +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import android.os.Bundle +import android.os.Parcel + +/** + * 回転などの構成変更で state が [GoogleMapViewSaver] を通って作り直されても、デザイン(id・ + * attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class GoogleMapViewSaverTest { + private val rules = + listOf(AttributionRule("© test"), AttributionRule("© ranged", minZoom = 3, maxZoom = 15)) + + private val camera = + MapCameraPosition( + position = GeoPoint(21.38, -157.93), + zoom = 12.0, + bearing = 20.0, + tilt = 10.0, + ) + + @Test + fun keepsCustomDesignWithAttributionRules() { + val design = GoogleMapDesign.Custom(GoogleMap.MAP_TYPE_SATELLITE, rules) + val state = GoogleMapViewState(id = "google-1", mapDesignType = design, cameraPosition = camera) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val restored = roundTrip(GoogleMapViewSaver().createSaver(), state) + + // 以前は id だけを保存して Create(id) で戻しており、Satellite(規則なし)になっていた。 + val actual = restored.mapDesignType + assertTrue(actual is GoogleMapDesign.Custom) + assertEquals(GoogleMap.MAP_TYPE_SATELLITE, actual.id) + assertEquals(rules, actual.attributionRules) + assertCommon(restored, "google-1") + } + + @Test + fun keepsPresetDesignAsTheSameObject() { + val state = GoogleMapViewState(id = "google-2", mapDesignType = GoogleMapDesign.Hybrid) + assertSame(GoogleMapDesign.Hybrid, roundTrip(GoogleMapViewSaver().createSaver(), state).mapDesignType) + } + + private fun assertCommon( + restored: com.mapconductor.core.map.MapViewStateInterface<*>, + id: String, + ) { + assertEquals(id, restored.id) + assertEquals(false, restored.uiSettings.rotateGesture) + assertEquals(false, restored.uiSettings.tiltGesture) + assertEquals(true, restored.uiSettings.scrollGesture) + assertEquals(12.0, restored.cameraPosition.zoom, 1e-9) + assertEquals(20.0, restored.cameraPosition.bearing, 1e-9) + assertEquals(21.38, restored.cameraPosition.position.latitude, 1e-9) + } + + private fun roundTrip( + saver: Saver, + value: T, + ): T { + val saved = with(saver) { SaverScope { true }.save(value) }!! + // 一度 Parcel に書いて読み戻す(プロセスが落ちたあとの復元と同じ経路)。 + val parcel = Parcel.obtain() + try { + parcel.writeBundle(saved) + parcel.setDataPosition(0) + return saver.restore(parcel.readBundle(javaClass.classLoader)!!)!! + } finally { + parcel.recycle() + } + } +} diff --git a/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt b/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index c86fb25..6183fe2 100644 --- a/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt +++ b/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -9,6 +9,7 @@ import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapCameraPositionInterface import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateBundle import com.mapconductor.core.map.MapViewStateInterface import java.util.UUID import android.os.Bundle @@ -40,13 +41,17 @@ class GoogleMapViewState( internal fun setController(controller: GoogleMapViewControllerInterface) { this.controller = controller -// _mapDesignType.let { -// controller.setMapDesignType(it) -// } + // コントローラが覚えているデザインを state のものにしておく。既定の None のままだと、 + // 変更リスナーを繋いだ時点で None が state へ返ってきて、復元したデザインを潰す。 + // 地図は state の mapType で作られているので、地図の見た目は変わらない。 + controller.setMapDesignType(_mapDesignType) attachController(controller) } internal fun onMapDesignTypeChange(value: GoogleMapDesignType) { + // 地図から読み戻した値は mapType(Int)から作った定義済みデザインで、attributionRules を + // 持たない。同じ mapType なら、アプリが渡した(あるいは復元した)デザインを残す。 + if (value.getValue() == _mapDesignType.getValue()) return _mapDesignType = value } @@ -64,26 +69,42 @@ class GoogleMapViewSaver : BaseMapViewSaver() { state: GoogleMapViewState, bundle: Bundle, ) { - bundle.putInt("id", state.mapDesignType.id) + val design = state.mapDesignType + bundle.putInt("id", design.id) + // 定義済み(Normal など)以外は Custom として戻す。id だけだと Custom の + // attributionRules が落ち、定義に無い id は None になってしまう。 + bundle.putBoolean(KEY_CUSTOM, design !== GoogleMapDesign.toMapDesignType(design.id)) + MapViewStateBundle.putAttributionRules(bundle, design.attributionRules) } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): GoogleMapViewState = - GoogleMapViewState( + ): GoogleMapViewState { + val id = mapDesignBundle?.getInt("id", GoogleMapDesign.Normal.id) ?: GoogleMapDesign.Normal.id + val design = + if (mapDesignBundle?.getBoolean(KEY_CUSTOM) == true) { + GoogleMapDesign.Custom( + id = id, + attributionRules = MapViewStateBundle.getAttributionRules(mapDesignBundle), + ) + } else { + GoogleMapDesign.Create(id) + } + return GoogleMapViewState( id = stateId, - mapDesignType = - GoogleMapDesign.Create( - id = mapDesignBundle?.getInt("id") ?: GoogleMapDesign.Normal.id, - ), + mapDesignType = design, cameraPosition = cameraPosition, ) + } override fun getStateId(state: GoogleMapViewState): String = state.id } +// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。 +private const val KEY_CUSTOM = "custom" + @Composable fun rememberGoogleMapViewState( mapDesign: GoogleMapDesign = GoogleMapDesign.Normal,