Skip to content
Open
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ build/
.env

.build/
.swiftpm/
.swiftpm/

# Local Android engine debugging artifacts
engine_patches/
example/android/engine_patch_repo/
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.kakao.vectormap.KakaoMapReadyCallback
import com.kakao.vectormap.LatLng
import com.kakao.vectormap.MapType
import com.kakao.vectormap.MapViewInfo
import com.kakao.vectormap.camera.CameraPosition
import kr.yhs.flutter_kakao_maps.converter.CameraTypeConverter.asLatLng

data class KakaoMapOption(
Expand Down Expand Up @@ -40,6 +41,9 @@ data class KakaoMapOption(
onReady = method
}

fun copyWithCameraPosition(cameraPosition: CameraPosition): KakaoMapOption =
copy(initialPosition = cameraPosition.position, zoomLevel = cameraPosition.zoomLevel)

companion object {
fun fromMessageable(onReady: ((KakaoMap) -> Unit), rawArgs: Map<String, Any?>): KakaoMapOption {
return KakaoMapOption(
Expand Down
196 changes: 186 additions & 10 deletions android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,48 @@ import android.app.Application
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.kakao.vectormap.KakaoMap
import com.kakao.vectormap.MapView
import io.flutter.plugin.common.MethodChannel
import com.kakao.vectormap.camera.CameraPosition
import com.kakao.vectormap.camera.CameraUpdateFactory
import io.flutter.plugin.platform.PlatformView
import kr.yhs.flutter_kakao_maps.controller.KakaoMapController
import kr.yhs.flutter_kakao_maps.model.KakaoMapOption

class KakaoMapView(
private val activity: Activity,
private val context: Context,
context: Context,
private val controller: KakaoMapController,
private val viewId: Int,
private val option: KakaoMapOption,
private val channel: MethodChannel,
private val recreateMapViewOnResume: Boolean,
recreateMapViewDelayMillis: Long,
private val recoverGLSurfaceViewOnResume: Boolean,
) : PlatformView, Application.ActivityLifecycleCallbacks {
private val mapView = MapView(activity)
private val container = FrameLayout(context)
private val recreateDelayMillis = recreateMapViewDelayMillis.coerceAtLeast(0L)
private var mapView = createMapView(option)
private lateinit var kakaoMap: KakaoMap
private var lastCameraPosition: CameraPosition? = null
private var pendingCameraPosition: CameraPosition? = null
private var isActivityResumed = false
private var wasActivityPaused = false
private var isDisposed = false
private var pendingRecreateRunnable: Runnable? = null
private var pendingRecovery = false
private var pendingRecoveryRunnable: Runnable? = null
private var pendingRecoveryTimeoutRunnable: Runnable? = null

init {
controller.mapView = mapView
mapView.start(controller, option)
container.addView(mapView, matchParentLayoutParams())
activity.application.registerActivityLifecycleCallbacks(this)
}

override fun getView(): View = mapView
override fun getView(): View = container

override fun dispose() {
activity.application.unregisterActivityLifecycleCallbacks(this)
mapView.finish()
disposeMapView()
controller.dispose()
}

Expand All @@ -44,11 +57,41 @@ class KakaoMapView(

override fun onActivityResumed(activity: Activity) {
if (activity != this.activity) return
val pendingRecreate = recreateMapViewOnResume && wasActivityPaused
val recoveryActive = recoverGLSurfaceViewOnResume && !recreateMapViewOnResume && wasActivityPaused
isActivityResumed = true
if (pendingRecreate) {
wasActivityPaused = false
scheduleMapViewRecreation()
return
}
Comment on lines +60 to +67
if (recoveryActive) {
wasActivityPaused = false
// Resume normally first; recovery only adds the fork GL-thread un-pause if the
// engine rehost (detach/reattach) pauses the view again after this point.
mapView.resume()
pendingRecovery = true
val timeoutRunnable = Runnable {
pendingRecoveryTimeoutRunnable = null
if (pendingRecovery) {
pendingRecovery = false
}
}
pendingRecoveryTimeoutRunnable = timeoutRunnable
container.postDelayed(timeoutRunnable, 600L)
return
}
wasActivityPaused = false
mapView.resume()
}

override fun onActivityPaused(activity: Activity) {
if (activity != this.activity) return
isActivityResumed = false
wasActivityPaused = true
cancelPendingMapViewRecreation()
cancelPendingRecovery()
captureCameraPosition()
mapView.pause()
}

Expand All @@ -58,7 +101,140 @@ class KakaoMapView(

override fun onActivityDestroyed(activity: Activity) {
if (activity != this.activity) return
disposeMapView()
}

private fun createMapView(startOption: KakaoMapOption): MapView {
val mapView = MapView(activity)
val wrappedOption = startOption.also { it.setOnReady(::onMapReady) }
controller.mapView = mapView

mapView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
if (pendingRecovery) {
cancelPendingRecoveryTimeout()
val recoveryRunnable = Runnable {
pendingRecoveryRunnable = null
executeGLSurfaceViewRecovery()
}
pendingRecoveryRunnable = recoveryRunnable
container.post(recoveryRunnable)
}
}
override fun onViewDetachedFromWindow(v: View) = Unit
})

mapView.start(controller, wrappedOption)
return mapView
}

private fun onMapReady(map: KakaoMap) {
kakaoMap = map
pendingCameraPosition?.let { cameraPosition ->
map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
lastCameraPosition = cameraPosition
pendingCameraPosition = null
} ?: run {
captureCameraPosition()
}
controller.onMapReady(map)
}

private fun scheduleMapViewRecreation() {
cancelPendingMapViewRecreation()
val runnable = Runnable {
pendingRecreateRunnable = null
recreateMapView()
}
pendingRecreateRunnable = runnable
container.postDelayed(runnable, recreateDelayMillis)
}

private fun cancelPendingMapViewRecreation() {
pendingRecreateRunnable?.let(container::removeCallbacks)
pendingRecreateRunnable = null
}

private fun cancelPendingRecoveryTimeout() {
pendingRecoveryTimeoutRunnable?.let(container::removeCallbacks)
pendingRecoveryTimeoutRunnable = null
}

private fun cancelPendingRecovery() {
cancelPendingRecoveryTimeout()
pendingRecoveryRunnable?.let(container::removeCallbacks)
pendingRecoveryRunnable = null
pendingRecovery = false
}

private fun executeGLSurfaceViewRecovery() {
if (isDisposed) return
val surfaceView = runCatching { mapView.getSurfaceView() }.getOrNull()

// Primary: cast to Kakao fork GLSurfaceView and call onResume()
val castResult = runCatching {
val glSurfaceView = surfaceView as? com.kakao.vectormap.graphics.gl.GLSurfaceView
if (glSurfaceView != null) {
glSurfaceView.onResume()
true
} else {
false
}
}.getOrElse {
false
}

// Fallback: reflection if cast yielded null (e.g. Vulkan surface view)
if (!castResult && surfaceView != null) {
runCatching {
surfaceView.javaClass.getMethod("onResume").invoke(surfaceView)
}
}

runCatching {
mapView.resume()
}

pendingRecovery = false
}

private fun recreateMapView() {
if (isDisposed) return
val cameraPosition = captureCameraPosition()
pendingCameraPosition = cameraPosition
val oldMapView = mapView
oldMapView.finish()
container.removeView(oldMapView)

val startOption = cameraPosition?.let(option::copyWithCameraPosition) ?: option
mapView = createMapView(startOption)
container.addView(mapView, matchParentLayoutParams())
if (isActivityResumed) {
mapView.resume()
}
}

private fun captureCameraPosition(): CameraPosition? {
if (!::kakaoMap.isInitialized) return lastCameraPosition
lastCameraPosition =
runCatching { kakaoMap.cameraPosition }.getOrElse { lastCameraPosition }
return lastCameraPosition
}

private fun disposeMapView() {
if (isDisposed) return
isDisposed = true
cancelPendingMapViewRecreation()
cancelPendingRecovery()
mapView.finish()
container.removeAllViews()
activity.application.unregisterActivityLifecycleCallbacks(this)
}

private fun matchParentLayoutParams(): FrameLayout.LayoutParams {
return FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ class KakaoMapViewFactory(private val activity: Activity, private val messenger:

val controller = KakaoMapController(viewId, context, channel, overlayChannel)
val option = KakaoMapOption.fromMessageable(controller::onMapReady, convertedArgs)
val recreateMapViewOnResume =
convertedArgs["recreateAndroidMapViewOnResume"] as Boolean? ?: false
val recreateMapViewDelayMillis =
(convertedArgs["androidMapViewRecreationDelayMillis"] as Number?)?.toLong() ?: 300L
val recoverGLSurfaceViewOnResume =
convertedArgs["recoverAndroidGLSurfaceViewOnResume"] as Boolean? ?: true
return KakaoMapView(
activity = activity,
context = context,
controller = controller,
viewId = viewId,
option = option,
channel = channel,
recreateMapViewOnResume = recreateMapViewOnResume,
recreateMapViewDelayMillis = recreateMapViewDelayMillis,
recoverGLSurfaceViewOnResume = recoverGLSurfaceViewOnResume,
)
}
}
32 changes: 32 additions & 0 deletions lib/controller/controller_implement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -466,4 +466,36 @@ class KakaoMapControllerImplement extends KakaoMapController {
Future<void> resume() async {
await channel.invokeMethod("resume");
}

void _resetAfterNativeMapRecreation() {
for (final style in _poiStyle.values) {
style._isAdded = false;
}
for (final styles in _polygonStyle.values) {
for (final style in styles) {
style._isAdded = false;
}
}
for (final styles in _polylineStyle.values) {
for (final style in styles) {
style._isAdded = false;
}
}
for (final styles in _routeStyle.values) {
for (final style in styles) {
style._isAdded = false;
}
}

_labelController.clear();
_lodLabelController.clear();
_shapeController.clear();
_routeController.clear();
_poiStyle.clear();
_polygonStyle.clear();
_polylineStyle.clear();
_routeStyle.clear();
buildingHeightScale = null;
_initalizeOverlayController();
}
}
4 changes: 2 additions & 2 deletions lib/controller/overlay/overlay_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ mixin OverlayManager {
final Map<String, LodLabelController> _lodLabelController = {};
final Map<String, ShapeController> _shapeController = {};
final Map<String, RouteController> _routeController = {};
late final DimScreenController _dimController;
late final TrackingController _trackingController;
late DimScreenController _dimController;
late TrackingController _trackingController;

// Overlay Style
final Map<String, PoiStyle> _poiStyle = {};
Expand Down
Loading