diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt deleted file mode 100644 index 01c1e1a..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import android.content.Context -import android.util.AttributeSet -import android.view.GestureDetector -import android.view.MotionEvent -import android.widget.FrameLayout - -class MapTouchEventInterceptView( - context: Context, - attrs: AttributeSet? = null, -) : FrameLayout( - context, - attrs, - ) { - private var onMapDragListener: OnMapDragListener? = null - - private var isMapDragging = false - - private val gestureDetector by lazy { - GestureDetector( - context, - object : GestureDetector.SimpleOnGestureListener() { - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent, - velocityX: Float, - velocityY: Float, - ): Boolean { - if (!isMapDragging) { - onMapDragListener?.onDrag() - isMapDragging = true - } - return super.onFling(e1, e2, velocityX, velocityY) - } - - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent, - distanceX: Float, - distanceY: Float, - ): Boolean { - if ((distanceY > 0 || distanceX > 0) && !isMapDragging) { - isMapDragging = true - onMapDragListener?.onDrag() - } - return super.onScroll(e1, e2, distanceX, distanceY) - } - }, - ) - } - - override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { - event?.let { - if (it.action == MotionEvent.ACTION_UP) { - isMapDragging = false - } - gestureDetector.onTouchEvent(it) - } - return false - } - - fun setOnMapDragListener(listener: OnMapDragListener) { - onMapDragListener = listener - } -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt deleted file mode 100644 index a193bee..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import com.naver.maps.map.MapFragment -import com.naver.maps.map.NaverMap -import kotlinx.coroutines.suspendCancellableCoroutine - -suspend fun MapFragment.getMap() = - suspendCancellableCoroutine { cont -> - getMapAsync { map -> - cont.resumeWith( - Result.success(map), - ) - } - } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt deleted file mode 100644 index aa98139..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -fun interface OnMapDragListener { - fun onDrag() -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt deleted file mode 100644 index fb5eb58..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import com.daedan.festabook.domain.model.TimeTag - -interface OnTimeTagSelectedListener { - fun onTimeTagSelected(item: TimeTag) - - fun onNothingSelected() -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index 53b5778..8c7a65f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -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( @@ -62,7 +61,6 @@ class PlaceMapFragment( placeDetailPreviewFragment: PlaceDetailPreviewFragment, placeCategoryFragment: PlaceCategoryFragment, placeDetailPreviewSecondaryFragment: PlaceDetailPreviewSecondaryFragment, - mapFragment: MapFragment, override val defaultViewModelProviderFactory: ViewModelProvider.Factory, ) : BaseFragment(), OnMenuItemReClickListener { @@ -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) } @@ -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) @@ -103,11 +98,9 @@ class PlaceMapFragment( hide(placeDetailPreviewSecondaryFragment) } } - lifecycleScope.launch { - setUpMapManager() - setupComposeView() - setUpObserver() - } + + setupComposeView() + binding.logger.log( PlaceFragmentEnter( baseLogData = binding.logger.getBaseLogData(), @@ -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, @@ -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) { diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt new file mode 100644 index 0000000..aa72f6e --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt @@ -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, +): 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 + } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt new file mode 100644 index 0000000..4fab146 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt @@ -0,0 +1,18 @@ +package com.daedan.festabook.presentation.placeMap.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.naver.maps.map.widget.LogoView + +@Composable +fun NaverMapLogo(modifier: Modifier = Modifier) { + val context = LocalContext.current + val logoView = remember { LogoView(context) } + AndroidView( + factory = { logoView }, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt new file mode 100644 index 0000000..3443c86 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt @@ -0,0 +1,47 @@ +package com.daedan.festabook.presentation.placeMap.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.daedan.festabook.domain.model.TimeTag +import com.daedan.festabook.presentation.placeMap.timeTagSpinner.component.TimeTagMenu +import com.daedan.festabook.presentation.theme.FestabookColor +import com.naver.maps.map.NaverMap + +@Composable +fun PlaceMapScreen( + timeTags: List, + title: String, + onMapReady: (NaverMap) -> Unit, + onTimeTagClick: (TimeTag) -> Unit, + modifier: Modifier = Modifier, +) { + NaverMapContent( + modifier = modifier.fillMaxSize(), + onMapReady = onMapReady, + ) { + Column( + modifier = Modifier.wrapContentSize(), + ) { + if (timeTags.isNotEmpty()) { + TimeTagMenu( + title = title, + timeTags = timeTags, + onTimeTagClick = { timeTag -> + onTimeTagClick(timeTag) + }, + modifier = + Modifier + .background( + FestabookColor.white, + ).padding(horizontal = 24.dp), + ) + } + } + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt index c62a906..ddccdc1 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt @@ -20,7 +20,7 @@ interface MapFilterManager { * * @param selectedTimeTagId 필터링에 사용할 시간 태그의 ID입니다. null 또는 특정 ID가 될 수 있습니다. */ - fun filterMarkersByTimeTag(selectedTimeTagId: Long?) + fun filterMarkersByTimeTag(selectedTimeTagId: Long) /** * 모든 필터링 조건을 해제하고 마커를 초기 상태로 복원합니다. diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt index d934fbd..1283ecb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt @@ -20,25 +20,32 @@ class MapFilterManagerImpl( ) : MapFilterManager { private var selectedMarker: Marker? = null - private var selectedTimeTagId: Long? = null + private var selectedTimeTagId: Long = TimeTag.EMTPY_TIME_TAG_ID override fun filterMarkersByCategories(categories: List) { markers.forEach { marker -> val place = marker.tag as? PlaceCoordinateUiModel ?: return@forEach val isSelectedMarker = marker == selectedMarker - // 필터링된 마커이거나 선택된 마커인 경우에만 보이게 처리 - marker.isVisible = - place.category in categories && - place.timeTagIds.contains(selectedTimeTagId) || - isSelectedMarker + // 필터링된 마커이거나 선택된 마커인 경우에만 보이게 처리, + // 타임태그가 없다면, 타임태그 검사 생략 + if (selectedTimeTagId == TimeTag.EMTPY_TIME_TAG_ID) { + marker.isVisible = + place.category in categories || + isSelectedMarker + } else { + marker.isVisible = + place.category in categories && + place.timeTagIds.contains(selectedTimeTagId) || + isSelectedMarker + } // 선택된 마커는 크기를 유지하고, 필터링되지 않은 마커는 원래 크기로 되돌림 markerManager.setMarkerIcon(marker, isSelectedMarker) } } - override fun filterMarkersByTimeTag(selectedTimeTagId: Long?) { + override fun filterMarkersByTimeTag(selectedTimeTagId: Long) { if (selectedTimeTagId == TimeTag.EMTPY_TIME_TAG_ID) { markers.forEach { it.isVisible = true } return diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt index bad82c6..5b63718 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt @@ -9,14 +9,18 @@ enum class PlaceCategoryUiModel { FOOD_TRUCK, BOOTH, BAR, - TRASH_CAN, - TOILET, - SMOKING_AREA, - PRIMARY, - PARKING, + STAGE, PHOTO_BOOTH, + PRIMARY, + EXTRA, + PARKING, + TOILET, + + SMOKING_AREA, + + TRASH_CAN, ; companion object { diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt index 91e932c..edf6650 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt @@ -1,20 +1,30 @@ package com.daedan.festabook.presentation.placeMap.placeCategory import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import androidx.core.view.children +import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceCategoryBinding +import com.daedan.festabook.di.appGraph import com.daedan.festabook.di.fragment.FragmentKey -import com.daedan.festabook.logging.logger import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlaceCategoryClick import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel -import com.google.android.material.chip.Chip +import com.daedan.festabook.presentation.placeMap.placeCategory.component.PlaceCategoryScreen +import com.daedan.festabook.presentation.theme.FestabookTheme import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -30,42 +40,48 @@ class PlaceCategoryFragment( private val viewModel: PlaceMapViewModel by viewModels({ requireParentFragment() }) - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - setUpBinding() - } + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + FestabookTheme { + val initialCategories = PlaceCategoryUiModel.entries + // StateFlow로 변경 시 asFlow 제거 예정 + val timeTagChanged = + viewModel.selectedTimeTag + .asFlow() + .collectAsStateWithLifecycle(viewLifecycleOwner) + var selectedCategoriesState by remember(timeTagChanged.value) { + mutableStateOf( + emptySet(), + ) + } - private fun setUpBinding() { - binding.cgCategories.setOnCheckedStateChangeListener { group, checkedIds -> - val selectedCategories = - checkedIds.mapNotNull { - val category = group.findViewById(it).tag - category as? PlaceCategoryUiModel + PlaceCategoryScreen( + initialCategories = initialCategories, + selectedCategories = selectedCategoriesState, + onCategoryClick = { selectedCategories -> + selectedCategoriesState = selectedCategories + viewModel.unselectPlace() + viewModel.setSelectedCategories(selectedCategories.toList()) + appGraph.defaultFirebaseLogger.log( + PlaceCategoryClick( + baseLogData = appGraph.defaultFirebaseLogger.getBaseLogData(), + currentCategories = selectedCategories.joinToString(",") { it.toString() }, + ), + ) + }, + onDisplayAllClick = { selectedCategories -> + selectedCategoriesState = selectedCategories + viewModel.unselectPlace() + viewModel.setSelectedCategories(initialCategories) + }, + ) } - - viewModel.unselectPlace() - viewModel.setSelectedCategories(selectedCategories) - binding.chipCategoryAll.isChecked = selectedCategories.isEmpty() - binding.logger.log( - PlaceCategoryClick( - baseLogData = binding.logger.getBaseLogData(), - currentCategories = selectedCategories.joinToString(",") { it.toString() }, - ), - ) - } - - setUpChipCategoryAllListener() - } - - private fun setUpChipCategoryAllListener() { - binding.chipCategoryAll.setOnClickListener { - binding.cgCategories.children.forEach { - val chip = (it as? Chip) ?: return@forEach - chip.isChecked = chip.id == binding.chipCategoryAll.id } } - } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt new file mode 100644 index 0000000..723992d --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt @@ -0,0 +1,140 @@ +package com.daedan.festabook.presentation.placeMap.placeCategory.component + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.getIconId +import com.daedan.festabook.presentation.placeMap.model.getTextId +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookShapes +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun PlaceCategoryScreen( + modifier: Modifier = Modifier, + selectedCategories: Set = emptySet(), + onDisplayAllClick: (selectedCategories: Set) -> Unit = {}, + onCategoryClick: (selectedCategories: Set) -> Unit = {}, + initialCategories: List = PlaceCategoryUiModel.entries, +) { + val scrollState = rememberScrollState() + + Row( + modifier = + modifier + .horizontalScroll(scrollState) + .padding( + vertical = festabookSpacing.paddingBody2, + horizontal = festabookSpacing.paddingScreenGutter, + ), + horizontalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), + ) { + CategoryChip( + text = stringResource(R.string.map_category_all), + selected = selectedCategories.isEmpty(), + onClick = { + onDisplayAllClick(emptySet()) + }, + ) + + initialCategories.forEach { category -> + val text = stringResource(category.getTextId()) + CategoryChip( + text = text, + selected = selectedCategories.contains(category), + icon = { + Icon( + painter = painterResource(category.getIconId()), + contentDescription = text, + tint = Color.Unspecified, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + }, + onClick = { + val newSelectedCategories = + if (selectedCategories.contains(category)) { + selectedCategories.filter { it != category } + } else { + selectedCategories + setOf(category) + } + onCategoryClick(newSelectedCategories.toSet()) + }, + ) + } + } +} + +@Composable +private fun CategoryChip( + text: String, + modifier: Modifier = Modifier, + selected: Boolean = false, + icon: @Composable (() -> Unit)? = null, + onClick: () -> Unit = {}, +) { + FilterChip( + selected = selected, + onClick = { + onClick() + }, + modifier = modifier, + label = { + Text( + text = text, + style = FestabookTypography.bodyLarge, + ) + }, + shape = festabookShapes.radiusFull, + colors = + FilterChipDefaults.filterChipColors( + containerColor = FestabookColor.white, + selectedContainerColor = FestabookColor.gray200, + labelColor = FestabookColor.black, + selectedLabelColor = FestabookColor.black, + ), + border = + FilterChipDefaults.filterChipBorder( + enabled = true, + selected = selected, + borderColor = FestabookColor.gray200, + selectedBorderColor = FestabookColor.black, + borderWidth = 2.dp, + selectedBorderWidth = 2.dp, + ), + leadingIcon = icon, + ) +} + +@Composable +@Preview(showBackground = true) +private fun CategoryChipPreview() { + FestabookTheme { + CategoryChip("전체") + } +} + +@Composable +@Preview(showBackground = true) +private fun PlaceCategoryScreenPreview() { + FestabookTheme { + PlaceCategoryScreen() + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt index 650fb19..f44724c 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt @@ -134,6 +134,8 @@ class PlaceListFragment( placeAdapter.submitList(places.value) { if (places.value.isEmpty()) { binding.tvErrorToLoadPlaceInfo.visibility = View.VISIBLE + } else { + binding.tvErrorToLoadPlaceInfo.visibility = View.GONE } binding.rvPlaces.scrollToPosition(0) } diff --git a/app/src/main/res/layout/fragment_place_map.xml b/app/src/main/res/layout/fragment_place_map.xml index c2df411..6734ddf 100644 --- a/app/src/main/res/layout/fragment_place_map.xml +++ b/app/src/main/res/layout/fragment_place_map.xml @@ -10,17 +10,15 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - + - + + + app:layout_constraintTop_toBottomOf="@id/cv_time_tag_spinner" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/fcv_place_category_container" />