Android Google Maps 與定位服務實戰

在地圖應用開發中,整合 Google Maps 與精確的定位追蹤是許多 App 的核心功能。透過官方提供的 Maps Compose 函式庫,我們可以用宣告式的方式控制地圖,並結合 Fused Location Provider 實現穩定的定位。

準備工作

在開始之前,你需要前往 Google Cloud Console 啟用 Maps SDK for Android 並取得 API Key。

安裝依賴

build.gradle.kts 加入核心庫與 Compose 封裝庫:

dependencies {
    // Maps Compose 封裝庫
    implementation("com.google.maps.android:maps-compose:4.3.3")
    // Google Play Services Maps 核心
    implementation("com.google.android.gms:play-services-maps:18.2.0")
    // 定位服務核心
    implementation("com.google.android.gms:play-services-location:21.2.0")
}

Manifest 設定

AndroidManifest.xml 中加入 API Key 以及必要的權限:

<manifest ...>
    <!-- 精確定位與模糊定位 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <application ...>
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR_API_KEY_HERE" />
    </application>
</manifest>

定位權限處理 (Android 12+)

從 Android 12 開始,系統允許使用者選擇「精確 (Fine)」或「大約 (Coarse)」位置。開發者必須同時請求這兩個權限

如果你只請求 ACCESS_FINE_LOCATION 而沒有包含 ACCESS_COARSE_LOCATION,系統可能會忽略該請求或直接降級。

Maps Compose 基礎顯示

使用 GoogleMap Composable 元件,我們可以輕鬆控制相機視角與地圖選單。

@Composable
fun MapScreen() {
    val taipei101 = LatLng(25.0339, 121.5644)
    
    // 控制地圖相機狀態 (如縮放層級、中心點)
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(taipei101, 15f)
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        properties = MapProperties(
            isMyLocationEnabled = true, // 顯示藍色小圓點 (需權限)
            mapType = MapType.NORMAL // 地圖類型 (衛星、混合等)
        ),
        uiSettings = MapUiSettings(
            zoomControlsEnabled = true, // 顯示縮放按鈕
            myLocationButtonEnabled = true // 顯示「我的位置」按鈕
        )
    ) {
        // 在地圖上放置標記
        Marker(
            state = MarkerState(position = taipei101),
            title = "台北 101",
            snippet = "點擊可查看詳細資訊"
        )
    }
}

自定義標記與繪製

自定義 Marker 圖示

你可以使用 BitmapDescriptorFactory 來更改標記的外觀。

Marker(
    state = MarkerState(position = customPos),
    icon = BitmapDescriptorFactory.fromResource(R.drawable.custom_marker_icon),
    onClick = { 
        // 點擊事件處理
        true 
    }
)

繪製折線 (Polyline)

用於顯示移動軌跡或導航路徑。

Polyline(
    points = listOf(startNode, midNode, endNode),
    color = Color.Blue,
    width = 8f,
    jointType = JointType.ROUND // 轉角處設定為圓角
)

實戰:響應式定位追蹤 (Flow)

使用 FusedLocationProviderClient 並配合 Coroutines 的 callbackFlow,可以將定位更新轉換為響應式的數據流。

class LocationRepository(
    private val fusedLocationClient: FusedLocationProviderClient
) {
    @SuppressLint("MissingPermission")
    fun getLocationUpdates(interval: Long): Flow<Location> = callbackFlow {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, interval)
            .setMinUpdateDistanceMeters(1.0f) // 移動超過 1 公尺才更新
            .build()

        val callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let { trySend(it) }
            }
        }

        fusedLocationClient.requestLocationUpdates(request, callback, Looper.getMainLooper())
        
        // 當 Flow 被取消時,停止定位以節省電力
        awaitClose { fusedLocationClient.removeLocationUpdates(callback) }
    }
}

地理編碼與距離運算

地理編碼 (Geocoding)

將地址字串轉換為座標,或反之。

val geocoder = Geocoder(context, Locale.TAIWAN)
// 舊版 API 為同步操作,建議在 Dispatchers.IO 執行
val addresses = geocoder.getFromLocationName("台北市信義區五段7號", 1)
val latLng = addresses?.firstOrNull()?.let { LatLng(it.latitude, it.longitude) }

距離計算

不需透過 API,本地即可計算兩點間的直線距離。

val results = FloatArray(1)
Location.distanceBetween(
    startLat, startLng,
    endLat, endLng,
    results
)
val distanceInMeters = results[0] // 單位:公尺

結合這些技術,你就能建立出具備流暢互動地圖與真實精確導航功能的 Android 應用程式。