diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 6def8511..f4ae951c 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ + + + + + - - - diff --git a/presentation/src/main/java/com/cheocharm/presentation/common/Constants.kt b/presentation/src/main/java/com/cheocharm/presentation/common/Constants.kt index f6bf8c53..c5772c83 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/common/Constants.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/common/Constants.kt @@ -3,3 +3,8 @@ package com.cheocharm.presentation.common const val SIGN_UP_TYPE = "signUpType" const val GOOGLE_ID_TOKEN = "googleIdToken" const val GROUP_JOIN_REQUEST_BOTTOM = "groupJoinRequestBottom" + +const val SOUTH_KOREA_LAT = 35.95 +const val SOUTH_KOREA_LNG = 128.25 +const val SOUTH_KOREA_ZOOM_LEVEL = 6.5F +const val DEFAULT_ZOOM_LEVEL = 15F diff --git a/presentation/src/main/java/com/cheocharm/presentation/common/Extensions.kt b/presentation/src/main/java/com/cheocharm/presentation/common/Extensions.kt new file mode 100644 index 00000000..abe25558 --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/common/Extensions.kt @@ -0,0 +1,6 @@ +package com.cheocharm.presentation.common + +import android.location.Location +import com.google.android.gms.maps.model.LatLng + +fun Location.toLatLng() = LatLng(this.latitude, this.longitude) diff --git a/presentation/src/main/java/com/cheocharm/presentation/common/ext.kt b/presentation/src/main/java/com/cheocharm/presentation/common/ext.kt deleted file mode 100644 index a865a2ac..00000000 --- a/presentation/src/main/java/com/cheocharm/presentation/common/ext.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.cheocharm.presentation.common - -import com.google.android.gms.maps.model.LatLng - -fun LatLng?.toCoordString() = this?.let { - with("%.5f") { - val lat = format(it.latitude) - val lng = format(it.longitude) - "($lat, $lng)" - } -} diff --git a/presentation/src/main/java/com/cheocharm/presentation/enum/LatLngSelectionType.kt b/presentation/src/main/java/com/cheocharm/presentation/enum/LatLngSelectionType.kt new file mode 100644 index 00000000..8db5f85e --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/enum/LatLngSelectionType.kt @@ -0,0 +1,7 @@ +package com.cheocharm.presentation.enum + +enum class LatLngSelectionType(val locationString: String) { + DEFAULT("대한민국"), + CURRENT("현재 위치"), + SPECIFIED("") +} diff --git a/presentation/src/main/java/com/cheocharm/presentation/model/Picture.kt b/presentation/src/main/java/com/cheocharm/presentation/model/Picture.kt index 67b88c02..d23b509a 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/model/Picture.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/model/Picture.kt @@ -1,13 +1,8 @@ package com.cheocharm.presentation.model import android.net.Uri -import com.cheocharm.presentation.common.toCoordString import com.google.android.gms.maps.model.LatLng data class Picture(val uri: Uri, val latLng: LatLng?) { var address: String? = null - - fun getLocationString(): String { - return address ?: latLng.toCoordString() ?: "현재 위치" - } } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/MainActivity.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/MainActivity.kt index fc882bba..07f180d8 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/MainActivity.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/MainActivity.kt @@ -1,22 +1,50 @@ package com.cheocharm.presentation.ui +import android.Manifest import android.content.Context import android.graphics.Rect +import android.os.Build import android.os.Bundle import android.view.MotionEvent import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.EditText +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.core.view.isVisible import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.cheocharm.presentation.R import com.cheocharm.presentation.base.BaseActivity import com.cheocharm.presentation.databinding.ActivityMainBinding +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.gms.maps.SupportMapFragment import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : BaseActivity(R.layout.activity_main) { + private var mapFragment: SupportMapFragment? = null + private var fusedLocationClient: FusedLocationProviderClient? = null + + @RequiresApi(Build.VERSION_CODES.N) + private val locationPermissionRequest = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + when { + permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false -> { + fusedLocationClient = + LocationServices.getFusedLocationProviderClient(this) + } + permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false -> { + fusedLocationClient = + LocationServices.getFusedLocationProviderClient(this) + } + else -> { + // TODO: 위치 권한을 얻지 못했을 때 + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -37,6 +65,21 @@ class MainActivity : BaseActivity(R.layout.activity_main) { binding.bottomNavMain.visibility = View.VISIBLE } } + + mapFragment = + supportFragmentManager.findFragmentById(R.id.fragment_main_map) as? SupportMapFragment + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onStart() { + super.onStart() + + locationPermissionRequest.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { @@ -60,4 +103,12 @@ class MainActivity : BaseActivity(R.layout.activity_main) { } fun getBinding(): ActivityMainBinding = binding + + fun setMapVisible(boolean: Boolean) { + binding.fragmentMainMap.isVisible = boolean + } + + fun getMap(): SupportMapFragment? = mapFragment + + fun getLocationClient(): FusedLocationProviderClient? = fusedLocationClient } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeFragment.kt index 54b4d5ba..034a599b 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeFragment.kt @@ -1,13 +1,23 @@ package com.cheocharm.presentation.ui.home +import android.Manifest +import android.content.pm.PackageManager import android.os.Bundle import android.view.View -import androidx.core.view.isVisible +import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import com.cheocharm.presentation.R import com.cheocharm.presentation.base.BaseFragment +import com.cheocharm.presentation.common.DEFAULT_ZOOM_LEVEL +import com.cheocharm.presentation.common.SOUTH_KOREA_LAT +import com.cheocharm.presentation.common.SOUTH_KOREA_LNG +import com.cheocharm.presentation.common.SOUTH_KOREA_ZOOM_LEVEL +import com.cheocharm.presentation.common.toLatLng import com.cheocharm.presentation.databinding.FragmentHomeBinding import com.cheocharm.presentation.ui.MainActivity +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -19,9 +29,47 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) { binding.viewmodel = homeViewModel - val mainActivityBinding = (activity as MainActivity).getBinding() - mainActivityBinding.fragmentMainMap.isVisible = true + val mainActivity = requireActivity() as MainActivity + mainActivity.setMapVisible(true) - homeViewModel.countUp() + val mapFragment = (activity as MainActivity).getMap() + mapFragment?.getMapAsync { map -> + map.setOnCameraMoveListener { + homeViewModel.updateZoomLevel(map.cameraPosition.zoom) + } + + map.setOnMapLoadedCallback { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + val locationClient = mainActivity.getLocationClient() + locationClient?.lastLocation?.addOnSuccessListener { location -> + if (location != null) { + map.moveCamera( + CameraUpdateFactory.newLatLngZoom( + location.toLatLng(), + DEFAULT_ZOOM_LEVEL + ) + ) + } else { + map.moveCameraToDefaultLocation() + } + } + } else { + map.moveCameraToDefaultLocation() + } + } + } + } + + private fun GoogleMap.moveCameraToDefaultLocation() { + moveCamera( + CameraUpdateFactory.newLatLngZoom( + LatLng(SOUTH_KOREA_LAT, SOUTH_KOREA_LNG), + SOUTH_KOREA_ZOOM_LEVEL + ) + ) } } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeViewModel.kt index 22d320cb..cea5c91d 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/home/HomeViewModel.kt @@ -1,5 +1,6 @@ package com.cheocharm.presentation.ui.home +import android.location.Location import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -8,10 +9,17 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor() : ViewModel() { - private val _count = MutableLiveData() - val count: LiveData = _count + private val _location = MutableLiveData() + val location: LiveData = _location - fun countUp() { - _count.value = count.value?.plus(1) ?: 1 + private val _zoomLevel = MutableLiveData() + val zoomLevel: LiveData = _zoomLevel + + fun updateLocation(location: Location) { + _location.value = location + } + + fun updateZoomLevel(level: Float) { + _zoomLevel.value = level } } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/mypage/MyPageFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/mypage/MyPageFragment.kt index cebdda9f..5ef9e047 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/mypage/MyPageFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/mypage/MyPageFragment.kt @@ -3,7 +3,6 @@ package com.cheocharm.presentation.ui.mypage import android.content.Intent import android.os.Bundle import android.view.View -import androidx.core.view.isVisible import androidx.fragment.app.viewModels import com.cheocharm.presentation.BuildConfig import com.cheocharm.presentation.R @@ -38,8 +37,8 @@ class MyPageFragment : BaseFragment(R.layout.fragment_my_ binding.viewmodel = myPageViewModel - val mainActivityBinding = (activity as MainActivity).getBinding() - mainActivityBinding.fragmentMainMap.isVisible = false + val mainActivity = requireActivity() as MainActivity + mainActivity.setMapVisible(false) initButton() myPageViewModel.countUp() diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt index 5290305d..7d976eca 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt @@ -3,7 +3,6 @@ package com.cheocharm.presentation.ui.search import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo -import androidx.core.view.isVisible import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.navigation.fragment.findNavController import com.cheocharm.presentation.R @@ -24,7 +23,9 @@ class SearchFragment : BaseFragment(R.layout.fragment_sea binding.viewmodel = searchViewModel - mainActivityBinding = (activity as MainActivity).getBinding() + val mainActivity = requireActivity() as MainActivity + mainActivity.setMapVisible(false) + mainActivityBinding = mainActivity.getBinding() initEditTexts() initObservers() @@ -32,8 +33,6 @@ class SearchFragment : BaseFragment(R.layout.fragment_sea override fun onResume() { super.onResume() - - mainActivityBinding.fragmentMainMap.isVisible = false mainActivityBinding.bottomNavMain.visibility = View.VISIBLE } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationFragment.kt index b1d4a386..0de0a84f 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationFragment.kt @@ -1,5 +1,9 @@ package com.cheocharm.presentation.ui.write +import android.Manifest +import android.content.pm.PackageManager +import android.location.Location +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -8,42 +12,65 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider -import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.navGraphViewModels import com.cheocharm.presentation.R import com.cheocharm.presentation.base.BaseFragment +import com.cheocharm.presentation.common.DEFAULT_ZOOM_LEVEL import com.cheocharm.presentation.common.EventObserver +import com.cheocharm.presentation.common.SOUTH_KOREA_LAT +import com.cheocharm.presentation.common.SOUTH_KOREA_LNG +import com.cheocharm.presentation.common.SOUTH_KOREA_ZOOM_LEVEL +import com.cheocharm.presentation.common.toLatLng import com.cheocharm.presentation.databinding.FragmentLocationBinding +import com.cheocharm.presentation.enum.LatLngSelectionType +import com.cheocharm.presentation.model.Picture import com.cheocharm.presentation.ui.MainActivity -import com.cheocharm.presentation.util.UriUtil -import com.google.android.gms.maps.SupportMapFragment +import com.cheocharm.presentation.util.GeocodeUtil +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import java.io.File @AndroidEntryPoint class LocationFragment : BaseFragment(R.layout.fragment_location), MenuProvider { - private val pictureViewModel by navGraphViewModels(R.id.write) private val locationViewModel by navGraphViewModels(R.id.write) { defaultViewModelProviderFactory } private val writeViewModel by navGraphViewModels(R.id.write) { defaultViewModelProviderFactory } + private lateinit var map: GoogleMap + private lateinit var geocodeUtil: GeocodeUtil + + private var initialLatLng: LatLng? = null + private var initialType: LatLngSelectionType? = null + private var draggableMarker: Marker? = null private var address: String? = null private var location: LatLng? = null private var file: File? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + geocodeUtil = GeocodeUtil(requireContext(), Dispatchers.IO) + + setHasOptionsMenu(true) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val menuHost: MenuHost = requireActivity() as MenuHost + val menuHost: MenuHost = requireActivity() menuHost.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) return super.onCreateView(inflater, container, savedInstanceState) @@ -53,15 +80,14 @@ class LocationFragment : BaseFragment(R.layout.fragment super.onViewCreated(view, savedInstanceState) val picturesAdapter = PicturesAdapter() + val mainActivity = requireActivity() as MainActivity - binding.viewmodel = pictureViewModel + binding.viewmodel = locationViewModel binding.rvLocationPictures.apply { adapter = picturesAdapter } with(binding.toolbarLocation) { - val mainActivity = activity as MainActivity - mainActivity.setSupportActionBar(this) setNavigationIcon(R.drawable.ic_back) setNavigationOnClickListener { @@ -69,50 +95,150 @@ class LocationFragment : BaseFragment(R.layout.fragment } } - val mainActivityBinding = (activity as MainActivity).getBinding() - mainActivityBinding.fragmentMainMap.isVisible = true - - val mapFragment = - requireActivity().supportFragmentManager.findFragmentById(R.id.fragment_main_map) as? SupportMapFragment - mapFragment?.getMapAsync { - it.setOnMapLoadedCallback { - // TODO: 마커 생성 -// val selectedLocation = pic.latLng -// if (selectedLocation != null) { -// val markerOptions = MarkerOptions() -// .position(selectedLocation) -// .draggable(true) -// draggableMarker = it.addMarker(markerOptions) -// it.moveCamera(CameraUpdateFactory.newLatLngZoom(selectedLocation, 15F)) -// } else { -// // TODO: 사진에 장소 정보가 없으면 기본 위치로 카메라 이동 -// } - } - } + if (savedInstanceState != null) { + with(savedInstanceState) { + val uri = getString(KEY_URI) + val lat = getString(KEY_LAT)?.toDoubleOrNull() + val lng = getString(KEY_LNG)?.toDoubleOrNull() + val isDefaultLocation = getBoolean(KEY_IS_DEFAULT_LOCATION) - pictureViewModel.picture.observe(viewLifecycleOwner) { picture -> - picture?.let { pic -> - picturesAdapter.submitList(listOf(pic)) + if (uri != null) { + val latLng = if (lat != null && lng != null) LatLng(lat, lng) else null + val picture = Picture(Uri.parse(uri), latLng) - address = pic.address - location = pic.latLng + locationViewModel.loadPicture(picture, geocodeUtil) + } - activity?.applicationContext?.let { context -> - file = UriUtil.getFileFromUri(context, pic.uri) + if (isDefaultLocation) { + val typeDefault = LatLngSelectionType.DEFAULT + initialType = typeDefault + locationViewModel.setSelectedLatLng(defaultLatLng.toDoubleArray(), typeDefault) } } } + setupMap(mainActivity, picturesAdapter) setupToast() setupNavigation() } + private fun setupMap( + mainActivity: MainActivity, + picturesAdapter: PicturesAdapter + ) { + mainActivity.setMapVisible(true) + + val mapFragment = mainActivity.getMap() + mapFragment?.getMapAsync { googleMap -> + map = googleMap + + googleMap.setOnMapLoadedCallback { + val top = binding.toolbarLocation.height + val bottom = binding.containerLocationPictures.height + googleMap.setPadding(0, top, 0, bottom) + + locationViewModel.picture.observe(viewLifecycleOwner) { picture -> + picture?.let { pic -> + picturesAdapter.submitList(listOf(pic)) + + val selectedLatLng = pic.latLng + + if (selectedLatLng != null) { + initTypeToSpecified(selectedLatLng) + } else if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + val locationClient = mainActivity.getLocationClient() + locationClient?.lastLocation?.addOnSuccessListener { location -> + if (location != null) { + initTypeToCurrent(location.toLatLng()) + } else { + initTypeToDefault() + } + } + } else { + initTypeToDefault() + } + } + } + } + + googleMap.setOnCameraMoveListener { + val latLng = map.cameraPosition.target + locationViewModel.setSelectedLatLng( + latLng.toDoubleArray(), + LatLngSelectionType.SPECIFIED + ) + } + + googleMap.setOnCameraIdleListener { + val latLng = map.cameraPosition.target + val type = if (initialLatLng != null && + distanceBetween(initialLatLng!!, latLng) <= 1 + ) { + initialType ?: LatLngSelectionType.SPECIFIED + } else { + LatLngSelectionType.SPECIFIED + } + val array = latLng.toDoubleArray() + + if (type == LatLngSelectionType.SPECIFIED) { + locationViewModel.geocode(geocodeUtil, array, type) + } else { + locationViewModel.setSelectedLatLng(array, type) + } + } + } + } + + private fun initTypeToSpecified(latLng: LatLng) { + locationViewModel.geocode( + geocodeUtil, + latLng.toDoubleArray(), + LatLngSelectionType.SPECIFIED + ) + } + private fun setupToast() { locationViewModel.toastText.observe(viewLifecycleOwner, EventObserver { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() }) } + private fun initTypeToCurrent(latLng: LatLng) { + locationViewModel.setSelectedLatLng(latLng.toDoubleArray(), LatLngSelectionType.CURRENT) + + initialLatLng = latLng + initialType = LatLngSelectionType.CURRENT + + createMarkerAndMoveCamera(latLng, DEFAULT_ZOOM_LEVEL) + } + + private fun initTypeToDefault() { + locationViewModel.setSelectedLatLng( + defaultLatLng.toDoubleArray(), + LatLngSelectionType.DEFAULT + ) + + initialLatLng = defaultLatLng + initialType = LatLngSelectionType.DEFAULT + + createMarkerAndMoveCamera(defaultLatLng, SOUTH_KOREA_ZOOM_LEVEL) + } + + private fun LatLng.toDoubleArray(): DoubleArray = doubleArrayOf(latitude, longitude) + + private fun createMarkerAndMoveCamera(latLng: LatLng, zoomLevel: Float) { + val markerOptions = MarkerOptions() + .position(latLng) + .draggable(true) + draggableMarker = map.addMarker(markerOptions) + + map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoomLevel)) + } + private fun setupNavigation() { locationViewModel.locationSelectedEvent.observe(viewLifecycleOwner, EventObserver { writeViewModel.temp = it @@ -123,6 +249,19 @@ class LocationFragment : BaseFragment(R.layout.fragment }) } + private fun distanceBetween(latLng1: LatLng, latLng2: LatLng): Float { + val result = FloatArray(1) + Location.distanceBetween( + latLng1.latitude, + latLng1.longitude, + latLng2.latitude, + latLng2.longitude, + result + ) + + return result.first() + } + override fun onDestroyView() { draggableMarker?.remove() super.onDestroyView() @@ -152,7 +291,43 @@ class LocationFragment : BaseFragment(R.layout.fragment } } + override fun onSaveInstanceState(outState: Bundle) { + val picture = locationViewModel.picture.value + + if (picture != null) { + with(outState) { + putString(KEY_URI, picture.uri.toString()) + + if (picture.latLng != null) { + putString(KEY_LAT, picture.latLng.latitude.toString()) + putString(KEY_LNG, picture.latLng.longitude.toString()) + } + + val latLng = map.cameraPosition.target + val isDefaultLocation = distanceBetween(defaultLatLng, latLng) <= 1 + putBoolean(KEY_IS_DEFAULT_LOCATION, isDefaultLocation) + } + } + + super.onSaveInstanceState(outState) + } + + override fun onDestroy() { + with(map) { + setOnCameraMoveListener(null) + setOnCameraIdleListener(null) + } + + super.onDestroy() + } + companion object { + private val defaultLatLng = LatLng(SOUTH_KOREA_LAT, SOUTH_KOREA_LNG) + + private const val KEY_URI = "uri" + private const val KEY_LAT = "lat" + private const val KEY_LNG = "lng" + private const val KEY_IS_DEFAULT_LOCATION = "isDefaultLocation" private const val TEST_GROUP_ID = 1L } } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationViewModel.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationViewModel.kt index 2bc105c6..14afb9a5 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationViewModel.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/write/LocationViewModel.kt @@ -1,5 +1,6 @@ package com.cheocharm.presentation.ui.write +import android.os.Build import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,7 +10,10 @@ import com.cheocharm.domain.model.WriteImageRequest import com.cheocharm.domain.usecase.write.RequestWriteImagesUseCase import com.cheocharm.presentation.common.Event import com.cheocharm.presentation.common.TestValues +import com.cheocharm.presentation.enum.LatLngSelectionType +import com.cheocharm.presentation.model.Picture import com.cheocharm.presentation.model.Sticker +import com.cheocharm.presentation.util.GeocodeUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File @@ -28,6 +32,64 @@ class LocationViewModel @Inject constructor( var stickers: List = TestValues.testStickers private set + private val _picture = MutableLiveData() + val picture: LiveData = _picture + + private val _locationString = MutableLiveData() + val locationString: LiveData = _locationString + + fun loadPicture(picture: Picture, geocodeUtil: GeocodeUtil) { + _picture.value = picture + + if (picture.address == null) { + viewModelScope.launch { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + geocodeUtil.execute(picture) { + _picture.value?.address = it[0].getAddressLine(0) + } + } else { + geocodeUtil.execute(picture) + } + } + } + } + + fun geocode(geocodeUtil: GeocodeUtil, latLng: DoubleArray, type: LatLngSelectionType) { + viewModelScope.launch { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + geocodeUtil.execute(latLng, type, ::setSelectedLatLng) { + _locationString.postValue( + if (type != LatLngSelectionType.SPECIFIED) { + type.locationString + } else { + it[0].getAddressLine(0) ?: latLng.toCoordString() + } + ) + } + } else { + geocodeUtil.execute(latLng, type, ::setSelectedLatLng) + } + } + } + + fun setSelectedLatLng(latLng: DoubleArray, type: LatLngSelectionType, address: String? = null) { + _locationString.postValue( + if (type == LatLngSelectionType.SPECIFIED) { + address ?: latLng.toCoordString() + } else { + type.locationString + } + ) + } + + private fun DoubleArray.toCoordString(): String { + val format = "%.5f" + val lat = format.format(get(0)) + val lng = format.format(get(1)) + + return "($lat, $lng)" + } + fun uploadImages( groupId: Long, address: String?, diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureFragment.kt index 5d5fab11..d14239cf 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureFragment.kt @@ -14,15 +14,16 @@ import com.cheocharm.presentation.databinding.FragmentPictureBinding import com.cheocharm.presentation.model.Picture import com.cheocharm.presentation.util.GeocodeUtil import com.google.android.gms.maps.model.LatLng +import kotlinx.coroutines.Dispatchers class PictureFragment : BaseFragment(R.layout.fragment_picture) { - private val pictureViewModel: PictureViewModel by navGraphViewModels(R.id.write) + private val locationViewModel: LocationViewModel by navGraphViewModels(R.id.write) private val requestPermissionsLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> permissions.forEach { if (it.value.not()) { - TODO("권한 없을 때 처리") + // TODO: 권한 없을 때 처리 } } } @@ -35,9 +36,9 @@ class PictureFragment : BaseFragment(R.layout.fragment_p LatLng(array[0], array[1]) } val picture = Picture(uri, latLng) + val geocodeUtil = GeocodeUtil(requireContext(), Dispatchers.IO) - pictureViewModel.setPicture(picture) - GeocodeUtil.execute(requireContext(), picture) + locationViewModel.loadPicture(picture, geocodeUtil) inputStream.close() } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureViewModel.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureViewModel.kt deleted file mode 100644 index 0559e6db..00000000 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/write/PictureViewModel.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.cheocharm.presentation.ui.write - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.cheocharm.presentation.model.Picture -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class PictureViewModel @Inject constructor() : ViewModel() { - private val _picture = MutableLiveData() - val picture: LiveData = _picture - - fun setPicture(picture: Picture) { - _picture.value = picture - } -} diff --git a/presentation/src/main/java/com/cheocharm/presentation/util/GeocodeUtil.kt b/presentation/src/main/java/com/cheocharm/presentation/util/GeocodeUtil.kt index 6504b408..f521f269 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/util/GeocodeUtil.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/util/GeocodeUtil.kt @@ -2,26 +2,72 @@ package com.cheocharm.presentation.util import android.content.Context import android.location.Geocoder +import android.location.Geocoder.GeocodeListener +import android.os.Build +import com.cheocharm.presentation.enum.LatLngSelectionType import com.cheocharm.presentation.model.Picture +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.util.* -object GeocodeUtil { - fun execute( - context: Context, - picture: Picture - ) { - picture.latLng?.let { - try { - val geocoder = Geocoder(context, Locale.KOREAN) - val addresses = geocoder.getFromLocation(it.latitude, it.longitude, 1) +class GeocodeUtil( + context: Context, + private val coroutineDispatcher: CoroutineDispatcher +) { + private val geocoder = Geocoder(context, Locale.KOREAN) + suspend fun execute(picture: Picture, geocodeListener: GeocodeListener? = null) = + withContext(coroutineDispatcher) { + picture.latLng?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && geocodeListener != null) { + runCatching { + geocoder.getFromLocation(it.latitude, it.longitude, 1, geocodeListener) + }.onFailure { throwable -> + throwable.printStackTrace() + } + } else { + runCatching { + geocoder.getFromLocation(it.latitude, it.longitude, 1) + }.onSuccess { addresses -> + if (addresses != null && addresses.isEmpty().not()) { + val fetchedAddress = addresses.first() + val address = fetchedAddress.getAddressLine(0) + picture.address = address + } + }.onFailure { throwable -> + throwable.printStackTrace() + } + } + } + } + + suspend fun execute( + latLng: DoubleArray, + type: LatLngSelectionType, + callback: (DoubleArray, LatLngSelectionType, String?) -> Unit, + geocodeListener: GeocodeListener? = null + ) = withContext(coroutineDispatcher) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && geocodeListener != null) { + runCatching { + geocoder.getFromLocation(latLng[0], latLng[1], 1, geocodeListener) + }.onFailure { throwable -> + throwable.printStackTrace() + } + } else { + runCatching { + geocoder.getFromLocation(latLng[0], latLng[1], 1) + }.onSuccess { addresses -> if (addresses != null && addresses.isEmpty().not()) { val fetchedAddress = addresses.first() val address = fetchedAddress.getAddressLine(0) - picture.address = address + callback(latLng, type, address) + return@withContext + } else { + callback(latLng, type, null) } - } catch (e: Exception) { - e.printStackTrace() + }.onFailure { throwable -> + throwable.printStackTrace() } } } diff --git a/presentation/src/main/res/color/color_map_z_red_20.xml b/presentation/src/main/res/color/color_map_z_red_20.xml new file mode 100644 index 00000000..b247af46 --- /dev/null +++ b/presentation/src/main/res/color/color_map_z_red_20.xml @@ -0,0 +1,4 @@ + + + + diff --git a/presentation/src/main/res/drawable/bg_marker_around.xml b/presentation/src/main/res/drawable/bg_marker_around.xml new file mode 100644 index 00000000..aee2bce9 --- /dev/null +++ b/presentation/src/main/res/drawable/bg_marker_around.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/presentation/src/main/res/layout/fragment_home.xml b/presentation/src/main/res/layout/fragment_home.xml index 55a0961c..7b11e65c 100644 --- a/presentation/src/main/res/layout/fragment_home.xml +++ b/presentation/src/main/res/layout/fragment_home.xml @@ -1,5 +1,6 @@ @@ -17,9 +18,15 @@ + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{@string/format_home_count(viewmodel.zoomLevel)}" + android:textSize="@dimen/text_xx_large" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Zoom Level: null" /> diff --git a/presentation/src/main/res/layout/fragment_location.xml b/presentation/src/main/res/layout/fragment_location.xml index 92fe62fe..a5e9dc07 100644 --- a/presentation/src/main/res/layout/fragment_location.xml +++ b/presentation/src/main/res/layout/fragment_location.xml @@ -7,7 +7,7 @@ + type="com.cheocharm.presentation.ui.write.LocationViewModel" /> @@ -25,7 +25,28 @@ app:title="@string/location_title" app:menu="@menu/menu_base" /> + + + + 120dp 10dp 15dp + 37dp + 22dp 50dp 20dp 30dp diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index a77c3c5e..555f8d14 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -12,7 +12,7 @@ Search Write My Page - Home %d + Zoom Level: %f Search %d Write %d My Page %d