위치
지도를 사용하는 앱은 사용자의 위치를 추척하고 지도에 표현하는 경우가 많습니다. 네이버 지도 SDK는 이런 기능을 손쉽게 구현할 수 있도록 위치 오버레이와 위치 추적 기능을 제공합니다. 내장된 위치 추적 기능을 사용하지 않고 직접 위치 관련 기능을 구현할 수도 있습니다.
위치 오버레이
사용자의 위치를 지도에 나타내고자 할 때는 위치 오버레이를 사용하는 것이 권장됩니다. 위치 오버레이는 지도상에 단 하나만 존재하며, 좌표, 방향 등 위치 표현에 특화된 기능을 제공합니다. 위치 오버레이에 대한 자세한 내용은 위치 오버레이 문서를 참고하십시오.
내장 위치 추적 기능 사용
네이버 지도 SDK는 사용자의 위치 이벤트를 받아서 이를 지도에 표시하고 카메라를 움직이는 위치 추적 기능을 내장하고 있습니다.
권한과 LocationSource
네이버 지도 SDK는 기본적으로 사용자의 위치 정보를 사용하지 않으므로 사용자에게 위치와 관련된 권한을 요구하지 않습니다. 따라서 위치 추적 기능을 사용하고자 하는 앱은 AndroidManifest.xml
에 ACCESS_COARSE_LOCATION
또는 ACCESS_FINE_LOCATION
권한을 명시해야 합니다.
다음은 AndroidManifext.xml
에 ACCESS_FINE_LOCATION
권한을 명시하는 예제입니다.
<manifest>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
또한 setLocationSource()
를 호출해 LocationSource
구현체를 지정해야 합니다. LocationSource
는 네이버 지도 SDK에 위치를 제공하는 인터페이스입니다. activate()
, deactivate()
등 LocationSource
의 메서드는 지도 객체가 호출하므로 개발자가 직접 호출해서는 안됩니다.
FusedLocationSource
네이버 지도 SDK는 Google Play 서비스의 FusedLocationProviderClient
와 지자기, 가속도 센서를 활용해 최적의 위치를 반환하는 구현체인 FusedLocationSource
를 제공합니다. FusedLocationSource
를 사용하려면 앱 모듈의 build.gradle
에 play-services-location
21.0.1 이상 버전에 대한 의존성을 추가해야 합니다.
dependencies {
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
Groovy
dependencies {
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
Kotlin
dependencies {
implementation("com.google.android.gms:play-services-location:21.0.1")
}
FusedLocationSource
는 런타임 권한 처리를 위해 액티비티 또는 프래그먼트를 필요로 합니다. 생성자에 액티비티나 프래그먼트 객체를 전달하고 권한 요청 코드를 지정해야 합니다. 그리고 onRequestPermissionResult()
의 결과를 FusedLocationSource
의 onRequestPermissionsResult()
에 전달해야 합니다.
다음은 액티비티에서 FusedLocationSource
를 생성하고 NaverMap
에 지정하는 예제입니다.
public class LocationTrackingActivity extends AppCompatActivity
implements OnMapReadyCallback {
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1000;
private FusedLocationSource locationSource;
private NaverMap naverMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
locationSource =
new FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
if (locationSource.onRequestPermissionsResult(
requestCode, permissions, grantResults)) {
if (!locationSource.isActivated()) { // 권한 거부됨
naverMap.setLocationTrackingMode(LocationTrackingMode.None);
}
return;
}
super.onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
@Override
public void onMapReady(@NonNull NaverMap naverMap) {
this.naverMap = naverMap;
naverMap.setLocationSource(locationSource);
}
}
Java
public class LocationTrackingActivity extends AppCompatActivity
implements OnMapReadyCallback {
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1000;
private FusedLocationSource locationSource;
private NaverMap naverMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
locationSource =
new FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
if (locationSource.onRequestPermissionsResult(
requestCode, permissions, grantResults)) {
if (!locationSource.isActivated()) { // 권한 거부됨
naverMap.setLocationTrackingMode(LocationTrackingMode.None);
}
return;
}
super.onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
@Override
public void onMapReady(@NonNull NaverMap naverMap) {
this.naverMap = naverMap;
naverMap.setLocationSource(locationSource);
}
}
Kotlin
class LocationTrackingActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var locationSource: FusedLocationSource
private lateinit var naverMap: NaverMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
locationSource =
FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
if (locationSource.onRequestPermissionsResult(requestCode, permissions,
grantResults)) {
if (!locationSource.isActivated) { // 권한 거부됨
naverMap.locationTrackingMode = LocationTrackingMode.None
}
return
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onMapReady(naverMap: NaverMap) {
this.naverMap = naverMap
naverMap.locationSource = locationSource
}
companion object {
private const val LOCATION_PERMISSION_REQUEST_CODE = 1000
}
}
위치 추적 모드
NaverMap
에 LocationSource
를 지정하면 위치 추적 기능을 사용할 수 있습니다. 위치 추적 기능을 사용하는 방법은 크게 두 가지입니다.
- 위치 추적 모드 지정:
setLocationTrackingMode()
를 호출하면 프로그램적으로 위치 추적 모드를 지정할 수 있습니다. - 현위치 버튼 컨트롤 사용:
UiSettings.setLocationButtonEnabled(true)
로 현위치 버튼 컨트롤을 활성화하면 사용자의 클릭에 따라 위치 추적 모드를 변경할 수 있습니다.
위치 추적 모드는 다음의 네 가지이며, LocationTrackingMode
열거형에 정의되어 있습니다.
Follow
: 위치 추적이 활성화되고, 현위치 오버레이와 카메라의 좌표가 사용자의 위치를 따라 움직입니다. API나 제스처를 사용해 임의로 카메라를 움직일 경우 모드가NoFollow
로 바뀝니다.
Face
:위치 추적이 활성화되고, 현위치 오버레이, 카메라의 좌표, 베어링이 사용자의 위치 및 방향을 따라 움직입니다. API나 제스처를 사용해 임의로 카메라를 움직일 경우 모드가NoFollow
로 바뀝니다.
다음은 setLocationTrackingMode()
를 호출해 위치 추적 모드를 Follow
로 지정하는 예제입니다.
naverMap.setLocationTrackingMode(LocationTrackingMode.Follow);
Java
naverMap.setLocationTrackingMode(LocationTrackingMode.Follow);
Kotlin
naverMap.locationTrackingMode = LocationTrackingMode.Follow
위치 변경 이벤트
addOnLocationChangeListener()
메서드로 OnLocationChangeListener
를 등록하면 위치 변경에 대한 이벤트를 받을 수 있습니다. 위치 추적 모드가 활성화되고 사용자의 위치가 변경되면 onLocationChange()
콜백 메서드가 호출되며, 파라미터로 사용자의 위치가 전달됩니다.
다음은 사용자의 위치가 변경되면 그 좌표를 토스트로 표시하는 예제입니다.
naverMap.addOnLocationChangeListener(location ->
Toast.makeText(this,
location.getLatitude() + ", " + location.getLongitude(),
Toast.LENGTH_SHORT).show());
Java
naverMap.addOnLocationChangeListener(location ->
Toast.makeText(this,
location.getLatitude() + ", " + location.getLongitude(),
Toast.LENGTH_SHORT).show());
Kotlin
naverMap.addOnLocationChangeListener { location ->
Toast.makeText(this, "${location.latitude}, ${location.longitude}",
Toast.LENGTH_SHORT).show()
}
커스텀 구현
네이버 지도 SDK가 내장하고 있는 LocationSource
나 위치 추적 기능을 사용하기 부적합한 경우 직접 구현할 수 있습니다.
커스텀 위치 소스
내장 위치 트래킹 모드를 사용할 경우 센서를 활용하고 권한 처리 기능도 잘 구현되어 있는 FusedLocationSource
를 사용하는 것이 권장됩니다. 그러나 필요한 경우 LocationSource
를 직접 구현할 수도 있습니다.
LocationSource
의 메서드는 네이버 지도 SDK가 호출합니다. 위치 트래킹 모드가 활성화되면 activate()
가, 비활성화되면 deactivate()
가 호출됩니다. 따라서 LocationSource
구현체는 activate()
가 호출되면 사용자의 위치를 트래킹하기 시작하고, deactivate()
가 호출되면 트래킹을 중지해야 합니다. 또한 위치 트래킹이 활성화된 동안에는 사용자의 위치가 변경될 때 마다 activate()
에 전달된 LocationSource.OnLocationChangedListener
객체의 onLocationChanged()
를 호출해야 합니다.
위치 권한과 관련된 처리도 LocationSource
의 역할입니다. activate()
가 호출될 때 권한을 검사하고 필요한 경우 요청해야 합니다.
다음은 LocationManager
를 사용해 GPS 위치를 수신하도록 구현한 LocationSource
구현체 예제입니다.
public class GpsOnlyLocationSource implements LocationSource, LocationListener {
@NonNull
private final Context context;
@Nullable
private final LocationManager locationManager;
@Nullable
private LocationSource.OnLocationChangedListener listener;
public GpsOnlyLocationSource(@NonNull Context context) {
this.context = context;
locationManager =
(LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
public void activate(
@NonNull LocationSource.OnLocationChangedListener listener) {
if (locationManager == null) {
return;
}
if (PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 권한 요청 로직 생략
return;
}
this.listener = listener;
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
@Override
public void deactivate() {
if (locationManager == null) {
return;
}
listener = null;
locationManager.removeUpdates(this);
}
@Override
public void onLocationChanged(Location location) {
if (listener != null) {
listener.onLocationChanged(location);
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
}
Java
public class GpsOnlyLocationSource implements LocationSource, LocationListener {
@NonNull
private final Context context;
@Nullable
private final LocationManager locationManager;
@Nullable
private LocationSource.OnLocationChangedListener listener;
public GpsOnlyLocationSource(@NonNull Context context) {
this.context = context;
locationManager =
(LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
public void activate(
@NonNull LocationSource.OnLocationChangedListener listener) {
if (locationManager == null) {
return;
}
if (PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 권한 요청 로직 생략
return;
}
this.listener = listener;
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
@Override
public void deactivate() {
if (locationManager == null) {
return;
}
listener = null;
locationManager.removeUpdates(this);
}
@Override
public void onLocationChanged(Location location) {
if (listener != null) {
listener.onLocationChanged(location);
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
}
Kotlin
class GpsOnlyLocationSource(
private val context: Context) : LocationSource, LocationListener {
private val locationManager = context
.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
private var listener: LocationSource.OnLocationChangedListener? = null
override fun activate(listener: LocationSource.OnLocationChangedListener) {
if (locationManager == null) {
return
}
if (PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& PermissionChecker.checkSelfPermission(context,
Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 권한 요청 로직 생략
return
}
this.listener = listener
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10f, this)
}
override fun deactivate() {
if (locationManager == null) {
return
}
listener = null
locationManager.removeUpdates(this)
}
override fun onLocationChanged(location: Location) {
listener?.onLocationChanged(location)
}
override fun onStatusChanged(provider: String, status: Int,
extras: Bundle) {
}
override fun onProviderEnabled(provider: String) {
}
override fun onProviderDisabled(provider: String) {
}
}
커스텀 위치 추적
사용자의 위치를 지도에 나타낼 때 반드시 내장된 위치 추적 기능을 사용할 필요는 없습니다. 커스터마이징이 필요할 경우 앱에서 직접 위치 이벤트를 받아서 LocationOverlay
의 position
과 bearing
을 지정하고, 필요에 따라 카메라를 이동하도록 구현할 수 있습니다.
다음은 액티비티에서 직접 LocationManager
를 사용해 GPS 위치를 수신하고 LocationOverlay
와 카메라를 움직이는 예제입니다.
public class CustomLocationTrackingActivity extends AppCompatActivity
implements LocationListener {
private static final int PERMISSION_REQUEST_CODE = 100;
private static final String[] PERMISSIONS = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
private NaverMap map;
@Nullable
private LocationManager locationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
FragmentManager fm = getSupportFragmentManager();
MapFragment mapFragment = (MapFragment)fm.findFragmentById(R.id.map);
if (mapFragment == null) {
mapFragment = MapFragment.newInstance();
fm.beginTransaction().add(R.id.map, mapFragment).commit();
}
mapFragment.getMapAsync(naverMap -> map = naverMap);
locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
if (hasPermission() && locationManager != null) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
return;
}
super.onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
@Override
protected void onStart() {
super.onStart();
if (hasPermission()) {
if (locationManager != null) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
} else {
ActivityCompat.requestPermissions(
this, PERMISSIONS, PERMISSION_REQUEST_CODE);
}
}
@Override
protected void onStop() {
super.onStop();
if (locationManager != null) {
locationManager.removeUpdates(this);
}
}
@Override
public void onLocationChanged(Location location) {
if (map == null || location == null) {
return;
}
LatLng coord = new LatLng(location);
LocationOverlay locationOverlay = map.getLocationOverlay();
locationOverlay.setVisible(true);
locationOverlay.setPosition(coord);
locationOverlay.setBearing(location.getBearing());
map.moveCamera(CameraUpdate.scrollTo(coord));
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
private boolean hasPermission() {
return PermissionChecker.checkSelfPermission(this, PERMISSIONS[0])
== PermissionChecker.PERMISSION_GRANTED
&& PermissionChecker.checkSelfPermission(this, PERMISSIONS[1])
== PermissionChecker.PERMISSION_GRANTED;
}
}
Java
public class CustomLocationTrackingActivity extends AppCompatActivity
implements LocationListener {
private static final int PERMISSION_REQUEST_CODE = 100;
private static final String[] PERMISSIONS = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
private NaverMap map;
@Nullable
private LocationManager locationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
FragmentManager fm = getSupportFragmentManager();
MapFragment mapFragment = (MapFragment)fm.findFragmentById(R.id.map);
if (mapFragment == null) {
mapFragment = MapFragment.newInstance();
fm.beginTransaction().add(R.id.map, mapFragment).commit();
}
mapFragment.getMapAsync(naverMap -> map = naverMap);
locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
if (hasPermission() && locationManager != null) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
return;
}
super.onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
@Override
protected void onStart() {
super.onStart();
if (hasPermission()) {
if (locationManager != null) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10, this);
}
} else {
ActivityCompat.requestPermissions(
this, PERMISSIONS, PERMISSION_REQUEST_CODE);
}
}
@Override
protected void onStop() {
super.onStop();
if (locationManager != null) {
locationManager.removeUpdates(this);
}
}
@Override
public void onLocationChanged(Location location) {
if (map == null || location == null) {
return;
}
LatLng coord = new LatLng(location);
LocationOverlay locationOverlay = map.getLocationOverlay();
locationOverlay.setVisible(true);
locationOverlay.setPosition(coord);
locationOverlay.setBearing(location.getBearing());
map.moveCamera(CameraUpdate.scrollTo(coord));
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
private boolean hasPermission() {
return PermissionChecker.checkSelfPermission(this, PERMISSIONS[0])
== PermissionChecker.PERMISSION_GRANTED
&& PermissionChecker.checkSelfPermission(this, PERMISSIONS[1])
== PermissionChecker.PERMISSION_GRANTED;
}
}
Kotlin
class CustomLocationTrackingActivity : AppCompatActivity(), LocationListener {
private var map: NaverMap? = null
private var locationManager: LocationManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val fm = supportFragmentManager
val mapFragment = fm.findFragmentById(R.id.map) as MapFragment?
?: MapFragment.newInstance().also {
fm.beginTransaction().add(R.id.map, it).commit()
}
mapFragment.getMapAsync { naverMap ->
map = naverMap
}
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager?
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
if (requestCode == PERMISSION_REQUEST_CODE) {
if (hasPermission()) {
locationManager?.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10f, this)
}
return
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onStart() {
super.onStart()
if (hasPermission()) {
locationManager?.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 10f, this)
} else {
ActivityCompat.requestPermissions(
this, PERMISSIONS, PERMISSION_REQUEST_CODE)
}
}
override fun onStop() {
super.onStop()
locationManager?.removeUpdates(this)
}
override fun onLocationChanged(location: Location?) {
if (location == null) {
return
}
map?.let {
val coord = LatLng(location)
val locationOverlay = it.locationOverlay
locationOverlay.isVisible = true
locationOverlay.position = coord
locationOverlay.bearing = location.bearing
it.moveCamera(CameraUpdate.scrollTo(coord))
}
}
override fun onStatusChanged(provider: String, status: Int,
extras: Bundle) {
}
override fun onProviderEnabled(provider: String) {
}
override fun onProviderDisabled(provider: String) {
}
private fun hasPermission(): Boolean {
return PermissionChecker.checkSelfPermission(this, PERMISSIONS[0]) ==
PermissionChecker.PERMISSION_GRANTED &&
PermissionChecker.checkSelfPermission(this, PERMISSIONS[1]) ==
PermissionChecker.PERMISSION_GRANTED
}
companion object {
private const val PERMISSION_REQUEST_CODE = 100
private val PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION)
}
}