=== 지도와 GPS ===

 

- 기능 소개

구글 지도에 현재 위치를 표시하고 이동중인 자취를 실선으로 표시.

 

 

- 주로 사용하는 도구

Google Maps Activity (지도를 표시하는 기본 템플릿)

FusedLocationProviderClient (현재 위치 정보 클래스)

play-services-maps (구글 지도 라이브러리)

play-services-location (위치 정보 라이브러리)

 

 

1. 프로젝트 생성

(구글 지도를 사용하는 가장 쉬운 방법이 이렇게 기본 액티비티로 맵을 선택하는 것임)

 

프로젝트 명 : GPSMAP

기본 액티비티 선택 : Google Maps Activity

 

관련 라이브러리 추가

   play-services-maps (구글 지도 라이브러리) - 자동으로 추가됨

   play-services-location (위치 정보 라이브러리)

     (이전 예제에서 처럼 File 메뉴의 Project Structure... 로 검색하여 추가하면 더 편리)

 

Anko 라이브러리도 설정할 것

 

 

2. 구글지도 표시

 

1단계 코딩) 구글 지도 API 키 발급받기와 구글 지도 표시

자동으로 열려있는 google_maps_api.xml에 표시된 웹 링크로 들어가면 구글 API 콘솔화면에서

키를 발급받을 수 있습니다. 이전에 프로젝트를 만든 적이 있다면 기존 프로젝트를 선택하면 됩니다.

 

발급받은 키를 google_maps_api.xml "YOUR_KEY_HERE" 부분에 복사해 넣읍시다.

(발급받은 키는 분실하지 않도록 잘 보관할 것. 사용 횟수 제한도 있고 향후 구글지도는 유료화될 예정임)

[구글 지도 API 키 발급]

 

[구글 지도]

 

※ 자동으로 생성되어 있는 MapsActivity.kt 코드(커맨트 번역)

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

 

    private lateinit var mMap: GoogleMap

 

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_maps)

 

        // (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.

        val mapFragment = supportFragmentManager

            .findFragmentById(R.id.map) as SupportMapFragment

        mapFragment.getMapAsync(this)

    }

 

    /** (자동 생성된 코드)

     * 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)

     * 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역 변경 가능 (디폴트로 호주 시드니를 표시됨)

     * Google Play 서비스가 설치되어 있지 않으면

     * SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨

     * 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨

     */

    override fun onMapReady(googleMap: GoogleMap) {

        mMap = googleMap

 

        // 표시 지역을 시드니로 마킹하고 카메라를 이동시킴

        val sydney = LatLng(-34.0, 151.0)

        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))

        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))

    }

}

 

 

※ 자동으로 생성되어 있는 activity_maps.xml 디자인

   트리를 보면, name com.google.android.gms....인 특별한 프래그먼트가 하나 배치되어 있는데,

   play-services-maps 라이브러리에서 제공됩니다.

 

2단계 코딩) 주기적으로 현재 위치 정보 업데이트 하기

 

AndroidManifest.xml 에는 다음과 같은 자동으로 위치 권한이 추가 되어 있을 것입니다.

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

 

위치 권한은 위험 권한이므로 실행 중에 권한 요청을 해야 합니다.

(참고로 위치 서비스는 구글 플레이 서비스를 취신 버전으로 업데이트해야 연결됨)

 

// 주기적인 위치 정보 요청

requestLocationUpdates(locationRequest: LocationRequest, locationCallback: LocationCallback, looper: Looper)

   * locationRequiest (위치요청 객체)

   * locationCallback (위치가 갱신되면 호출되는 콜백)

   * looper (특정 루퍼 스레드 지정. 특별한 경우가 아니라면 null)

   ! 주의) 위치 정보를 주기적으로 요청하는 코드는 액티비티가 화면에 보일 때에만 수행되도록 할 것!

          , 아래 코드의 onResume()에서 정보를 요청하고, onPause()에서 요청을 삭제.

 

 

3단계 코딩) 소스 코드 참고

4단계 코딩) 소스 코드 참고

 

 

 

 

- 소스 코드

 

package com.tistory.gpsmap

import android.Manifest
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.gms.maps.model.PolylineOptions
import org.jetbrains.anko.alert
import org.jetbrains.anko.noButton
import org.jetbrains.anko.toast
import org.jetbrains.anko.yesButton

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap

    /* (1단계 코딩)
    *
    *    ===== 구글 지도 표시 =====
    *     (기본 코드는 자동 생성됨)
    *
    */
    // ① 위치정보를 주기적으로 받는데 필요한 객체들 선언
    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private lateinit var locationCallback: LocationCallback

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

        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)  // 화면 꺼지지 않게.
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT    // 세로 모드 고정.

        setContentView(R.layout.activity_maps)
        
        // (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

        // ② ①에서 선언한 객체들을 onCreate() 마지막에 초기화
        locationInit()
    }

    // ②
    private fun locationInit() {
        fusedLocationProviderClient = FusedLocationProviderClient(this)
        locationCallback = MyLocationCallBack()

        locationRequest = LocationRequest()   // LocationRequest객체로 위치 정보 요청 세부 설정을 함
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY   // GPS 우선
        locationRequest.interval = 10000   // 10초. 상황에 따라 다른 앱에서 더 빨리 위치 정보를 요청하면
                                          // 자동으로 더 짧아질 수도 있음
        locationRequest.fastestInterval = 5000  // 이보다 더 빈번히 업데이트 하지 않음 (고정된 최소 인터벌)
    }

    /** (자동 생성된 코드)
     * 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)
     * 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역을 변경할 수 있음 (디폴트로 호주 시드니를 표시됨)
     * Google Play 서비스가 설치되어 있지 않으면 SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨
     * 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        // 표시 지역을 시드니로 마킹하고 카메라를 이동시킴
        val sydney = LatLng(-34.0, 151.0)
        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
    }

    /* (2단계 코딩)
    *
    *  ===== 현재 위치 요청 하기 =====
    *
    */
    // ③
    private fun addLocationListener() {
        fusedLocationProviderClient.requestLocationUpdates(locationRequest,
            locationCallback,
            null)  // 혹시 안드로이드 스튜디오에서 비정상적으로 권한 요청 오류를 표시할 경우, 'Alt+Enter'로
                   // 표시되는 제안 중, Suppress: Add @SuppressLint("MissingPermission") annotation
                   // 을 클릭할 것
                   // (에디터가 원래 권한 요청이 필요한 코드 주변에서만 권한 요청 코딩을 허용했었기 때문임.
                   //  현재 우리 코딩처럼 이렇게 별도의 메소드에 권한 요청 코드를 작성하지 못하게 했었음)
    }

    // ④ MyActivity 클래스의 내부 클래스로 생성
    inner class MyLocationCallBack: LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            super.onLocationResult(locationResult)

            val location = locationResult?.lastLocation   // GPS가 꺼져 있을 경우 Location 객체가
                                                      // null이 될 수도 있음

            location?.run {
                val latLng = LatLng(latitude, longitude)   // 위도, 경도
                mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17f))  // 카메라 이동

                Log.d("MapsActivity", "위도: $latitude, 경도: $longitude")     // 로그 확인 용



                /* (4단계 코딩)
                *
                *   이동 경로 그리기 (여기에서는 구글맵에서 이동 자취 그리기용으로 지원해주는
                *                     편리한 메서드를 이용)
                *
                */
                mMap.addPolyline(polylineOptions)         // 선 그리기 (위치 정보가 갱신되면
                                                          // polyLineOptions 객체에 추가되고
                                                          // 지도에 polylineOptions 객체를 추가 함
            }
        }
    }


    /* (3단계 코딩)
    *
    *  ===== 실행 중 권한요청 =====
    *
    *  */
    private val REQUEST_ACCESS_FINE_LOCATION = 1000

    private fun permissionCheck(cancel: () -> Unit, ok: () -> Unit) {   // 전달인자도, 리턴값도 없는
                                                                   // 두 개의 함수를 받음

       if (ContextCompat.checkSelfPermission(this,                  // 권한이 없는 경우
          android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

          if (ActivityCompat.shouldShowRequestPermissionRationale(this,
             Manifest.permission.ACCESS_FINE_LOCATION)) {       // 권한 거부 이력이 있는 경우

             cancel()

          } else {
             ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                REQUEST_ACCESS_FINE_LOCATION)
          }
       } else {                                                    // 권한이 있는 경우
          ok()
       }
    }

    private  fun showPermissionInfoDialog() {
       alert("위치 정보를 얻으려면 위치 권한이 필요합니다", "권한이 필요한 이유") {
          yesButton {
             ActivityCompat.requestPermissions(this@MapsActivity,  // 첫 전달인자: Context 또는 Activity
                    // this: DialogInterface 객체
                    // this@MapsActivity는 액티비티를 명시적으로 가리킨 것임
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                REQUEST_ACCESS_FINE_LOCATION)
          }
          noButton {  }
       }.show()
    }

    // 권한 요청 결과 처리
    override fun onRequestPermissionsResult(
       requestCode: Int,
       permissions: Array,
       grantResults: IntArray
    ) {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults)

       when (requestCode) {
          REQUEST_ACCESS_FINE_LOCATION -> {
             if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                addLocationListener()
             } else {
                toast("권한이 거부 됨")
             }
          return
          }
       }
    }

    override fun onResume() {
       super.onResume()

       // 권한 요청
       permissionCheck(
          cancel = { showPermissionInfoDialog() },   // 권한 필요 안내창
          ok = { addLocationListener()}      // ③   주기적으로 현재 위치를 요청
       )
    }

    override fun onPause() {
       super.onPause()

       removeLocationListener()    // 앱이 동작하지 않을 때에는 위치 정보 요청 제거
    }

    private fun removeLocationListener() {
       fusedLocationProviderClient.removeLocationUpdates(locationCallback)
    }


    /* (4단계 코딩)
    *
    *   ===== 이동 경로 그리기 =====
    *   MyLocationCallBack 클래스
    */

    private val polylineOptions = PolylineOptions().width(5f).color(Color.RED)

}

 

※ 구글 맵에서 이동 자취 그리기를 위해 지원해주는 편리한 메서드들

   (만약 이들이 지원하지 않는 그래픽 기능이 필요하다면,

    수평 측정기 제작 때처럼 뷰에 그래픽 API를 이용해서 그려야 함)

 

addPolyLine()   - 여러 개의 경로 선을 그림

addCircle()     - 원 그리기

addPolygon()   - 영역 그리기

 

 

구글 지도를 활용하는 프로그램 소스에 특별한 내용은 없습니다.

이 예제는 지도를 그리고 현재 위치를 요청하고 그에 관한 권한을 요청하는 게 전부입니다.

 

지도를 표시하는 것과 이동 자취를 그리는 기능들을 전부 구글 맵용 라이브러리에서

지원하고 있으니 참 편합니다. 좀 더 세부적인 기능들을 구현하는 게 아니라면 제공되는

라이브러리를 쓰면 될 것 같습니다.

 

 

그럼, 이만~

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

+ Recent posts