位置与地图
本教程共 100 篇 · 第 92 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
92. 位置与地图
本节目标:学会用 FusedLocationProviderClient 获取位置、请求位置更新、处理位置权限,用 Google Maps SDK 显示地图、加标记、控制视角。
位置服务是高频需求:导航、外卖、天气、运动记录。Android 提供两套定位 API:老的 LocationManager 和现代的 FusedLocationProviderClient(Google Play 服务的一部分),后者更省电更准,本教程主线。
两种定位 API
| API | 来源 | 现在用不用 |
|---|---|---|
LocationManager | 系统内置 | 能用,但效率低,没 Google 服务时备用 |
FusedLocationProviderClient | Google 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))
}
系统会跳到设置页让用户选「始终允许」。
WarningGoogle 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
)
常见坑
- 没申请权限就调位置 API:崩或者返回 null。
lastLocation是 null:设备没缓存,用getCurrentLocation。requestLocationUpdates不停:耗电严重。onPause必须removeLocationUpdates。- 后台定位权限和前台一起请求:Android 10+ 不让,要单独请求。
- Maps API Key 没配:地图显示灰屏。
- Maps API Key 配错包名/签名:地图不显示,检查 Cloud Console 配置。
- 目标 SDK 31+ 后台定位:要在前台才能请求后台权限。
小结
位置用 FusedLocationProviderClient,lastLocation 拿缓存位置,getCurrentLocation 拿新鲜位置,requestLocationUpdates 持续更新。权限分前台(COARSE/FINE)和后台(BACKGROUND),后台要单独请求。Google Maps 用 maps-compose 库,配置 API Key 后 GoogleMap 显示地图,Marker 加标记,cameraPositionState 控制视角。地理围栏用 GeofencingClient。
下一章讲蓝牙和 NFC。