
针对高德、百度等第三方定位服务收费的问题,本文介绍了一套免费 Android 定位工具包的开发与使用方案。该工具包通过整合 Android 原生 GPS、网络定位及缓存机制获取精准经纬度,结合 Geocoder 与 GeoNames 离线数据库实现模糊位置解析,支持单次 / 多次定位、广播 / 接口回调等功能,以 Module/AAR 形式集成,可满足项目中精准经纬度获取与基础位置信息查询的需求,避免第三方服务的费用依赖。
项目地址
需求
- 获取经纬度信息(
精准
) - 获取位置信息(
模糊位置,不精准
)
实现过程
GPS_PROVIDER/NETWORK_PROVIDER/Android缓存位置信息
得到经纬度信息GeoNames
获取模糊位置信息(越精准数据库越大,建议放在服务端)- 回调/广播返回数据信息
1. 获取经纬度信息 (GPS/NET/缓存
)
实现获取经纬度数据的方式:
方式 | 精度 | 耗电 | 权限需求 | 国内/国外适用性 | 特点 |
---|---|---|---|---|---|
LocationManager GPS | 米级 | 高 | ACCESS_FINE_LOCATION | 全球 | 高精度户外定位,耗电高,获取慢,可单次或多次定位 |
LocationManager 网络 | 十米到百米 | 中低 | ACCESS_COARSE_LOCATION / ACCESS_FINE_LOCATION | 全球 | 省电,适合低精度场景,结合 Wi-Fi/基站定位 |
FusedLocationProvider (Google Play Services) | 米级 | 中 | ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION | 国外优,国内需兼容 | 自动融合 GPS/Wi-Fi/基站,省电高精度,需要 Google 服务 |
IP 地址定位 | 城市级(几公里) | 极低 | 无 | 全球(国内可用国内 API) | 完全免费,无需权限,精度低,仅可获取城市/省份级别 |
Wi-Fi / 蓝牙定位 | 米级(室内) | 中 | ACCESS_FINE_LOCATION / ACCESS_WIFI_STATE / BLUETOOTH |
Android提供了通过GPS,网络和缓存的位置信息等方式来获取经纬度信息,但是IP,卫星,wifi蓝牙等定位方式,实现起来相对麻烦这里就不做扩展了,有时间和有条件的朋友,实现过的朋友可以评论区聊一下
实现定位方式:
GPS_PROVIDER
scss
requestProvider(LocationManager.GPS_PROVIDER, time, distance)
NETWORK_PROVIDER
scss
requestProvider(LocationManager.NETWORK_PROVIDER, time, distance)
- 缓存
kotlin
/**
* 获取系统缓存的最后一次定位
*/
@SuppressLint("MissingPermission")
fun getLastKnownLocation(): Location? {
val providers = locationManager.getProviders(true)
var best: Location? = null
for (p in providers) {
val l = locationManager.getLastKnownLocation(p) ?: continue
if (best == null || l.accuracy < best.accuracy) best = l
}
return best
}
2.位置信息(Geocoder+GeoNames)
获取位置信息的方式:
方式 | 精度 | 免费额度 | 是否需 Key | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|---|
Android 原生 Geocoder | 街道级(部分设备可能不全) | 完全免费 | 否 | 无需第三方,低耗电 | 国内地址可能不完整,部分低端设备/模拟器支持不全 | 小型 App,低流量,低依赖 |
高德 Web API | 街道级 | 免费 5000 次/天 | 是 | 国内精准,街道/小区级 | 需申请 key,超过免费额度需付费 | 国内高精度需求,商业 App |
腾讯 Web API | 街道级 | 免费 10000 次/天 | 是 | 国内精准,免费额度高 | 需申请 key,超过免费额度需付费 | 国内高精度需求,流量较大 App |
GeoNames 离线数据库 | 城市/县级 | 完全免费 | 否(数据库文件直接使用) | 离线可用,无网络依赖,可自定义精度 | 精度较低(街道级不可用),数据库文件较大 | 国内/国外低精度离线定位,断网场景,教育或工具类 App |
由于,三方融合定位会是有费用的,所以直接排除.我选择了Geocoder+GeoNames的方式获取位置信息.
注意:
考虑的包大小,生成的GeoNames
数据库采取的是模糊定位,位置不精确.
2.1 Python生成GeoNames数据库
python
import sqlite3
import re
from collections import defaultdict
import os
# -------------------------------
# 文件路径
# -------------------------------
DB_FILE = "geonames_cn_optimized.db"
TXT_FILE = "CN.txt"
SYS_REGION_SQL = "sys_region.sql"
MERGE_FILES = ["TW.txt", "HK.txt", "MO.txt"]
# -------------------------------
# 配置参数
# -------------------------------
MAX_VILLAGES_PER_COUNTY = 100
MIN_POPULATION = 500
GRID_SIZE = 0.05
BATCH_SIZE = 1000
# -------------------------------
# -------------------------------
# 删除旧数据库
# -------------------------------
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
# -------------------------------
# 解析 sys_region.sql
# -------------------------------
region_map = {} # code -> 中文名称
parent_map = {} # code -> parent_code
level_map = {} # code -> level
with open(SYS_REGION_SQL, "r", encoding="utf-8") as f:
for line in f:
m = re.match(r"INSERT INTO `sys_region` VALUES ('(\d+)', '(\d*)', '(.*?)', (\d+), '.*?');", line)
if m:
code, parent, name, level = m.groups()
region_map[code] = name
parent_map[code] = parent if parent else None
level_map[code] = int(level)
# -------------------------------
# admin1 code → 中文省级名称
# -------------------------------
admin1_map = {
'00': '未知', '0': '未知',
'01': '北京市', '1': '北京市',
'02': '天津市', '2': '天津市',
'03': '台湾省', '3': '台湾省',
'04': '上海市', '4': '上海市',
'05': '重庆市', '5': '重庆市',
'06': '河北省', '6': '河北省',
'07': '山西省', '7': '山西省',
'08': '内蒙古自治区', '8': '内蒙古自治区',
'09': '辽宁省', '9': '辽宁省',
'0Z': '吉林省', 'Z': '吉林省',
'10': '黑龙江省',
'11': '江苏省',
'12': '浙江省',
'13': '安徽省',
'14': '西藏自治区',
'15': '福建省',
'16': '江西省',
'17': '山东省',
'18': '河南省',
'19': '湖北省',
'20': '湖南省',
'21': '广东省',
'22': '甘肃省',
'23': '广西壮族自治区',
'24': '海南省',
'25': '贵州省',
'26': '云南省',
'28': '宁夏回族自治区',
'29': '青海省',
'30': '陕西省',
'31': '甘肃省',
'32': '四川省',
'33': '西藏自治区',
'59': '重庆市',
'91': '香港特别行政区',
'92': '澳门特别行政区',
'93': '国外',
'99': '未知'
}
# -------------------------------
# 经纬度范围映射省级行政区(完整覆盖)
# -------------------------------
province_bbox = [
("北京市", 39.4, 41.0, 115.7, 117.4),
("天津市", 38.6, 40.2, 116.7, 118.1),
("上海市", 30.9, 31.8, 121.0, 122.0),
("重庆市", 28.0, 31.0, 105.0, 110.0),
("河北省", 36.0, 42.6, 113.0, 119.5),
("山西省", 35.5, 40.9, 110.0, 114.5),
("辽宁省", 38.4, 43.4, 118.0, 125.5),
("吉林省", 40.8, 46.3, 121.0, 131.2),
("黑龙江省", 43.3, 53.5, 121.0, 135.1),
("江苏省", 31.5, 35.0, 116.0, 121.0),
("浙江省", 27.0, 31.3, 118.0, 123.2),
("安徽省", 29.3, 34.7, 114.5, 119.5),
("福建省", 23.5, 28.5, 116.5, 120.5),
("江西省", 24.0, 30.0, 113.0, 118.5),
("山东省", 34.0, 38.5, 114.0, 122.0),
("河南省", 31.4, 36.5, 110.0, 116.8),
("湖北省", 29.0, 32.7, 108.9, 116.7),
("湖南省", 24.5, 30.1, 108.5, 114.2),
("广东省", 20.1, 25.3, 109.5, 117.2),
("广西壮族自治区", 20.5, 26.5, 104.5, 112.0),
("海南省", 18.0, 20.5, 108.5, 111.0),
("重庆市", 28.0, 31.0, 105.0, 110.0),
("四川省", 26.0, 34.5, 97.5, 108.0),
("贵州省", 24.5, 29.5, 103.4, 109.5),
("云南省", 21.0, 29.5, 97.5, 106.0),
("西藏自治区", 26.5, 36.0, 78.0, 99.5),
("陕西省", 31.2, 39.5, 105.0, 111.5),
("甘肃省", 32.0, 42.0, 92.0, 108.9),
("宁夏回族自治区", 35.0, 39.5, 104.0, 107.6),
("青海省", 31.0, 39.0, 89.0, 103.0),
("新疆维吾尔自治区", 34.0, 49.0, 73.0, 96.0),
("台湾省", 21.8, 25.3, 119.5, 122.0),
("香港特别行政区", 22.1, 22.6, 113.8, 114.3),
("澳门特别行政区", 22.1, 22.3, 113.5, 113.7),
]
# -------------------------------
# 工具函数
# -------------------------------
def extract_best_name(name: str, alternatenames: str) -> str:
if alternatenames:
names = alternatenames.split(',')
chinese = [n for n in names if re.search(r'[\u4e00-\u9fff]', n)]
if chinese:
return min(chinese, key=len)
return name
def get_grid_key(lat, lon):
return f"{int(lat / GRID_SIZE)}_{int(lon / GRID_SIZE)}"
def map_admin2(admin2_code):
if not admin2_code:
return None
level = level_map.get(admin2_code, 0)
if level == 4:
parent = parent_map.get(admin2_code)
if parent:
return region_map.get(parent, parent)
return region_map.get(admin2_code, admin2_code)
def correct_admin1_by_latlon(admin1, lat, lon):
for prov, lat_min, lat_max, lon_min, lon_max in province_bbox:
if lat_min <= lat <= lat_max and lon_min <= lon <= lon_max:
return prov
return admin1
# -------------------------------
# 打开 SQLite 并创建表
# -------------------------------
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE geonames (
geoname_id TEXT PRIMARY KEY,
name_cn TEXT,
admin1 TEXT,
admin2 TEXT,
lat REAL,
lon REAL
)
''')
conn.commit()
conn.execute("PRAGMA synchronous = OFF")
conn.execute("PRAGMA journal_mode = MEMORY")
batch = []
county_count = defaultdict(int)
grid_count = defaultdict(int)
seen_ids = set()
# -------------------------------
# 合并文件列表
# -------------------------------
all_files = [TXT_FILE] + MERGE_FILES
for file_path in all_files:
if not os.path.exists(file_path):
continue
with open(file_path, encoding='utf-8') as f:
for line in f:
parts = line.strip().split('\t')
if len(parts) < 19:
continue
geoname_id = parts[0].strip()
if geoname_id in seen_ids:
continue
seen_ids.add(geoname_id)
feature_class = parts[6]
feature_code = parts[7]
name = parts[1].strip()
alternatenames = parts[3].strip() if len(parts) > 3 else ""
name_cn = extract_best_name(name, alternatenames)
admin1_code = parts[10].strip() or None
admin1 = admin1_map.get(admin1_code, admin1_code)
admin2_code = parts[11].strip() or None
admin2 = map_admin2(admin2_code)
lat = float(parts[4])
lon = float(parts[5])
population = int(parts[14]) if parts[14].isdigit() else 0
# 经纬度修正
admin1 = correct_admin1_by_latlon(admin1, lat, lon)
keep = False
if feature_class == "A" and feature_code in ("ADM1", "ADM2", "ADM3"):
keep = True
elif feature_class == "P" and feature_code in ("PPLA", "PPLC"):
keep = True
elif feature_class == "P" and feature_code == "PPL":
county_key = f"{admin1}_{admin2}"
grid_key = get_grid_key(lat, lon)
if population >= MIN_POPULATION:
keep = True
county_count[county_key] += 1
grid_count[grid_key] += 1
elif county_count[county_key] < MAX_VILLAGES_PER_COUNTY:
keep = True
county_count[county_key] += 1
grid_count[grid_key] += 1
elif grid_count[grid_key] < 1:
keep = True
grid_count[grid_key] += 1
if feature_class == "P" and feature_code == "PPL" and not admin2:
continue
if not keep:
continue
batch.append((geoname_id, name_cn, admin1, admin2, lat, lon))
if len(batch) >= BATCH_SIZE:
cursor.executemany(
'INSERT INTO geonames (geoname_id, name_cn, admin1, admin2, lat, lon) VALUES (?, ?, ?, ?, ?, ?)',
batch
)
batch.clear()
# 插入剩余记录
if batch:
cursor.executemany(
'INSERT INTO geonames (geoname_id, name_cn, admin1, admin2, lat, lon) VALUES (?, ?, ?, ?, ?, ?)',
batch
)
# 压缩数据库
conn.commit()
conn.execute("VACUUM")
conn.close()
print("导入完成!admin1 已映射中文")
4.2 查询数据库
kotlin
package com.wkq.address
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import kotlin.io.copyTo
import kotlin.io.use
import kotlin.math.*
/**
* @Author: wkq
* @Time: 2025/8/29
* @Desc: 根据经纬度查询地名的数据库帮助类(Kotlin 层计算距离,兼容 Android SQLite)
*/
class GeoDbHelper(private val context: Context) : SQLiteOpenHelper(
context, DB_NAME, null, DB_VERSION
) {
/**
* 查询结果对象
*/
data class GeoResult(
val name: String,
val admin1: String?,
val admin2: String?,
val lat: Double,
val lon: Double,
val distanceKm: Double
) {
/** 格式化显示,例如:村镇 区 市 */
fun toDisplayString(): String = buildString {
append(name)
if (!admin2.isNullOrEmpty()) append(" $admin2")
if (!admin1.isNullOrEmpty()) append(" $admin1")
}
}
companion object {
private const val DB_NAME = "location.db"
private const val DB_VERSION = 1
private const val EARTH_RADIUS_KM = 6371
}
init {
copyDatabaseIfNeeded()
}
private fun copyDatabaseIfNeeded() {
val dbFile = context.getDatabasePath(DB_NAME)
if (dbFile.exists()) return
dbFile.parentFile?.mkdirs()
context.assets.open(DB_NAME).use { input ->
FileOutputStream(dbFile).use { output ->
input.copyTo(output)
}
}
}
override fun onCreate(db: SQLiteDatabase) {}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
// --- 单个地点 ---
/** 返回 String 格式(兼容旧接口) */
fun getNearestPlace(lat: Double, lon: Double, radius: Double = 0.05): String? {
return getNearestPlaceResult(lat, lon, radius)?.toDisplayString()
}
/** 返回完整 GeoResult 对象 */
fun getNearestPlaceResult(lat: Double, lon: Double, radius: Double = 0.05): GeoResult? {
return getNearbyPlacesInternal(lat, lon, radius, 1).firstOrNull()
}
/** 异步返回 GeoResult 对象 */
suspend fun getNearestPlaceResultAsync(
lat: Double, lon: Double, radius: Double = 0.05
): GeoResult? = withContext(Dispatchers.IO) {
getNearestPlaceResult(lat, lon, radius)
}
// --- 多个地点 ---
/** 返回 String 列表(兼容旧接口) */
fun getNearbyPlaces(
lat: Double, lon: Double, radius: Double = 0.05, limit: Int = 10
): List<String> {
return getNearbyPlacesResult(lat, lon, radius, limit)
.map { it.toDisplayString() }
}
/** 返回完整 GeoResult 列表 */
fun getNearbyPlacesResult(
lat: Double, lon: Double, radius: Double = 0.05, limit: Int = 10
): List<GeoResult> {
return getNearbyPlacesInternal(lat, lon, radius, limit)
}
/** 异步返回完整 GeoResult 列表 */
suspend fun getNearbyPlacesResultAsync(
lat: Double, lon: Double, radius: Double = 0.05, limit: Int = 10
): List<GeoResult> = withContext(Dispatchers.IO) {
getNearbyPlacesResult(lat, lon, radius, limit)
}
// --- 内部通用方法 ---
private fun getNearbyPlacesInternal(
lat: Double, lon: Double, radius: Double, limit: Int
): List<GeoResult> {
val db = readableDatabase
val latMin = lat - radius
val latMax = lat + radius
val lonMin = lon - radius
val lonMax = lon + radius
val sql = """
SELECT name_cn, admin1, admin2, lat, lon
FROM geonames
WHERE lat BETWEEN ? AND ?
AND lon BETWEEN ? AND ?
""".trimIndent()
val cursor: Cursor = db.rawQuery(
sql, arrayOf(latMin.toString(), latMax.toString(), lonMin.toString(), lonMax.toString())
)
val results = mutableListOf<GeoResult>()
while (cursor.moveToNext()) {
val name = cursor.getString(cursor.getColumnIndexOrThrow("name_cn"))
val admin1 = cursor.getString(cursor.getColumnIndexOrThrow("admin1"))
val admin2 = cursor.getString(cursor.getColumnIndexOrThrow("admin2"))
val latRes = cursor.getDouble(cursor.getColumnIndexOrThrow("lat"))
val lonRes = cursor.getDouble(cursor.getColumnIndexOrThrow("lon"))
val distance = haversine(lat, lon, latRes, lonRes)
results.add(GeoResult(name, admin1, admin2, latRes, lonRes, distance))
}
cursor.close()
return results.sortedBy { it.distanceKm }.take(limit)
}
// --- Kotlin 层 Haversine 公式 ---
private fun haversine(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = sin(dLat / 2).pow(2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLon / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return EARTH_RADIUS_KM * c
}
}
4.3 Geocoder+GeoNames 获取位置信息
kotlin
package com.wkq.location
import android.content.Context
import android.location.Address
import android.location.Geocoder
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.wkq.address.GeoDbHelper
import kotlinx.coroutines.*
import java.util.Locale
/**
* @Author: wkq
* @Date: 2025/09/02
* @Desc: 生命周期安全的地理位置解析工具(Geocoder 优先,数据库兜底)
* 支持:
* 1. 单地址查询
* 2. 附近位置列表查询
* 3. Kotlin 协程和 Java 回调
*/
object LocationGeocoderHelper {
/**
* 返回的数据结构
*/
data class LocationInfo(
val address: String?, // 详细地址
val city: String?, // 城市
val province: String?, // 省/州
val country: String?, // 国家
val latitude: Double,
val longitude: Double
)
/**
* Java 回调接口
*/
interface AddressCallback {
fun onAddressResult(result: LocationInfo?)
}
interface NearbyCallback {
fun onNearbyResult(results: List<LocationInfo>)
}
// ------------------- 单地址查询 -------------------
/**
* Kotlin 挂起函数方式
*/
suspend fun getAddress(
context: Context,
latitude: Double,
longitude: Double,
maxResults: Int = 1,
locale: Locale = Locale.getDefault()
): LocationInfo? = withContext(Dispatchers.IO) {
getAddressInternal(context, latitude, longitude, maxResults, locale)
}
/**
* Java / 生命周期安全方式
*/
@JvmStatic
fun getAddressAsync(
context: Context,
latitude: Double,
longitude: Double,
maxResults: Int = 1,
locale: Locale = Locale.getDefault(),
lifecycleOwner: LifecycleOwner? = null,
callback: AddressCallback
) {
if (latitude !in -90.0..90.0 || longitude !in -180.0..180.0) {
callback.onAddressResult(null)
return
}
val scope = lifecycleOwner?.lifecycleScope ?: CoroutineScope(Dispatchers.IO)
val job = scope.launch(Dispatchers.IO) {
val result = getAddressInternal(context, latitude, longitude, maxResults, locale)
withContext(Dispatchers.Main) {
if (lifecycleOwner == null ||
lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
) {
callback.onAddressResult(result)
}
}
}
lifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
job.cancel()
}
})
}
private fun getAddressInternal(
context: Context,
latitude: Double,
longitude: Double,
maxResults: Int,
locale: Locale
): LocationInfo? {
// 1. 系统 Geocoder
try {
if (Geocoder.isPresent()) {
val geocoder = Geocoder(context, locale)
val addresses: List<Address>? =
geocoder.getFromLocation(latitude, longitude, maxResults)
val address = addresses?.firstOrNull()
formatLocationInfo(address)
?.let { return it.copy(latitude = latitude, longitude = longitude) }
}
} catch (e: Exception) {
e.printStackTrace()
}
// 2. GeoDbHelper 数据库兜底
return try {
val geoDb = GeoDbHelper(context)
val nearest = geoDb.getNearestPlaceResult(latitude, longitude)
nearest?.let {
LocationInfo(
address = listOfNotNull(it.name, it.admin2, it.admin1).joinToString(" "),
city = it.admin2,
province = it.admin1,
country = "中国", // 国内 GeoNames 数据库默认中国
latitude = it.lat,
longitude = it.lon
)
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun formatLocationInfo(address: Address?): LocationInfo? {
if (address == null) return null
val fullAddress = address.getAddressLine(0)
?: listOfNotNull(address.locality, address.adminArea, address.countryName)
.joinToString(" ")
return LocationInfo(
address = fullAddress.takeIf { it.isNotBlank() },
city = address.locality,
province = address.adminArea,
country = address.countryName,
latitude = address.latitude,
longitude = address.longitude
)
}
// ------------------- 附近位置列表 -------------------
suspend fun getNearbyAddresses(
context: Context,
latitude: Double,
longitude: Double,
radiusKm: Double = 1.0,
maxResults: Int = 10
): List<LocationInfo> = withContext(Dispatchers.IO) {
val results = mutableListOf<LocationInfo>()
// Geocoder 查询附近 POI
try {
if (Geocoder.isPresent()) {
val geocoder = Geocoder(context)
val addresses = geocoder.getFromLocation(latitude, longitude, maxResults)
addresses?.forEach { addr ->
formatLocationInfo(addr)?.let { results.add(it) }
}
}
} catch (e: Exception) {
e.printStackTrace()
}
// 数据库兜底
if (results.isEmpty()) {
try {
val geoDb = GeoDbHelper(context)
val nearby = geoDb.getNearbyPlacesResult(latitude, longitude, radiusKm, maxResults)
nearby.forEach { geo ->
results.add(
LocationInfo(
address = listOfNotNull(geo.name, geo.admin2, geo.admin1).joinToString(" "),
city = geo.admin2,
province = geo.admin1,
country = "中国",
latitude = geo.lat,
longitude = geo.lon
)
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
results
}
@JvmStatic
fun getNearbyAddressesAsync(
context: Context,
latitude: Double,
longitude: Double,
radiusKm: Double = 1.0,
maxResults: Int = 10,
lifecycleOwner: LifecycleOwner? = null,
callback: NearbyCallback
) {
val scope = lifecycleOwner?.lifecycleScope ?: CoroutineScope(Dispatchers.IO)
val job = scope.launch(Dispatchers.IO) {
val results = getNearbyAddresses(context, latitude, longitude, radiusKm, maxResults)
withContext(Dispatchers.Main) {
if (lifecycleOwner == null ||
lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
) {
callback.onNearbyResult(results)
}
}
}
lifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
job.cancel()
}
})
}
}
3:获取经纬度和获取位置信息示例
3.1 创建 数据回调对象
kotlin
// 1:广播方式
var locationReceiver = LocationReceiver { loc ->
Log.d("广播定位", "lat=${loc?.latitude}, lon=${loc?.longitude}")
showToast("广播定位:${loc?.latitude}, ${loc?.longitude}")
}
// 2:监听接口方式
private val callback: (location: Location?) -> Unit = { location ->
location?.let {
lifecycleScope.launch {
val address = LocationGeocoderHelper.getAddress(this@LocationShowActivity, location.latitude, location.longitude)
val addressList = LocationGeocoderHelper.getNearbyAddresses(this@LocationShowActivity, location.latitude, location.longitude)
addressList.forEach { Log.d("定位状态", "附近位置: $it") }
binding.tvLocation.text = "类型: ${location?.provider} \n" + "纬度: ${location?.latitude}, 经度: ${location?.longitude}"
binding.tvAddress.text ="位置:${address?.address} \n 城市: ${address?.city} \n 省份: ${address?.province} \n 国家: ${address?.country} "
}
}
}
3.2 判断是否拥有权限
ini
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
3.3 创建对象且获取经纬度
ini
hasRequest = isGranted(permissions)
locationSingleHelper = AndroidLocationManager(
context = this, // Activity 或 Context
timeout = 5000L, // 超时 5 秒
singleUpdate = true ,// 单次定位
useBroadcast = true,
callback = callback
)
if (hasRequest){
locationSingleHelper?.startLocation()
}
3.4 经纬度获取详细位置信息
less
lifecycleScope.launch {
val address = LocationGeocoderHelper.getAddress(this@LocationShowActivity, location.latitude, location.longitude)
val addressList = LocationGeocoderHelper.getNearbyAddresses(this@LocationShowActivity, location.latitude, location.longitude)
addressList.forEach { Log.d("定位状态", "附近位置: $it") }
binding.tvLocation.text = "类型: ${location?.provider} \n" + "纬度: ${location?.latitude}, 经度: ${location?.longitude}"
binding.tvAddress.text ="位置:${address?.address} \n 城市: ${address?.city} \n 省份: ${address?.province} \n 国家: ${address?.country} "
}
4:使用方式 (module/aar)

项目中是以module形式使用的 要是想用aar形式使用 自己去module中生成 按照项目中的形式 集成就可以了
5:注意
Geocoder
由于Google服务问题 存在获取不到信息的情况GeoNames
作为兜底的存在,是一个模糊定位位置不那么精确
总结
该 Android 定位工具包通过整合 Android 原生能力与离线数据库,解决了第三方定位服务收费的痛点,具备免费无依赖、多场景适配、易于集成的优势。其核心价值在于:
-
成本优势:完全基于免费技术方案,无调用次数或流量费用。
-
灵活性:支持多种定位方式与回调机制,可根据项目需求选择精准度与耗电平衡的方案。
-
稳定性:通过 "原生 + 离线" 双方案兜底,降低单一方式失效的风险,保障定位功能可用性。
项目已开源(定位信息处理),开发者可直接获取代码,根据需求调整数据库精度或扩展定位方式(如 Wi-Fi / 蓝牙室内定位)。