위치

지도를 사용하는 앱은 사용자의 위치를 추척하고 지도에 표현하는 경우가 많습니다. 네이버 지도 SDK는 이런 기능을 손쉽게 구현할 수 있도록 위치 오버레이와 위치 추적 기능을 제공합니다. 내장된 위치 추적 기능을 사용하지 않고 직접 위치 관련 기능을 구현할 수도 있습니다.

위치 오버레이

사용자의 위치를 지도에 나타내고자 할 때는 위치 오버레이를 사용하는 것이 권장됩니다. 위치 오버레이는 지도상에 단 하나만 존재하며, 좌표, 방향 등 위치 표현에 특화된 기능을 제공합니다. 위치 오버레이에 대한 자세한 내용은 위치 오버레이 문서를 참고하십시오.

내장 위치 추적 기능 사용

네이버 지도 SDK는 사용자의 위치 이벤트를 받아서 이를 지도에 표시하고 카메라를 움직이는 위치 추적 기능을 내장하고 있습니다.

권한과 LocationSource

네이버 지도 SDK는 기본적으로 사용자의 위치 정보를 사용하지 않으므로 사용자에게 위치와 관련된 권한을 요구하지 않습니다. 따라서 위치 추적 기능을 사용하고자 하는 앱은 AndroidManifest.xmlACCESS_COARSE_LOCATION 또는 ACCESS_FINE_LOCATION 권한을 명시해야 합니다.

다음은 AndroidManifext.xmlACCESS_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.gradleplay-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()의 결과를 FusedLocationSourceonRequestPermissionsResult()에 전달해야 합니다.

다음은 액티비티에서 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
    }
}

위치 추적 모드

NaverMapLocationSource를 지정하면 위치 추적 기능을 사용할 수 있습니다. 위치 추적 기능을 사용하는 방법은 크게 두 가지입니다.

  • 위치 추적 모드 지정: setLocationTrackingMode()를 호출하면 프로그램적으로 위치 추적 모드를 지정할 수 있습니다.
  • 현위치 버튼 컨트롤 사용: UiSettings.setLocationButtonEnabled(true)로 현위치 버튼 컨트롤을 활성화하면 사용자의 클릭에 따라 위치 추적 모드를 변경할 수 있습니다.

위치 추적 모드는 다음의 네 가지이며, LocationTrackingMode 열거형에 정의되어 있습니다.

  • None: 위치를 추적하지 않습니다.
  • NoFollow: 위치 추적이 활성화되고, 현위치 오버레이가 사용자의 위치를 따라 움직입니다. 그러나 지도는 움직이지 않습니다.

NoFollow 모드가 적용된 모습

  • Follow: 위치 추적이 활성화되고, 현위치 오버레이와 카메라의 좌표가 사용자의 위치를 따라 움직입니다. API나 제스처를 사용해 임의로 카메라를 움직일 경우 모드가 NoFollow로 바뀝니다.

Follow 모드가 적용된 모습

  • Face:위치 추적이 활성화되고, 현위치 오버레이, 카메라의 좌표, 베어링이 사용자의 위치 및 방향을 따라 움직입니다. API나 제스처를 사용해 임의로 카메라를 움직일 경우 모드가 NoFollow로 바뀝니다.

Face 모드가 적용된 모습

다음은 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) {
    }
}

커스텀 위치 추적

사용자의 위치를 지도에 나타낼 때 반드시 내장된 위치 추적 기능을 사용할 필요는 없습니다. 커스터마이징이 필요할 경우 앱에서 직접 위치 이벤트를 받아서 LocationOverlaypositionbearing을 지정하고, 필요에 따라 카메라를 이동하도록 구현할 수 있습니다.

다음은 액티비티에서 직접 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)
    }
}

results matching ""

    No results matching ""