针对高德、百度等第三方定位服务收费的问题,本文介绍了一套免费 Android 定位工具包的开发与使用方案。该工具包通过整合 Android 原生 GPS、网络定位及缓存机制获取精准经纬度,结合 Geocoder 与 GeoNames 离线数据库实现模糊位置解析,支持单次 / 多次定位、广播 / 接口回调等功.,以 Module/AAR 形式集成,可满足项目中精准经纬度获取与基础位置信息查询的需求,避免第三方服务的费用依赖。
项目地址
需求
- 获取经纬度信息(
精准) - 获取位置信息(
模糊位置,不精准)
实现过程
GPS_PROVIDER/NETWORK_PROVIDER/Android缓存位置信息得到经纬度信息GeoNames获取模糊位置信息(越精准数据库越大,建议放在服务端)- 回调/广播返回数据信息
使用
// 回调
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} "
}
}
}
// 判断权限
hasRequest = isGranted(permissions)
locationSingleHelper = AndroidLocationManager(
context = this, // Activity 或 Context
timeout = 5000L, // 超时 5 秒
singleUpdate = true,// 单次定位
useBroadcast = true, callback = callback
)
// 获取定位
if (hasRequest) {
locationSingleHelper?.startLocation()
}
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_PROVIDERGPS定位
requestProvider(LocationManager.GPS_PROVIDER, time, distance)
NETWORK_PROVIDER网络定位
requestProvider(LocationManager.NETWORK_PROVIDER, time, distance)
PASSIVE_PROVIDER被动定位方式。这个意思也比較明显。就是用现成的,当其它应用使用定位更新了定位信息
requestProvider(LocationManager.PASSIVE_PROVIDER, time, distance)
- 缓存
/**
* 获取系统缓存的最后一次定位
*/
@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数据库
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 查询数据库
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 获取位置信息
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 创建 数据回调对象
// 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 判断是否拥有权限
<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 创建对象且获取经纬度
hasRequest = isGranted(permissions)
locationSingleHelper = AndroidLocationManager(
context = this, // Activity 或 Context
timeout = 5000L, // 超时 5 秒
singleUpdate = true ,// 单次定位
useBroadcast = true,
callback = callback
)
if (hasRequest){
locationSingleHelper?.startLocation()
}
3.4 经纬度获取详细位置信息
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 / 蓝牙室内定位)。