diff --git a/build.gradle.kts b/build.gradle.kts index af80bd2..e170901 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,6 +97,15 @@ dependencies { ), ), ) + // 実機テスト(HereMapViewSaverTest)は MapScheme などを実行時に読むので、テスト APK には同梱する。 + androidTestImplementation( + fileTree( + mapOf( + "dir" to rootDir.resolve("libs").toString(), + "include" to arrayOf("heresdk*.jar", "heresdk*.aar"), + ), + ), + ) if (findProject(":android-sdk-compose") != null) { api(project(":android-sdk-compose")) } else { diff --git a/src/androidTest/java/com/mapconductor/here/HereMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/here/HereMapViewSaverTest.kt new file mode 100644 index 0000000..283b8c5 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/here/HereMapViewSaverTest.kt @@ -0,0 +1,85 @@ +package com.mapconductor.here + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.here.sdk.mapview.MapScheme +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 が [HereMapViewSaver] を通って作り直されても、デザイン(id・ + * attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class HereMapViewSaverTest { + 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 = HereMapDesign.Custom(MapScheme.SATELLITE, rules) + val state = HereViewState(id = "here-1", mapDesignType = design, cameraPosition = camera) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val restored = roundTrip(HereMapViewSaver().createSaver(), state) + + val actual = restored.mapDesignType + assertTrue(actual is HereMapDesign.Custom) + assertEquals(MapScheme.SATELLITE, actual.id) + assertEquals(rules, actual.attributionRules) + assertCommon(restored, "here-1") + } + + @Test + fun keepsPresetDesignAsTheSameObject() { + val state = HereViewState(id = "here-2", mapDesignType = HereMapDesign.LiteNight) + assertSame(HereMapDesign.LiteNight, roundTrip(HereMapViewSaver().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/here/HereMapView.kt b/src/main/java/com/mapconductor/here/HereMapView.kt index 4fcd793..e0b6911 100644 --- a/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/src/main/java/com/mapconductor/here/HereMapView.kt @@ -149,6 +149,10 @@ fun HereMapView( controller.setMapClickListener(onMapClick) controller.setMapLongClickListener(onMapLongClick) state.setController(controller) + // コントローラの既定値(NormalDay)のままリスナーを繋ぐと、それが state へ返り、 + // アプリが渡した(あるいは回転後に復元した)デザインが NormalDay に化ける。 + // 下の loadScene は state のデザインを読むので、その前に揃えておく。 + controller.mapDesignType = state.mapDesignType controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) cameraRestriction?.let { controller.setCameraRestriction(it) } diff --git a/src/main/java/com/mapconductor/here/HereMapViewController.kt b/src/main/java/com/mapconductor/here/HereMapViewController.kt index e2f132d..8150c0d 100644 --- a/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -238,7 +238,7 @@ class HereMapViewController( polygonController.clickListener = listener } - private var mapDesignType: HereMapDesignType = HereMapDesign.NormalDay + internal var mapDesignType: HereMapDesignType = HereMapDesign.NormalDay private var mapDesignTypeChangeListener: HereMapDesignTypeChangeHandler? = null override fun applyUISettings(settings: MapUISettings) { diff --git a/src/main/java/com/mapconductor/here/HereViewStateImpl.kt b/src/main/java/com/mapconductor/here/HereViewStateImpl.kt index e21f0f0..426a0aa 100644 --- a/src/main/java/com/mapconductor/here/HereViewStateImpl.kt +++ b/src/main/java/com/mapconductor/here/HereViewStateImpl.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import com.here.sdk.mapview.MapScheme import com.mapconductor.compose.map.BaseMapViewSaver import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapCameraPositionInterface import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapPaddingsInterface import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateBundle import com.mapconductor.core.map.MapViewStateInterface import java.util.UUID import android.app.Activity @@ -60,28 +62,49 @@ class HereMapViewSaver : BaseMapViewSaver() { state: HereViewState, bundle: Bundle, ) { - bundle.putInt("id", state.mapDesignType.getValue().value) + val design = state.mapDesignType + val scheme = design.getValue().value + bundle.putInt("id", scheme) + // 定義済み(NormalDay など)以外は Custom として戻す。id だけだと Custom の + // attributionRules が落ちる。 + bundle.putBoolean(KEY_CUSTOM, design !== presetOrNull(scheme)) + MapViewStateBundle.putAttributionRules(bundle, design.attributionRules) } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): HereViewState = - HereViewState( + ): HereViewState { + val scheme = mapDesignBundle?.getInt("id", HereMapDesign.NormalDay.id.value) ?: HereMapDesign.NormalDay.id.value + val preset = presetOrNull(scheme) + val mapScheme = MapScheme.values().firstOrNull { it.value == scheme } + val design = + if ((mapDesignBundle?.getBoolean(KEY_CUSTOM) == true || preset == null) && mapScheme != null) { + HereMapDesign.Custom( + id = mapScheme, + attributionRules = MapViewStateBundle.getAttributionRules(mapDesignBundle), + ) + } else { + preset ?: HereMapDesign.NormalDay + } + return HereViewState( id = stateId, - mapDesignType = - HereMapDesign.create( - id = mapDesignBundle?.getInt("id") ?: HereMapDesign.NormalDay.id.value, - ), + mapDesignType = design, cameraPosition = cameraPosition, ) + } + + private fun presetOrNull(scheme: Int): HereMapDesign? = runCatching { HereMapDesign.create(scheme) }.getOrNull() override fun getCameraPaddings(): MapPaddingsInterface? = MapPaddings.Zeros override fun getStateId(state: HereViewState): String = state.id } +// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。 +private const val KEY_CUSTOM = "custom" + @Composable fun rememberHereMapViewState( mapDesign: HereMapDesign = HereMapDesign.NormalDay,