iT邦幫忙

2023 iThome 鐵人賽

DAY 3
0
Mobile Development

Google Maps SDK for Android 與 GIS App 開發筆記系列 第 3

Day 3: Android 中常與 GIS 服務結合的功能:GPS 定位

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20230917/20160271WLCcq23gpQ.jpg
Photo by henry perks on Unsplash

簡介

GPS 定位能讓我們取得使用者所在位置,並根據該位置提供環域資料,在 GIS 服務當中也是常見的功能之一。

在 Android 裝置上,要取得裝置的定位,可以透過 LocationManager 或是 FusedLocationProviderClient 這兩個 API 來獲得。

  • LoctionManager: 屬於系統原生 API,可在定位請求時指定定位結果來源(GPS/NETWORK/FUSED/PASSIVE)。
  • FusedLocationProviderClient: 由 Google Play Service 所提供的 API,提供的定位結果屬於混合式 (Fused) 定位。根據官方文件的說法,相較於 LocationManager 可以更省電。此方法也是目前官方最推薦的做法,能滿足大部分 App 的定位需求。

實作

接下來我們將實作 FusedLocationProvider API ,取得定位結果的流程,基本上有這幾個步驟要做。

  1. 環境建置
  2. 取得權限 (靜態宣告與動態請求)
  3. 初始化定位服務
  4. 發出定位請求
  5. 透過回呼取得定位結果

🚨 註:背景定位 (Background Location) 因為系統權限的限制,在不同版本上實作上會比較複雜。因此,這次分享將只討論前景定位 (Foreground Location)。

環境建置

build.gradle (app) 中新增以下 dependency,最新版本請參考官方網站或是 Android Studio 的自動提示。

apply plugin: 'com.android.application'

// 略...

dependencies {
    implementation 'com.google.android.gms:play-services-location:21.0.1'
}

文件可以看這裡 設定 Google Play 服務

取得權限

靜態宣告

根據不同的定位需求,App 必須在 AndroidManifest.xml 檔案中,宣告此 App 所需要使用的定位權限。

  • Manifest.permission.ACCESS_COARSE_LOCATION: 概略位置
  • Manifest.permission.ACCESS_FINE_LOCATION: 精確位置
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

動態請求

因為定位權限屬於系統的危險權限,而危險權限在 App 是執行在 Android 6.0 (API 23) 以上的系統時,都必須動態向使用者請求權限允許,否則會出例外。

以下範例程式碼是基本款的請求實作。

import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED


private val permissionRequestLauncher =
        registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            val result = if (granted) {
                ""
            } else {
                "not"
            }
            Toast.makeText(this, "Permission is $result granted!", Toast.LENGTH_SHORT).show()
        }
        
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 略...

    if (ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) != PERMISSION_GRANTED
    ) {
        permissionRequestLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
    }
}

最佳實踐(Best practice)的流程,可以參考官方的這張圖。

https://ithelp.ithome.com.tw/upload/images/20230917/20160271CkLsnfAamI.png
圖片來源:官方文件

初始化定位、呼叫定位、取得定位結果

我自己的習慣會將定位請求封裝起來,方便呼叫以及未來測試的撰寫。
因此這部分就不特別拆分開來看。

FuesdLocationManager

package tw.dh46.ithome23.sample

import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.os.Looper
import com.google.android.gms.location.LocationListener
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority

/**
 *  Created by danielhuang on 2023/9/7
 */
interface FusedLocationManager {

    // 開始定位請求
    fun start()

    // 停止定位請求
    fun stop()

    // 加入定位回呼
    fun addCallback(callback: Callback)

    // 移除定位回呼
    fun removeCallback(callback: Callback)

    interface Callback {
        // 定位更新回呼
        fun onLocationUpdate(location: Location)
    }
}

class FusedLocationManagerImpl(private val context: Context) : FusedLocationManager {

    private val fusedLocationClient by lazy {
        LocationServices.getFusedLocationProviderClient(context)
    }

    private val callbackSet = mutableSetOf<FusedLocationManager.Callback>()

    private val locationListener = object : LocationListener {
        override fun onLocationChanged(location: Location) {
            callbackSet.forEach {
                it.onLocationUpdate(location)
            }
        }
    }

    private var isRunning = false

    private fun createLocationRequest() =
        LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000L)
            .build()

    @SuppressLint("MissingPermission")
    override fun start() {
        if (isRunning) {
            return
        }
        fusedLocationClient.requestLocationUpdates(
            createLocationRequest(),
            locationListener,
            Looper.getMainLooper()
        )
        isRunning = true
    }

    override fun stop() {
        isRunning = false
        fusedLocationClient.removeLocationUpdates(locationListener)
    }

    override fun addCallback(callback: FusedLocationManager.Callback) {
        callbackSet.add(callback)
    }

    override fun removeCallback(callback: FusedLocationManager.Callback) {
        callbackSet.remove(callback)
    }
}
package tw.dh46.ithome23.sample

import android.Manifest
import android.location.Location
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import tw.dh46.ithome23.sample.databinding.ActivityLocationBinding
import java.time.ZonedDateTime

class LocationActivity : AppCompatActivity(), View.OnClickListener, FusedLocationManager.Callback {

    private lateinit var binding: ActivityLocationBinding

    private val fusedLocationManager by lazy {
        FusedLocationManagerImpl(this)
    }

    private val permissionRequestLauncher =
        registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            val result = if (granted) {
                ""
            } else {
                "not"
            }
            Toast.makeText(this, "Permission is $result granted!", Toast.LENGTH_SHORT).show()
            fusedLocationManager.addCallback(this)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLocationBinding.inflate(layoutInflater)
        setContentView(binding.root)

        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PERMISSION_GRANTED
        ) {
            permissionRequestLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
        } else {
            fusedLocationManager.addCallback(this)
        }

        binding.btnStart.setOnClickListener(this)
    }

    override fun onClick(view: View?) {
        when (view) {
            binding.btnStart -> {
                fusedLocationManager.start()
            }
            binding.btnStop -> {
                fusedLocationManager.stop()
                binding.tvResult.text = "定位結果輸出"
            }
        }
    }

    override fun onLocationUpdate(location: Location) {
        binding.tvResult.text = "定位結果:  ${location.longitude} / ${location.latitude} \n ${ZonedDateTime.now()}"
    }
}

以上就可以實作出一個簡單的定位呼叫功能。


上一篇
Day 2: GIS 是什麼? GIS 在 App 上常見的應用方式與資料格式
下一篇
Day 4: Google Maps SDK for Android – 基本功能與費用介紹
系列文
Google Maps SDK for Android 與 GIS App 開發筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言