免费替代高德 / 百度!Android 原生定位 + GeoNames 离线方案:精准经纬度与模糊位置工具包

2,279 阅读12分钟

原生定位之定位获取分析<一>

原生定位之系统代码获取定位<二>

原生定位之获取经纬度以及获取位置信息<三>

原生定位之终极版本动态配置获取定位信息<四>

针对高德、百度等第三方定位服务收费的问题,本文介绍了一套免费 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_PROVIDER GPS定位
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,           // 超时 5singleUpdate = 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)

image.png

项目中是以module形式使用的 要是想用aar形式使用 自己去module中生成 按照项目中的形式 集成就可以了

5:注意

  • Geocoder由于Google服务问题 存在获取不到信息的情况
  • GeoNames作为兜底的存在,是一个模糊定位位置不那么精确

总结

该 Android 定位工具包通过整合 Android 原生能力与离线数据库,解决了第三方定位服务收费的痛点,具备免费无依赖、多场景适配、易于集成的优势。其核心价值在于:

  • 成本优势:完全基于免费技术方案,无调用次数或流量费用。

  • 灵活性:支持多种定位方式与回调机制,可根据项目需求选择精准度与耗电平衡的方案。

  • 稳定性:通过 “原生 + 离线” 双方案兜底,降低单一方式失效的风险,保障定位功能可用性。

项目已开源(定位信息处理),开发者可直接获取代码,根据需求调整数据库精度或扩展定位方式(如 Wi-Fi / 蓝牙室内定位)。