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 ,取得定位結果的流程,基本上有這幾個步驟要做。
🚨 註:背景定位 (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)的流程,可以參考官方的這張圖。
我自己的習慣會將定位請求封裝起來,方便呼叫以及未來測試的撰寫。
因此這部分就不特別拆分開來看。
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()}"
}
}
以上就可以實作出一個簡單的定位呼叫功能。