[Android] 12(1). 구글 지도

7 minute read


구글 지도

구글 플레이 서비스의 Google Maps API를 사용하면 구글 지도 데이터를 기반으로 앱에 지도를 추가할 수 있습니다.

구글 지도는 Google Maps Platform 서비스 중 하나이며 교통 정보 기반의 경로 찾기와 장소 정보, 검색 등의 기능을 제공합니다.

이번 포스팅에서는 내 앱에 구글 지도를 추가하고 스마트폰의 위치를 검색해서 현재 위치를 마커로 표시하는 방법에 대해 알아봅니다.

image-20211109111628813


구글 지도 시작하기

안드로이드 스튜디오는 구글 지도를 쉽게 사용할 수 있도록 프로젝트 생성 시 프로젝트의 종류를 선택하는 메뉴에서 Google Maps Activity를 제공합니다.


구글 플레이 서비스 SDK 설치하기

Google Maps API를 사용하려면 구글 플레이 서비스 SDK를 설치해야 합니다.

[Welcome to Android Studio] 화면에서 하단의 [Configure] - [SDK Manager]를 클릭합니다. 안드로이드 스튜디오가 켜져 있는 상태라면 상단 메뉴의 [Tools] - [SDK Manager]를 클릭합니다.

다음 그림처럼 [SDK Tools] 탭에서 Google Play services를 설치합니다.

image-20211109131613200


Google Maps Activity로 시작하기

액티비티 선택 창에서 Google Maps Activity를 선택하고 프로젝트를 생성합니다.

image-20211109131822709


Google Maps API 키 받기

구글 지도를 포함한 구글 플레이 서비스에 액세스하려면 구글 플레이 서비스의 API 키가 필요합니다.

[Google Maps Activity]로 프로젝트를 생성하면 API 키가 있는 google_maps_api.xml 파일이 자동으로 생성됩니다.


1. 구글 API 키를 요청할 수 있는 주소로 이동

[app] - [res] - [values] 디렉터리에 있는 google_maps_api.xml 파일에서 https:// 로 시작하는 첫번째 URL을 복사해 웹 브라우저의 주소창에 붙여넣은 다음 이동합니다.

로그인을 요청 화면이 보이면 구글 계정으로 로그인합니다.


2. API 키 만들기

주소로 이동한 뒤 로그인까지 마치면 다음과 같이 Google Cloud Platform 콘솔 페이지가 열렸을 것입니다.

애플리케이션 등록 화면에서 [프로젝트 만들기]를 선택하고 [계속]을 클릭합니다.

image-20211109132330240

그리고 다음 화면으로 이동하면 [API 키 만들기]를 클릭합니다.

image-20211109132400866


3. 에뮬레이터에서 사용하기 위한 설정 변경

정상적으로 진행되었다면 사용자 인증 정보 화면의 API 키 목록에 생성된 API키가 보입니다.

목록 우측 끝에 보이는 연필 모양을 클릭해 수정 화면으로 들어간 뒤, [애플리케이션 제한사항]을 [없음]으로 변경한 다음 [저장]합니다.

image-20211109132536604


4. API 키 사용하기

이제 생성된 API 키를 복사하고, google_maps_api.xml 파일의 <string name="google_maps_key"> 요소의 YOUR_KEY_HERE이라고 적힌 부분 대신에 붙여넣습니다.

<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">AIzaSyA4OJ02IFMzzZtSw5pswhQWOz7AmKH5A48</string>


5. 앱을 실행하고 확인하기

앱을 빌드하고 시작하면 시드니에 마커가 표시된 지도를 표시합니다.



구글 지도 코드 살펴보기

구글 지도를 간단하게 사용하려면 먼저 SupportNapFragment에 대해 알고 있어야 합니다.


activity_maps.xml의 SupportMapFragment

구글 맵 액티비티로 프로젝트를 생성하면 [app] - [res] - [layout] 에 activity_maps.xml 파일이 자동 생성됩니다.

android:name"com.google.android.gms.maps.SupportMapFragment"가 설정되어 있습니다. Google Maps API는 SupportlMapFragment에 구글 지도를 표시합니다.

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MapsActivity" />


MapsActivity.kt의 SupportMapFragment.getMapAsync

MapsActivity.kt 파일을 열면 onCreate( ) 메서드 안에서는 SupportFragmentManager의 findFragmentById( ) 메서드로 id가 map인 SupportMapFragment를 찾은 후, getMapAsync( )를 호출해서 안드로이드에 구글 지도를 그려달라는 요청을 합니다.

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

        binding = ActivityMapsBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // id가 map인 SupportMapFragment를 찾은 후 getMapAsync()를 호출해서
        // 안드로이드에 구글 지도를 그려달라는 요청
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }


MapsActivity.kt의 OnMapReadyCallback

안드로이드는 구글 지도가 준비되면 OnMapReadyCallback 인터페이스의 onMapReady( ) 메서드를 호출하면서 파라미터로 준비된 GoogleMap을 전달해줍니다.

메서드 안에서 미리 선언된 mMap 프로퍼티에 GoogleMap을 저장해두면 액티비티 전체에서 맵을 사용할 수 있습니다.

    // 구글 지도가 준비되면 안드로이드가 OnMapReadyCallback 인터페이스의 onMapReady() 메서드를
    // 호출하면서 파라미터로 준비된 GoogleMap을 전달
    override fun onMapReady(googleMap: GoogleMap) {
        // 메서드 안에서 미리 선언된 mMap 프로퍼티에 GoogleMap을 저장해두면 액티비티 전체에서 맵을 사용할 수 있음
        mMap = googleMap
        
        ...
    }

image-20211110092314553



카메라와 지도 뷰

구글 지도에서는 카메라를 통해 현재 화면의 지도 뷰를 변경할 수 있습니다.

카메라의 위치는 CameraPosition 클래스에 각종 옵션을 사용해서 조절할 수 있습니다.

CameraPosition.Builder().옵션1.옵션2.build()

옵션의 종류를 알아보겠습니다.


Target

카메라의 목표 지점은 지도 중심의 위치이며 위도 및 경도로 표시됩니다.

CameraPosition.Builder.target(LatLng(-34.0, 151.0))

Zoom

카메라의 줌(확대/축소) 레벨에 따라 지도의 배율이 결정됩니다. 줌 레벨이 높을수록 더 가깝게 볼 수 있습니다.

CameraPosition.Builder().zoom(15.5f)

줌 레벨이 0인 지도의 배율은 전 세계의 너비가 약 256dp가 되며 레벨 범위는 다음과 같습니다.

레벨 설명
1.0 세계
5.0 대륙
10.0 도시
15.0 거리
20.0 건물

Bearing

카메라의 베어링은 지도의 수직선이 불쪽을 기준으로 시계 방향 단위로 측정되는 방향입니다.

자동차를 운전하는 사람은 지도를 돌려가며 여행 방향에 맞추고 지도와 나침반을 사용하는 등산객은 지도의 수직선이 북쪽을 향하도록 지도의 방향을 정합니다.

CameraPosition.Builder().bearing(300f)

Tilt

카메라의 기울기는 지도의 중앙 위치와 지구 표현 사이의 원호에서 카메라 위치를 지정합니다.

기울기로 시야각을 변경하면 멀리 떨어진 지형이 더 작게 나타나고 주변 지형이 더 커져 맵이 원근으로 나타납니다.

CameraPosition.Builder().tilt(50f)



소스코드에서 카메라 이동하기

앞에서 설명한 옵션을 이용해서 CameraPosition 객체를 생성하고 moveCamera() 메서드로 카메라의 위치를 이동시켜 지도를 변경할 수 있습니다.

MapsActivity.kt 파일의 onMapReady( ) 메서드 안에 작성합니다.


1. CameraPosition 객체로 카메라 포지션 설정

val LATLNG = LatLng(-34.0, 151.0)
val cameraPosition = CameraPosition.Builder()
.target(LATLNG)
.zoom(15.0f)
.build()

2. CameraUpdateFactory.newCameraPosition에 전달하여 지도에서 사용 가능한 카메라 정보 생성

val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)

3. 카메라 정보를 GoogleMap의 moveCamera( ) 메서드에 전달

mMap.moveCamera(cameraUpdate)



마커

마커는 지도에 위치를 표시합니다. 마커는 아이콘의 색상, 이미지, 위치를 변경할 수 있으며 대화식으로 설계되었기 때문에 마커를 클릭하면 정보 창을 띄우거나 클릭 리스터처럼 클릭에 대한 코드 처리를 할 수 있습니다.


마커 표시하기

특정 지역의 좌표에 마커를 다음과 같은 순서로 추가하고 사용할 수 있습니다.

MapsActivity.kt 파일의 onMapReady( ) 메서드 안에 작성합니다.

1. mMap = GoogleMap 코드 아래에 LatLng 객체 생성

// 위치(위도, 경도) 정보
val LATLNG = LatLng(37.566418, 126.977943)

2. MarkerOptions( ) 객체 생성

// 마커 옵션 객체
val markerOptions = MarkerOptions()
    .position(LATLNG)
    .title("Marker in Seoul City Hall")
    .snippet("37.566418, 126.977943")

3. GoogleMap 객체의 addMarker( ) 메서드에 전달

// 구글 지도에 마커 추가
mMap.addMarker(markerOptions)

4. 카메라를 마커의 좌표로 이동

val cameraPosition = CameraPosition.Builder()
    .target(LATLNG)
    .zoom(15.0f)
    .build()

val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)
mMap.moveCamera(cameraUpdate)

5. 마커 수정해보기

// 마커 옵션 객체
val markerOptions = MarkerOptions()
    .position(LATLNG)
    .title("Marker in Seoul City Hall")
    .snippet("37.566418, 126.977943")
// 구글 지도에 마커 추가
mMap.addMarker(markerOptions)


마커 아이콘 변경하기

마커 아이콘을 비트맵 이미지로 변경할 수 있습니다.

PNG 이미지 파일을 프로젝트에 추가하고 비트맵으로 변환해서 아이콘을 변경하는 방법을 다음과 같습니다.


1. drawable 디렉터리에 PNG 이미지 파일 추가

해당 내용은 4(2). 화면에 그려지는 디자인 요소 위젯 - 1 포스팅에서 이미지버튼 부분의 내용을 확인해주세요.

2. BitmapDrawable 객체 생성

롤리팝 버전 이전과 이후에서 동작하는 코드가 다르므로 버전 처리 코드를 추가해야 합니다.

var bitmapDrawable: BitmapDrawable

if(Build.VERSION_SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
    bitmapDrawable = getDrawable(R.drawable.marker) as BitmapDrawable // marker.png
}else{
    bitmapDrawable = resources.getDrawable(R.drawable.marker) as BitmapDrawable
}

3. BitmapDescriptor 객체 생성

var discriptor = BitmapDescriptorFactory.fromBitmap(bitmapDrawable.bitmap)

4. MarkerOptions 객체의 icon( ) 메서드에 전달하여 수정

val markerOptions = MarkerOptions()
    .position(LATLNG)
    .icon(discriptor)

mMap.addMarker(markerOptions)


아이콘의 크기

아이콘의 크기가 클 경우 Bitmap.createScaledBitmap() 메서드를 호출해서 크기를 줄인 비트맵 객체를 반환받아야 합니다.

var scaledBitmap = Bitmap.createScaledBitmap(originBitmap, 50, 50, false)

파라미터

  • src: 원본 Bitmap 객체
  • dstWidth: 새로운 Bitmap의 가로
  • dstHeight: 새로운 Bitmap의 세로
  • filter: 원본 이미지의 pixel 형태를 조정해서 이미지가 선명해지도록 합니다. (bool)



현재 위치 검색하기

앱에서 스마트폰의 현재 위치를 검색하려면 위치 권한이 필요합니다.

안드로이드 플랫폼은 현재 위치를 검색하는 FusedLocationProviderClient API를 제공합니다.

FusedLocationProviderClient API는 GPS 신호 및 와이파이와 통신사 네트워크 위치를 결합해서 최소한의 배터리 사용량으로 빠르고 정확하게 위치를 검색합니다.


Google Play Service 의존성 추가하기

FusedLocationProviderClient API를 사용하기 위해서 build.gradle 파일에 구글 플레이 서비스의 Location 라이브러리 의존성을 추가합니다.

Location 라이브러리는 Maps 라이브러리와 버전이 같아야 합니다.

implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.android.gms:play-services-maps:17.0.0'


권한을 명세하고 요청/처리하기

1. AndroidManifest.xml 파일에 위치 권한 선언

<!-- 도시 블록 내에서의 정확한 위치 (네트워크 위치) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 정확한 위치 확보 (네트워크 위치 + GPS 위치) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

2. 권한을 요청하는 BaseActivity를 포함시키고 MapsActivity가 상속하도록 코드 수정

앞선 6(3). BaseActivity 설계하기 포스팅에서 권한을 요청하는 클래스인 BaseActivity.kt 코드를 작성했었습니다.

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

그리고 BaseActivity를 상속하면 2개의 추상 메서드를 구현해야 합니다.

    // 권한 승인 시 호출
    override fun permissionGranted(requestCode: Int) {
        startProcess() // 뒤에서 작성
    }
    // 권한 거부 시 호출
    override fun permissionDenied(requestCode: Int) {
        Toast.makeText(this,
                        "권한 승인이 필요합니다.",
                        Toast.LENGTH_LONG).show()
    }

TODO( ) 행만 삭제하고 빈 채로 둡니다.

3. 위치 권한 요청하기

onCreate( ) 메서드의 mapFragment… 로 시작하는 줄 아래에 앱에서 사용할 위치 권한을 변수에 저장하고, 권한을 요청하는 코드를 작성합니다.

requestCode에는 임의의 숫자 값을 전달합니다.

// 앱에서 사용할 위치 권한
val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,
                          Manifest.permission.ACCESS_FINE_LOCATION)
requirePermissions(permissions, 99)

4. 구글 지도를 준비하는 startProcess( ) 메서드 작성

onCreate( ) 메 작성되어 있는 val mapFragment…로 시작하는 세 줄을 잘라내기한 후 붙여넣으면 됩니다.

    // 구글 지도 준비 작업
    fun startProcess(){
        // id가 map인 SupportMapFragment를 찾은 후 getMapAsync()를 호출해서
        // 안드로이드에 구글 지도를 그려달라는 요청
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

이제 권한이 모두 승인되고 맵이 준비되면 onMapReady( ) 메서드가 정상적으로 호출됩니다.


현재 위치 검색하기

현재 위치를 검색하기 위해서 FusedLocationProviderClient를 생성하고 사용합니다.


1. FusedLocationClient와 LocationCallback 객체 선언

onCreate( ) 위에 OnMapReady( ) 위치를 처리하기 위한 변수 2개를 선언해둡니다.

FusedLocatoinClient는 위칫값을 사용하기 위해서 필요하고, LocationCallback은 위칫값 요청에 대한 갱신 정보를 받는 데 필요합니다.

private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback

2. OnMapReady( ) 안에서 위치 검색 클라이언트 생성

    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap
        // 위치 검색 클라이언트 생성
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        // 위치 갱신
        updateLocation() // 뒤에서 작성

3. 위치 갱신 메서드 updateLocation( ) 작성

// 위치 갱신 메서드
    @SuppressLint("MissingPermission")
    fun updateLocation(){
        // 위치 정보를 요청할 정확도와 주기를 설정
        val locationRequest = LocationRequest.create()
        locationRequest.run{
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
            interval = 1000
        }
        // 해당 주기마다 위치값을 반환받을 콜백 설정
        locationCallback = object: LocationCallback(){
            override fun onLocationResult(locationResult: LocationResult?){
                locationResult?.let{
                    for((i, location) in it.locations.withIndex()){
                        Log.d("Location", "$i ${location.latitude}, ${location.longitude}")
                        setLastLocation(location) // 뒤에서 작성
                    }
                }
            }
        }
        // 위치 검색 클라이언트가 위치 갱신 요청
        // 권한 처리가 필요한데 현재 코드에서는 확인 불가 -> 메서드 상단에 해당 코드를 체크하지 않아도 된다는 어노테이션 추가
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
    }

4. 위치 정보를 받아서 마커를 그리고 화면을 이동하는 메서드 setLastLocation( ) 작성

    // 위치 정보를 받아서 마커를 그리고 카메라을 이동
    fun setLastLocation(lastLocation: Location){
        val LATLNG = LatLng(lastLocation.latitude, lastLocation.longitude)
        val markerOptions = MarkerOptions()
            .position(LATLNG)
            .title("Here!")
        val cameraPosition = CameraPosition.Builder()
            .target(LATLNG)
            .zoom(15.0f)
            .build()
        mMap.clear() // 이전에 추가된 마커가 있으면 삭제
        mMap.addMarker(markerOptions)
        mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
    }

5. 에뮬레이터에서 위치 변경해보기

다음 과정을 통해 에뮬레이터에서 위치를 변경할 수 있습니다.

1) 에뮬레이터 좌측 메뉴 중 가장 아래에 있는 […] 선택 2) Location 선택 3) 지도에서 아무 곳이나 선택 4) 우측 하단의 [SET LOCATION] 버튼 클릭

image-20211110101854754



Categories:

Updated:

Leave a comment