From 720a139d625acc9564c7c2d21b50d316a568722d Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 19:25:12 -0800 Subject: [PATCH] Save uiSettings and camera paddings across configuration changes The Compose BaseMapViewSaver, which every android-for-* saver extends, did not save uiSettings, so after a rotation every gesture flag came back true. It now saves uiSettings and camera paddings through core's MapViewStateBundle. Adds a device round-trip test. --- .../map/BaseMapViewSaverRoundTripTest.kt | 125 ++++++++++++++++++ .../compose/map/BaseMapViewSaver.kt | 44 +++--- 2 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 src/androidTest/java/com/mapconductor/compose/map/BaseMapViewSaverRoundTripTest.kt diff --git a/src/androidTest/java/com/mapconductor/compose/map/BaseMapViewSaverRoundTripTest.kt b/src/androidTest/java/com/mapconductor/compose/map/BaseMapViewSaverRoundTripTest.kt new file mode 100644 index 0000000..977b2bb --- /dev/null +++ b/src/androidTest/java/com/mapconductor/compose/map/BaseMapViewSaverRoundTripTest.kt @@ -0,0 +1,125 @@ +package com.mapconductor.compose.map + +import androidx.compose.runtime.saveable.SaverScope +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.AttributionRule +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapDesignTypeInterface +import com.mapconductor.core.map.MapPaddings +import com.mapconductor.core.map.MapUISettings +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateBundle +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import android.os.Bundle +import android.os.Parcel + +/** + * プロバイダの `*MapViewSaver` が継承する [BaseMapViewSaver] が、uiSettings とカメラ + * (paddings を含む)を落とさないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class BaseMapViewSaverRoundTripTest { + @Test + fun keepsUiSettingsCameraAndDesignRules() { + val state = + FakeState( + id = "compose-state", + design = FakeDesign("d", listOf(AttributionRule("© test", minZoom = 2))), + camera = + MapCameraPosition( + position = GeoPoint(-33.86, 151.21), + zoom = 9.0, + bearing = 90.0, + tilt = 30.0, + paddings = MapPaddings(top = 5.0, left = 6.0, bottom = 7.0, right = 8.0), + ), + ) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val saver = FakeSaver().createSaver() + val saved = with(saver) { SaverScope { true }.save(state) }!! + val restored = saver.restore(throughParcel(saved))!! + + assertEquals("compose-state", restored.id) + assertEquals("d", restored.mapDesignType.id) + assertEquals( + "© test", + restored.mapDesignType.attributionRules + .single() + .attribution, + ) + assertEquals( + 2, + restored.mapDesignType.attributionRules + .single() + .minZoom, + ) + assertEquals(true, restored.uiSettings.scrollGesture) + assertEquals(true, restored.uiSettings.zoomGesture) + assertEquals(false, restored.uiSettings.rotateGesture) + assertEquals(false, restored.uiSettings.tiltGesture) + assertEquals(-33.86, restored.cameraPosition.position.latitude, 1e-9) + assertEquals(151.21, restored.cameraPosition.position.longitude, 1e-9) + assertEquals(9.0, restored.cameraPosition.zoom, 1e-9) + assertEquals(90.0, restored.cameraPosition.bearing, 1e-9) + assertEquals(30.0, restored.cameraPosition.tilt, 1e-9) + assertEquals(5.0, restored.cameraPosition.paddings!!.top, 1e-9) + assertEquals(8.0, restored.cameraPosition.paddings!!.right, 1e-9) + } + + private class FakeDesign( + override val id: String, + override val attributionRules: List, + ) : MapDesignTypeInterface { + override fun getValue(): String = id + } + + private class FakeState( + override val id: String, + design: FakeDesign, + camera: MapCameraPosition = MapCameraPosition.Default, + ) : MapViewState(camera) { + override var mapDesignType: FakeDesign = design + } + + private class FakeSaver : BaseMapViewSaver() { + override fun saveMapDesign( + state: FakeState, + bundle: Bundle, + ) { + bundle.putString("id", state.mapDesignType.id) + MapViewStateBundle.putAttributionRules(bundle, state.mapDesignType.attributionRules) + } + + override fun createState( + stateId: String, + mapDesignBundle: Bundle?, + cameraPosition: MapCameraPosition, + ): FakeState = + FakeState( + id = stateId, + design = + FakeDesign( + id = mapDesignBundle?.getString("id") ?: "", + attributionRules = MapViewStateBundle.getAttributionRules(mapDesignBundle), + ), + camera = cameraPosition, + ) + + override fun getStateId(state: FakeState): String = state.id + } + + private fun throughParcel(bundle: Bundle): Bundle { + val parcel = Parcel.obtain() + try { + parcel.writeBundle(bundle) + parcel.setDataPosition(0) + return parcel.readBundle(javaClass.classLoader)!! + } finally { + parcel.recycle() + } + } +} diff --git a/src/main/java/com/mapconductor/compose/map/BaseMapViewSaver.kt b/src/main/java/com/mapconductor/compose/map/BaseMapViewSaver.kt index e1fb919..6b7f35a 100644 --- a/src/main/java/com/mapconductor/compose/map/BaseMapViewSaver.kt +++ b/src/main/java/com/mapconductor/compose/map/BaseMapViewSaver.kt @@ -1,16 +1,25 @@ package com.mapconductor.compose.map import androidx.compose.runtime.saveable.Saver -import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.InternalMapConductorApi import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapPaddingsInterface +import com.mapconductor.core.map.MapViewStateBundle import com.mapconductor.core.map.MapViewStateInterface import android.os.Bundle /** * Base class for MapView state savers + * + * カメラ(paddings を含む)と [MapViewStateInterface.uiSettings] はここで保存・復元する。 + * デザインは型がプロバイダごとに違うので [saveMapDesign] / [createState] が受け持つ。 + * デザインの `attributionRules` を含め、アプリが設定したものは全部書くこと + * (`MapViewStateBundle.putAttributionRules` を使う)。書かなかった値は回転などの + * 構成変更のあと黙って既定値に戻る。 + * * @param T MapViewStateInterface type */ +@OptIn(InternalMapConductorApi::class) abstract class BaseMapViewSaver> { /** * Save map design type to bundle @@ -50,6 +59,7 @@ abstract class BaseMapViewSaver> { putString("stateId", getStateId(state)) putBundle("mapDesign", mapDesignBundle) putBundle("camera", cameraStateBundle) + putBundle(MapViewStateBundle.KEY_UI_SETTINGS, MapViewStateBundle.uiSettingsToBundle(state.uiSettings)) } }, restore = { storedData -> @@ -59,7 +69,14 @@ abstract class BaseMapViewSaver> { val cameraPosition = createCameraPositionFromBundle(cameraBundle) - createState(stateId, mapDesignBundle, cameraPosition) + createState(stateId, mapDesignBundle, cameraPosition).also { state -> + // uiSettings はどのプロバイダでも MapViewStateInterface の var なので、 + // createState に渡さずここで戻す(以前は保存しておらず、復元後は + // 全部 true の既定値に戻っていた)。 + MapViewStateBundle.uiSettingsFromBundle(storedData.getBundle(MapViewStateBundle.KEY_UI_SETTINGS))?.let { + state.uiSettings = it + } + } }, ) @@ -68,27 +85,8 @@ abstract class BaseMapViewSaver> { */ protected abstract fun getStateId(state: T): String - private fun createCameraBundle(state: T): Bundle = - state.cameraPosition.let { - Bundle().apply { - putDouble("zoom", it.zoom) - putDouble("tilt", it.tilt) - putDouble("bearing", it.bearing) - putDouble("latitude", it.position.latitude) - putDouble("longitude", it.position.longitude) - } - } + private fun createCameraBundle(state: T): Bundle = MapViewStateBundle.cameraToBundle(state.cameraPosition) private fun createCameraPositionFromBundle(cameraBundle: Bundle?): MapCameraPosition = - MapCameraPosition( - position = - GeoPoint.fromLatLong( - latitude = cameraBundle?.getDouble("latitude") ?: 0.0, - longitude = cameraBundle?.getDouble("longitude") ?: 0.0, - ), - zoom = cameraBundle?.getDouble("zoom") ?: 0.0, - bearing = cameraBundle?.getDouble("bearing") ?: 0.0, - tilt = cameraBundle?.getDouble("tilt") ?: 0.0, - paddings = getCameraPaddings(), - ) + MapViewStateBundle.cameraFromBundle(cameraBundle, fallbackPaddings = getCameraPaddings()) }