저번 글에서 MapView를 사용해서 지도 띄우기까지 진행했는데 이번에는 현재 위치를 위치 마커로 찍는 것을 해보려고 한다!
일단 코드를 상세하게 하나씩 보기 전에 전체 코드다.
@AndroidEntryPoint
class MapFragment : BaseMapFragment<FragmentMapBinding>(R.layout.fragment_map) {
private val mapViewModel: MapViewModel by viewModels()
private lateinit var naverMap: NaverMap
private lateinit var locationSource: FusedLocationSource // 현재 위치
private lateinit var fusedLocationClient: FusedLocationProviderClient
override var mapView: MapView? = null
override fun initOnCreateView() {
initMapView()
}
override fun initOnMapReady(naverMap: NaverMap) {
initNaverMap(naverMap)
setCameraChangeListener()
}
override fun iniViewCreated() {
observeClusterMarkerClick()
clickLocationSearchBtn()
}
override fun initOnResume() {
}
private fun initMapView() { // mapView 초기화
mapView = binding.mapView
mapView?.getMapAsync(this)
locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
}
private fun initNaverMap(naverMap: NaverMap) { // 위치 및 naverMap 세팅
this.naverMap = naverMap
this.naverMap.locationSource = locationSource
getLastLocation(naverMap)
}
private fun getLastLocation(map: NaverMap) { // 마지막 위치 가져오기
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
checkLocationPermission(requireActivity())
requireContext().requestMapPermission {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
val loc = if (location == null) {
LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE)
} else {
LatLng(location.latitude, location.longitude)
}
map.cameraPosition = CameraPosition(loc, DEFAULT_ZOOM)
map.locationTrackingMode = LocationTrackingMode.Follow
}
}
}
companion object {
const val DEFAULT_LATITUDE = 37.563242272383114
const val DEFAULT_LONGITUDE = 126.92566852521531
const val DEFAULT_ZOOM = 15.0
const val LOCATION_PERMISSION_REQUEST_CODE = 1000
}
}
🗺️ NaverMap 객체 등록
네이버 설명에 따르면
하나의 지도는 뷰 요소와 인터페이스 요소로 구성된다.
여기서 뷰 요소는 화면에 지도를 나타내는 역할을 하는데 → MapFragment, MapView가 여기에 해당 한다.
지도를 다루는 인터페이스 역할을 인터페이스 요소는 NaverMap 클래스가 담당한다. 이때 NaverMap 객체는 오직 콜백 메서드를 이용해서 얻어올 수 있다고 한다.
MapFragment 또는 MapView의 getMapAsync() 메서드로 OnMapReadyCallback을 등록하게 되면 비동기로 NaverMap 객체를 얻을 수 있다.
NaverMap 객체가 준비될 경우 onMapReady() 콜백 메서드가 호출된다.
private lateinit var naverMap: NaverMap
naverMap을 Fragment 전역에서 사용할 것이기 때문에 늦은 초기화로 따로 빼주었다.
override fun initOnCreateView() {
initMapView()
}
private fun initMapView() { // mapView 초기화
mapView = binding.mapView
mapView?.getMapAsync(this)
}
그 뒤 getMapAsync() 메서드를 호출하는 initMapView() 라는 메서드를 만들어 이를 onCreateView() 에서 호출하도록 코드를 작성했다.
이렇게 코드를 작성하면 onMapReady()라는 콜백 메서드가 호출되는데 이때, naverMap 객체도 같이 넘어온다.
그러면 이 naverMap 객체를 초기에 선언해뒀던 naverMap에 초기화 시켜주면 된다.
override fun initOnMapReady(naverMap: NaverMap) {
this.naverMap = naverMap
}
나의 경우 저번에 봤다시피 BaseMapFragment라는 추상 클래스를 따로 만들고, initOnMapReady를 추상 메서드로 onMapReady() 안에 넣어두었기 때문에 위와 같이 코드를 작성했다.
이렇게 하면 NaverMap 객체 선언은 끝!
🗺️ 현재 위치 불러오기
네이버 지도 SDK는 사용자의 위치 이벤트를 받아 지도에 표시하고 카메라를 움직이는 위치 추적 기능을 내장하고 있다.
💡 권한 및 LocationSource
네이버 지도 SDK는 기본적으로 사용자의 위치 정보를 사용하지 않기 때문에 위치 추적 기능을 사용하기 위해서는 AndroidManifest.xml에 다음과 같이 권한을 명시해줘야 한다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
그 뒤 setLocationSource() 를 호출해서 LocationSource 구현체를 지정해야 한다.
LocationSource → 네이버 지도 SDK에 위치를 제공하는 인터페이스이다. LocationSource의 active(), deactive() 등의 메서드는 지도 객체가 호출하기 때문에 개발자가 직접 호출해서는 안된다고 한다.
💡 FusedLocationSource
네이버 지도 SDK에서는 Google Play 서비스의 FusedLocationProviderClient의 지자기, 가속 센서를 활용해 최적의 위치를 반환하는 구현체인 FusedLocationSource를 제공한다고 한다.
그래서 이를 활용하려면 먼저, 앱 모듈의 build.gradle에 다음과 같이 선언해준다
dependencies {
implementation("com.google.android.gms:play-services-location:21.0.1")
}
여기서 부터 조금 복잡한데, 이것을 사용하기 위해서는 런타임 권한 요청이 필요하기 때문에 onRequestPermissionResult()의 결과를 FusedLocationSource의 onRequestPermissionResult()에 전달해야 한다.
private lateinit var locationSource: FusedLocationSource // 현재 위치
private lateinit var fusedLocationClient: FusedLocationProviderClient
private fun initMapView() { // mapView 초기화
mapView = binding.mapView
mapView?.getMapAsync(this)
locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
}
아까 위에 썼던 initMapView() 메소드 안에 locationSource를 FusedLocationSource로 초기화했다.
저 LOCATION_PERMISSION_REQUEST_CODE는 내가 임의로 상수로 지정한 것!
override fun initOnMapReady(naverMap: NaverMap) {
this.naverMap = naverMap
this.naverMap.locationSource = locationSource
}
그리고 initOnMapReady() 메소드 안에서 naverMap의 locationSource 구현체에 fusedLocationSource 객체를 지정해줬다.
💡 FusedLocationProviderClient
FusedLocationSource로는 사용자의 마지막 위치 그대로 띄어주는데 제약이 있어서 직접 구글에 FusedLocationProviderClient를 추가해서 사용했다.
private fun getLastLocation(map: NaverMap) { // 마지막 위치 가져오기
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
checkLocationPermission(requireActivity())
requireContext().requestMapPermission {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
val loc = if (location == null) {
LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE)
} else {
LatLng(location.latitude, location.longitude)
}
map.cameraPosition = CameraPosition(loc, DEFAULT_ZOOM)
map.locationTrackingMode = LocationTrackingMode.Follow
}
}
}
권한을 받아야 하는 코드가 필요해서 아래에 있는 확장 함수를 따로 만들어 줬다.
fun Context.requestMapPermission(complete: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
TedPermission.create()
.setDeniedMessage(this.getString(R.string.permission_denied_message)) // 권한이 없을 때 띄워주는 Dialog Message
.setDeniedCloseButtonText(this.getString(R.string.permission_denied_closed_button_message))
.setGotoSettingButtonText(this.getString(R.string.permission_go_to_setting_button_message))
.setPermissions(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() { // 권한이 허용됐을 때
Timber.d("권한 허용 완료!")
complete()
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) { // 권한이 거부됐을 때
Timber.d("권한 허용이 거부됨")
}
})
.check()
}
}
fun checkLocationPermission(context: Context) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION,
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION,
) != PackageManager.PERMISSION_GRANTED
) {
return
}
}
권한 설정은 TedPermission을 활용해서 작성했고, 그 위에 한번 더 검토하는 느낌(? 이렇게 아니면 계속 오류가 발생하더라…)으로 코드를 작성했다.
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
val loc = if (location == null) {
LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE)
} else {
LatLng(location.latitude, location.longitude)
}
map.cameraPosition = CameraPosition(loc, DEFAULT_ZOOM)
map.locationTrackingMode = LocationTrackingMode.Follow
}
처음 앱을 사용하거나 해서 마지막 GPS가 갱신된 적이 없다면 임의로 설정한 곳에 카메라가 위치하도록 했고, 갱신된 적이 있다면 그곳으로 카메라와 마커가 위치하도록 했다.
*참고
💡 위치 추적 모드
naverMap에 LocationSource를 지정하면 위치 추적 기능을 사용할 수 있다.
map.locationTrackingMode = LocationTrackingMode.Follow
이때, 추적 모드는 총 네가지 인데
None : 위치를 추적하지 않는다.
NoFollow : 위치 추적이 활성화되고, 지도는 움직이지 않고 위치 오버레이가 사용자의 위치를 따라 움직임.
Follow : 위치 추적이 활성화되고, 위치 오버레이와 카메라의 좌표가 사용자의 위치를 따라 움직임.
Face : 위치 추적이 활성화되고, 현위치 오버레이, 카메라의 좌표, 베어링이 사용자의 위치 및 방향을 따라 움직인다.
이렇게 코드 작성하면 끝!
다음번 글에는 클러스터링에 대해 작성해 볼 예정
'Android > Android 적용' 카테고리의 다른 글
[Android 적용] Health Service API(with.watch OS) (2) | 2024.11.25 |
---|---|
[Android 적용] TextView 더보기 기능 (0) | 2024.07.28 |
[Android 적용] debounce 코루틴 구현 (0) | 2024.06.29 |
[Android 적용] 네이버 지도 API(3) - 클러스터링 마커 (0) | 2024.06.13 |
[Android 적용] 네이버 지도 API(1) - 지도 띄우기(with.mapView) (7) | 2024.06.10 |