Skip to content
Merged
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

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
Expand All @@ -15,7 +16,6 @@ import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.daedan.festabook.R
import com.daedan.festabook.databinding.FragmentPlaceMapBinding
import com.daedan.festabook.di.fragment.FragmentKey
Expand All @@ -25,6 +25,7 @@ import com.daedan.festabook.presentation.common.BaseFragment
import com.daedan.festabook.presentation.common.OnMenuItemReClickListener
import com.daedan.festabook.presentation.common.showErrorSnackBar
import com.daedan.festabook.presentation.common.toPx
import com.daedan.festabook.presentation.placeMap.component.NaverMapContent
import com.daedan.festabook.presentation.placeMap.logging.CurrentLocationChecked
import com.daedan.festabook.presentation.placeMap.logging.PlaceFragmentEnter
import com.daedan.festabook.presentation.placeMap.logging.PlaceMarkerClick
Expand All @@ -39,7 +40,6 @@ import com.daedan.festabook.presentation.placeMap.placeList.PlaceListFragment
import com.daedan.festabook.presentation.placeMap.timeTagSpinner.component.TimeTagMenu
import com.daedan.festabook.presentation.theme.FestabookColor
import com.daedan.festabook.presentation.theme.FestabookTheme
import com.naver.maps.map.MapFragment
import com.naver.maps.map.NaverMap
import com.naver.maps.map.OnMapReadyCallback
import com.naver.maps.map.util.FusedLocationSource
Expand All @@ -48,7 +48,6 @@ import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metro.createGraphFactory
import kotlinx.coroutines.launch
import timber.log.Timber

@ContributesIntoMap(
Expand All @@ -62,7 +61,6 @@ class PlaceMapFragment(
placeDetailPreviewFragment: PlaceDetailPreviewFragment,
placeCategoryFragment: PlaceCategoryFragment,
placeDetailPreviewSecondaryFragment: PlaceDetailPreviewSecondaryFragment,
mapFragment: MapFragment,
override val defaultViewModelProviderFactory: ViewModelProvider.Factory,
) : BaseFragment<FragmentPlaceMapBinding>(),
OnMenuItemReClickListener {
Expand All @@ -78,8 +76,6 @@ class PlaceMapFragment(
placeDetailPreviewSecondaryFragment,
)
}
private val mapFragment by lazy { getIfExists(mapFragment) }

private val locationSource by lazy {
FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
}
Expand All @@ -94,7 +90,6 @@ class PlaceMapFragment(
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
childFragmentManager.commit {
addWithSimpleTag(R.id.fcv_map_container, mapFragment)
addWithSimpleTag(R.id.fcv_place_list_container, placeListFragment)
addWithSimpleTag(R.id.fcv_map_container, placeDetailPreviewFragment)
addWithSimpleTag(R.id.fcv_place_category_container, placeCategoryFragment)
Expand All @@ -103,11 +98,9 @@ class PlaceMapFragment(
hide(placeDetailPreviewSecondaryFragment)
}
}
lifecycleScope.launch {
setUpMapManager()
setupComposeView()
setUpObserver()
}

setupComposeView()

binding.logger.log(
PlaceFragmentEnter(
baseLogData = binding.logger.getBaseLogData(),
Expand All @@ -128,29 +121,27 @@ class PlaceMapFragment(
mapManager?.moveToPosition()
}

private suspend fun setUpMapManager() {
naverMap = mapFragment.getMap()
naverMap.addOnLocationChangeListener {
binding.logger.log(
CurrentLocationChecked(
baseLogData = binding.logger.getBaseLogData(),
),
)
}
(placeListFragment as? OnMapReadyCallback)?.onMapReady(naverMap)
naverMap.locationSource = locationSource
binding.viewMapTouchEventIntercept.setOnMapDragListener {
viewModel.onMapViewClick()
}
}

private fun setupComposeView() {
binding.cvPlaceMap.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
FestabookTheme {
val timeTags by viewModel.timeTags.collectAsStateWithLifecycle()
val title by viewModel.selectedTimeTagFlow.collectAsStateWithLifecycle()
NaverMapContent(
modifier = Modifier.fillMaxSize(),
onMapDrag = { viewModel.onMapViewClick() },
onMapReady = { setupMap(it) },
) {
// TODO 흩어져있는 ComposeView 통합, 추후 PlaceMapScreen 사용
}
}
}
}
binding.cvTimeTagSpinner.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val timeTags by viewModel.timeTags.collectAsStateWithLifecycle()
val title by viewModel.selectedTimeTagFlow.collectAsStateWithLifecycle()
FestabookTheme {
if (timeTags.isNotEmpty()) {
TimeTagMenu(
title = title.name,
Expand All @@ -176,6 +167,20 @@ class PlaceMapFragment(
}
}

private fun setupMap(map: NaverMap) {
naverMap = map
naverMap.addOnLocationChangeListener {
binding.logger.log(
CurrentLocationChecked(
baseLogData = binding.logger.getBaseLogData(),
),
)
}
(placeListFragment as? OnMapReadyCallback)?.onMapReady(naverMap)
naverMap.locationSource = locationSource
setUpObserver()
}

private fun setUpObserver() {
viewModel.placeGeographies.observe(viewLifecycleOwner) { placeGeographies ->
when (placeGeographies) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.daedan.festabook.presentation.placeMap.component

import android.content.ComponentCallbacks2
import android.content.res.Configuration
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.naver.maps.map.MapView
import com.naver.maps.map.NaverMap

@Composable
fun NaverMapContent(
modifier: Modifier = Modifier,
onMapDrag: () -> Unit = {},
onMapReady: (NaverMap) -> Unit = {},
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val mapView = remember { MapView(context) }
AndroidView(
factory = {
mapView.apply {
getMapAsync(onMapReady)
}
},
modifier = modifier.dragInterceptor(onMapDrag),
)
RegisterMapLifeCycle(mapView)
content()
}

private fun Modifier.dragInterceptor(onMapDrag: () -> Unit): Modifier =
this.then(
Modifier.pointerInput(Unit) {
val touchSlop = viewConfiguration.touchSlop // 시스템이 정의한 드래그 판단 기준 거리
awaitPointerEventScope {
while (true) {
// 1. 첫 번째 터치(Down)를 기다립니다.
val downEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
val downChange = downEvent.changes.firstOrNull { it.pressed } ?: continue

// 터치 시작 지점 저장
val startPosition = downChange.position
var isDragEmitted = false // 이번 드래그 세션에서 콜백을 호출했는지 체크

// 2. 터치가 유지되는 동안(드래그 중) 계속 감시합니다.
do {
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
val change = event.changes.firstOrNull { it.id == downChange.id }

if (change != null && change.pressed) {
// 현재 위치와 시작 위치 사이의 거리 계산
val currentPosition = change.position
val distance = (currentPosition - startPosition).getDistance()

// 3. 이동 거리가 touchSlop보다 크고, 아직 콜백을 안 불렀다면 호출
if (!isDragEmitted && distance > touchSlop) {
onMapDrag()
isDragEmitted = true
}
}
} while (event.changes.any { it.pressed }) // 손을 뗄 때까지 루프
}
}
},
)

@Composable
private fun RegisterMapLifeCycle(mapView: MapView) {
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle
val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) }
val savedInstanceState = rememberSaveable { Bundle() }

DisposableEffect(lifecycle, mapView) {
val mapLifecycleObserver =
mapView.lifecycleObserver(
savedInstanceState.takeUnless { it.isEmpty },
previousState,
)

val callbacks =
object : ComponentCallbacks2 {
override fun onConfigurationChanged(config: Configuration) = Unit

@Deprecated("This callback is superseded by onTrimMemory")
override fun onLowMemory() {
mapView.onLowMemory()
}

override fun onTrimMemory(level: Int) {
mapView.onLowMemory()
}
}

lifecycle.addObserver(mapLifecycleObserver)
context.registerComponentCallbacks(callbacks)
onDispose {
mapView.onSaveInstanceState(savedInstanceState)
lifecycle.removeObserver(mapLifecycleObserver)
context.unregisterComponentCallbacks(callbacks)

// dispose 시점에 Lifecycle.Event가 끝까지 진행되지 않아 발생되는
// MapView Memory Leak 수정합니다.
when (previousState.value) {
Lifecycle.Event.ON_CREATE, Lifecycle.Event.ON_STOP -> {
mapView.onDestroy()
}

Lifecycle.Event.ON_START, Lifecycle.Event.ON_PAUSE -> {
mapView.onStop()
mapView.onDestroy()
}

Lifecycle.Event.ON_RESUME -> {
mapView.onPause()
mapView.onStop()
mapView.onDestroy()
}

else -> Unit
}
}
}
}

private fun MapView.lifecycleObserver(
savedInstanceState: Bundle?,
previousState: MutableState<Lifecycle.Event>,
): LifecycleEventObserver =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> this.onCreate(savedInstanceState)
Lifecycle.Event.ON_START -> this.onStart()
Lifecycle.Event.ON_RESUME -> this.onResume()
Lifecycle.Event.ON_PAUSE -> this.onPause()
Lifecycle.Event.ON_STOP -> this.onStop()
Lifecycle.Event.ON_DESTROY -> this.onDestroy()
else -> throw IllegalStateException()
}
previousState.value = event
}
Loading