首页 / Android 入门教程 / 位置与地图

Android 入门教程

位置与地图

本教程共 100 篇 · 第 92 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程位置FusedLocationProviderGoogle Maps定位

92. 位置与地图

本节目标:学会用 FusedLocationProviderClient 获取位置、请求位置更新、处理位置权限,用 Google Maps SDK 显示地图、加标记、控制视角。

位置服务是高频需求:导航、外卖、天气、运动记录。Android 提供两套定位 API:老的 LocationManager 和现代的 FusedLocationProviderClient(Google Play 服务的一部分),后者更省电更准,本教程主线。

两种定位 API

API来源现在用不用
LocationManager系统内置能用,但效率低,没 Google 服务时备用
FusedLocationProviderClientGoogle Play 服务推荐,省电,融合 GPS/Wi-Fi/基站

FusedLocationProviderClient 融合多种定位源,自动选最优策略,比单纯 GPS 省电很多。

加依赖

dependencies {
    implementation("com.google.android.gms:play-services-location:21.3.0")
    implementation("com.google.maps.android:maps-compose:4.4.1")  // Compose 地图
    implementation("com.google.android.gms:play-services-maps:19.0.0")
}

位置权限

定位权限比较特殊,分前台和后台:

<!-- 前台定位(应用可见时) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- 后台定位(应用在后台也能拿位置) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

两档精度:

  • ACCESS_COARSE_LOCATION:粗略(约 10 公里,基于 Wi-Fi/基站)。
  • ACCESS_FINE_LOCATION:精确(约 10 米,含 GPS)。

请求时要一起请求两个,让用户选精度。

请求权限

val launcher = rememberLauncherForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { result ->
    val fineGranted = result[Manifest.permission.ACCESS_FINE_LOCATION] == true
    val coarseGranted = result[Manifest.permission.ACCESS_COARSE_LOCATION] == true
    if (fineGranted || coarseGranted) {
        startLocationUpdates()
    }
}

Button(onClick = {
    launcher.launch(arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    ))
}) {
    Text("请求位置权限")
}

后台定位权限

Android 10+ 后台定位要单独申请,不能和前台一起弹:

// 先有前台权限才能请求后台
if (hasForegroundPermission()) {
    launcher.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION))
}

系统会跳到设置页让用户选「始终允许」。

Warning

Google Play 对后台定位审核严格,只在必要时申请。能用前台就用前台。

获取当前位置

val fusedClient = LocationServices.getFusedLocationProviderClient(context)

// 检查权限
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
    != PackageManager.PERMISSION_GRANTED) {
    // 没权限,先请求
    return
}

fusedClient.lastLocation.addOnSuccessListener { location: Location? ->
    if (location != null) {
        val lat = location.latitude
        val lng = location.longitude
        // 显示位置
    } else {
        // lastLocation 可能是 null(设备没记过位置),主动请求一次
        requestSingleUpdate()
    }
}

lastLocation 是上次缓存的位置,可能为 null 或者过时。要新鲜数据用 getCurrentLocation

fusedClient.getCurrentLocation(
    Priority.PRIORITY_HIGH_ACCURACY,
    null  // CancellationToken
).addOnSuccessListener { location ->
    // location 可能为 null(拿不到)
}

Priority 几档:

  • PRIORITY_HIGH_ACCURACY:高精度,用 GPS,耗电。
  • PRIORITY_BALANCED_POWER_ACCURACY:平衡,约 100 米。
  • PRIORITY_LOW_POWER:省电,约 10 公里。
  • PRIORITY_NO_POWER:不主动定位,其他应用拿到时才通知。

请求位置更新

持续拿位置(运动记录、导航):

val locationRequest = LocationRequest.Builder(
    Priority.PRIORITY_HIGH_ACCURACY,
    5000L  // 间隔 5 秒
)
    .setMinUpdateIntervalMillis(2000L)  // 最快 2 秒
    .setWaitForAccurateLocation(false)
    .build()

val locationCallback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
        result.lastLocation?.let { location ->
            // 每次位置更新
            val lat = location.latitude
            val lng = location.longitude
        }
    }
}

// 开始
fusedClient.requestLocationUpdates(
    locationRequest,
    locationCallback,
    Looper.getMainLooper()
)

// 停止
fusedClient.removeLocationUpdates(locationCallback)

记得在 onPause 或者 Composable 离开时 removeLocationUpdates,省电。

Compose 里封装

@Composable
fun locationFlow(context: Context): Flow<Location> = remember {
    callbackFlow {
        val client = LocationServices.getFusedLocationProviderClient(context)
        val request = LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY, 5000L
        ).build()

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

        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
            == PackageManager.PERMISSION_GRANTED) {
            client.requestLocationUpdates(request, callback, Looper.getMainLooper())
        }

        awaitClose {
            client.removeLocationUpdates(callback)
        }
    }
}

地理围栏

到达特定地点触发通知,不用一直跟踪位置:

val geofencingClient = LocationServices.getGeofencingClient(context)

val geofence = Geofence.Builder()
    .setRequestId("cafe_001")
    .setCircularRegion(39.9, 116.4, 200f)  // 半径 200 米
    .setExpirationDuration(Geofence.NEVER_EXPIRE)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
    .build()

val request = GeofencingRequest.Builder()
    .addGeofence(geofence)
    .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
    .build()

geofencingClient.addGeofences(request, pendingIntent)

收到 Geofence 触发在 PendingIntent 指向的 BroadcastReceiver 处理。

Tip

导航应用用前台服务持续定位,外卖到店提醒用 Geofencing,比持续跟踪省电。

Google Maps 显示地图

配置 API Key

Google Cloud Console 创建项目,启用 Maps SDK for Android,拿到 API Key。

AndroidManifest.xml 加:

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

Compose 里显示地图

@Composable
fun MapScreen() {
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(
            LatLng(39.9042, 116.4074),  // 北京
            10f
        )
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState
    )
}

加标记

GoogleMap(
    modifier = Modifier.fillMaxSize(),
    cameraPositionState = cameraPositionState
) {
    Marker(
        state = rememberMarkerState(position = LatLng(39.9042, 116.4074)),
        title = "北京",
        snippet = "中国首都"
    )
}

移动视角

LaunchedEffect(Unit) {
    cameraPositionState.animate(
        update = CameraUpdateFactory.newLatLngZoom(
            LatLng(31.2304, 121.4737),  // 上海
            12f
        ),
        durationMs = 2000
    )
}

自定义标记图标

Marker(
    state = rememberMarkerState(position = latLng),
    title = "我的位置",
    icon = BitmapDescriptorFactory.fromResource(R.drawable.custom_marker)
)

绘制路线

Polyline(
    points = listOf(
        LatLng(39.9042, 116.4074),
        LatLng(39.92, 116.45),
        LatLng(39.95, 116.50)
    ),
    color = Color.Blue,
    width = 8f
)

常见坑

  1. 没申请权限就调位置 API:崩或者返回 null。
  2. lastLocation 是 null:设备没缓存,用 getCurrentLocation
  3. requestLocationUpdates 不停:耗电严重。onPause 必须 removeLocationUpdates
  4. 后台定位权限和前台一起请求:Android 10+ 不让,要单独请求。
  5. Maps API Key 没配:地图显示灰屏。
  6. Maps API Key 配错包名/签名:地图不显示,检查 Cloud Console 配置。
  7. 目标 SDK 31+ 后台定位:要在前台才能请求后台权限。

小结

位置用 FusedLocationProviderClientlastLocation 拿缓存位置,getCurrentLocation 拿新鲜位置,requestLocationUpdates 持续更新。权限分前台(COARSE/FINE)和后台(BACKGROUND),后台要单独请求。Google Maps 用 maps-compose 库,配置 API Key 后 GoogleMap 显示地图,Marker 加标记,cameraPositionState 控制视角。地理围栏用 GeofencingClient

下一章讲蓝牙和 NFC。