본문 바로가기

Study/Android

Android : Google Maps, 구글 지도 사용하기

위치서비스 등을 사용하여 지도에 위치를 표시하거나 위치정보를 기반으로 좌표나 주소로 변경할 수 있습니다.

준비된 예제는 지도에 위치정보를 수집하여 현재 위치를 표시해주거나 지정한 위치를 지도에 표시해주는 것입니다.


준비사항


API KEY 발급

구글 지도를 사용하기 위해서는 API키 발급이 필요합니다.

아래 링크에서 발급하여 사용할 수 있습니다.

https://developers.google.com/maps/documentation/android/start#get-key

여기에서 STEP3로 바로 넘어가서 사용자 인증 정보 페이지로 이동합니다.

"프로젝트 선택"을 통해 프로젝트를 만들어줍니다.

그리고 API키를 발급받아 가져와줍니다.

Maps프로젝트 생성

신규 프로젝트는 Map이 있는 프로젝트로 생성해줍니다.

그리고 google_maps_api.xml에 있는 <string name="google_maps_key" translatable="false" templateMergeStrategy="preserve">태그에 발급받은 API키를 넣어줍니다.

의존성 추가 및 권한 부여

우선 build.gradle에 다음과 같이 추가해줍니다.
implementation 'com.google.android.gms:play-services-location:17.1.0'

그리고 AndroidManifest.xml에 다음과 같이 추가해줍니다.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

MainActivity 작성하기


필요한 변수 선언

lateinit var locationPermission: ActivityResultLauncher<Array<String>>
lateinit var fusedLocationClient:FusedLocationProviderClient
lateinit var locationCallback:LocationCallback

lateinitby lazy와 같은 것으로 Main클래스가 생성될 때 초기화되지 않고 변수가 호출(사용)될 때 초기화 되도록 합니다.
locationPermission는 위치정보에 대한 권한 부여를 위해 필요한 변수입니다.
fusedLocationClient는 위치 서비스가 GPS를 이용하여 위치를 확인하기 위해 선언하였습니다. 위에서 gradle에 추가한 바로 그것입니다.
locationCallback은 위치값 요청에 대한 갱신 정보를 받을 함수입니다.

지금부터 3가지의 커스텀 함수를 작성해주겠습니다.

프로세스 시작

우선 클래스가 OnMapReadyCallback을 상속받게 해줍니다.

class MapsActivity : AppCompatActivity(), OnMapReadyCallback { ... }

이렇게 추가해주고 아래와 같은 함수를 작성할 것입니다.

fun startProcess() {
    val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
    mapFragment.getMapAsync(this)
}

Maps 프로젝트는 뷰 자체가 프래그먼트이기 때문에 프래그먼트 매니저로 제어합니다.
이 때 getMapAsync는 프래그먼트에서 콜백을 설정할 수 있게 하며 인자로 this 즉 상속받은 OnMapReadyCallback을 지정해줍니다.

위치 정보를 실시간으로 업데이트 해주는 함수

@SuppressLint("MissingPermission")
fun updateLocation() {
    val locationRequest = LocationRequest.create()
    locationRequest.run {
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        interval = 1000
    }

    locationCallback = object : LocationCallback() {
        // 1초에 한번씩 변경되는 위치정보가 onLocationResult로 전달됨
        override fun onLocationResult(locationResult: LocationResult?) {
            locationResult?.let {
                for(location in it.locations) {
                    Log.d("위치정보 - 위도", "${location.latitude}")
                    Log.d("위치정보 - 경도", "${location.longitude}")

                }
            }
        }
    }

    // 권한 처리
    fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())

}

위치정보는 실시간으로 업데이트되어 계속 화면에 표시되어야 하므로 위와 같은 함수가 필요합니다.

위치정보 요청을 위해 LocationRequest.create()로 초기화 해줍니다.
PRIORITY_HIGH_ACCURACY는 가장 정확한 위치정보를 반환하게 합니다. priority에 위치 정보를 넣어주고, interval을 1000으로 지정하여 1초마다 갱신되게 합니다.

locationCallbackLocationCallback형태의 object입니다.

1초마다 갱신되는 정보가 onLocationResult라는 오버라이드한 함수로 전달될 것입니다.

파라미터로 받는 locationResult에 대해 루프를 돌면서 위도와 경도를 로그로 표시해줍니다.

fusedLocationClient는 클라이언트의 위치정보를 requestLocationUpdates로 업데이트 해줍니다.

전달받는 인자는 요청받는 위치정보와 콜백, 그리고 반복가능한 객체가 들어갑니다.

위치를 설정하는 함수

updateLocation 함수를 통해 위치정보를 갱신하였다면 이제 setLastLocation이라는 함수로 위치 정보를 지도에 fix해 줄 것입니다.

fun setLastLocation(lastLocation:Location) {
    val latlng = LatLng(lastLocation.latitude, lastLocation.longitude)

    // 위도와 경도에 대한 옵션
    val makerOptions = MarkerOptions().position(latlng).title("I'm Here!")

    // 카메라 옵션
    val cameraPosition = CameraPosition.Builder().target(latlng).zoom(15.0f).build()

    mMap.clear()
    mMap.addMarker(makerOptions)
    mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))

}

LatLng를 통해 위도와 경도를 받아 저장합니다.
MarkerOptions을 통해 수집된 위치에 핀 포인트를 꽂아줄 것입니다. 핀을 클릭하면 "I'm Here!"라는 메시지를 보여줄 것입니다.

이 때 카메라 옵션을 지정해주는데 이는 카메라가 가리키는 방향을 북쪽에서 시계방향으로 설정해줍니다.

초기화, 마커추가 및 카메라 움직임에 따른 지도 표현을 활성화 해줍니다.

이 함수를 updateLocation 함수 내에 있는 onLocationResult에 사용합니다.

@SuppressLint("MissingPermission")
fun updateLocation() {
    val locationRequest = LocationRequest.create()
    locationRequest.run {
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        interval = 1000
    }

    locationCallback = object : LocationCallback() {
        // 1초에 한번씩 변경되는 위치정보가 onLocationResult로 전달됨
        override fun onLocationResult(locationResult: LocationResult?) {
            locationResult?.let {
                for(location in it.locations) {
                    Log.d("위치정보 - 위도", "${location.latitude}")
                    Log.d("위치정보 - 경도", "${location.longitude}")
                    setLastLocation(location)
                }
            }
        }
    }

    // 권한 처리
    fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())

}

onMapReady 함수 구성하기

이 함수는 오버라이드 된 함수이며 다음과 같이 구성합니다.

override fun onMapReady(googleMap: GoogleMap) {
    mMap = googleMap

    fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
    updateLocation()
}

(부록) 본인이 설정한 위도, 경도로 적용하는 경우

// 본인이 설정한 위도, 경도로 적용하는 경우
fun setLocation(latitude:Double, longitude:Double) {
    val latlng = LatLng(latitude, longitude)

    // 위도와 경도에 대한 옵션
    val makerOptions = MarkerOptions().position(latlng).title("I'm Here!")

    // 카메라 옵션
    val cameraPosition = CameraPosition.Builder().target(latlng).zoom(15.0f).build()

    mMap.clear()
    mMap.addMarker(makerOptions)
    mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
}

위도와 경도를 함수의 매개변수로 받습니다.

onCreate 구성

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = ActivityMapsBinding.inflate(layoutInflater)
    setContentView(binding.root)

    locationPermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
        if(results.all { it.value }) {
            startProcess()
        } else {    // 문제 발생 시
            Toast.makeText(this, "권한 승인이 필요합니다.", Toast.LENGTH_LONG).show()
        }
    }

    // 권한 요청
    locationPermission.launch(
        arrayOf(
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_FINE_LOCATION
        )
    )

}

locationPermissionActivityResultLauncher<Array<String>>로 선언된 배열 형태입니다.

안드로이드 개발자 문서를 보면

결과를 얻기 위해 활동을 시작할 때, 메모리 부족으로 인해 프로세스와 활동이 소멸될 수 있습니다(특히, 카메라 사용과 같이 메모리를 많이 사용하는 작업의 경우 소멸 확률이 매우 높음).
따라서, Activity Result API는 다른 활동을 실행하는 코드 위치에서 결과 콜백을 분리합니다. 결과 콜백은 프로세스와 활동을 다시 생성할 때 사용할 수 있어야 하므로 다른 활동을 실행하는 로직이 사용자 입력 또는 기타 비즈니스 로직을 기반으로만 발생하더라도 활동이 생성될 때마다 콜백을 무조건 등록해야 합니다.

라고 되어있으며 추가적으로

ComponentActivity 또는 Fragment에 있을 때, Activity Result API에서 제공하는 registerForActivityResult() API를 통해 결과 콜백을 등록할 수 있습니다. registerForActivityResult()ActivityResultContractActivityResultCallback을 가져와서 다른 활동을 실행하는 데 사용할 ActivityResultLauncher를 반환합니다.

ActivityResultContracts.RequestMultiplePermissions()는 런타임 요청을 요청하는 역할을 하며 사용자의 응답을 처리하는 콜백입니다.

권한 획득에 성공하였을 경우 startProcess 메서드를 호출하여 실행하고, 문제가 발생했을 때 토스트를 띄워줍니다.

아래 locationPermission.launch는 권한을 호출하는 함수입니다.

런타임 시 위치 정보 액세스 권한 요청 시 사용자 선택이 권한 부여에 영향을 미치는데, 아래 표는 사용자가 권한 런타임 대화상자에서 선택하는 옵션에 따라 시스템이 앱에 부여하는 권한을 보여줍니다.

정확한 위치 대략적인 위치
앱 사용 중에만 허용 ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_COARSE_LOCATION
이번만 허용 ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_COARSE_LOCATION
거부 위치 정보 액세스 권한 없음 위치 정보 액세스 권한 없음

'Study > Android' 카테고리의 다른 글

[Android] 블루투스 개념  (2) 2022.07.17
[Android] Android 12에서 강화된 블루투스 퍼미션  (0) 2022.05.20
Android : Camera  (0) 2022.02.16
Android : SQLite  (0) 2022.02.16
Android : 장면 전환 시 데이터 주고 받기  (0) 2022.02.16