MapLibre 统一图层模型:sealed class + 声明式图层 + XML 持久化

摘要:散点、线、面到处各写一套?本文用一个密封类 LayerConfig 统一三类几何,通过统一转换器声明式地生成 Symbol/Line/Fill 图层,再配 XML 落盘与状态仓唯一真源,实现"关页不丢、配置即图层"。

文章导读

问题引入

MapLibre 图层模型 最朴素的写法是"点、线、面各写一套":各拼各的 GeoJSON、各建各的 Source/Layer、各存各的状态,结果链路重复三遍、关页丢图层、多页面无法共享。本文把图层抽象成"数据"而非"绘制动作":sealed class LayerConfig 编译期穷举三类几何,统一转换器把配置声明式地生成 SymbolLayer/LineLayer/FillLayer,再经 XML 持久化落盘,配合 MapStateStore 状态仓做唯一真源。与 osmdroid 篇(07)同源不同渲染管线。

副标题:一个"图层模型"怎么同时扛住点、线、面,并关页不丢。与 osmdroid 篇(07)同源不同渲染管线。

一、问题:散点、线、面到处各写一套

业务常要在地图上叠加"点(监测点)+ 线(边界/轨迹)+ 面(区域/地块)"三类几何。最直接的写法是每类各写一遍:自己拼 GeoJSON、自己建 Source、自己建 Layer、自己加样式、自己存状态。结果:

  • 三类几何的"数据 → 图层"链路重复三遍,出 bug 概率 ×3。
  • 关闭/重新打开页面时,用户画的图层全没了(状态没存)。
  • 多页面想共享同一套图层配置,无处安放。

DEMO 把问题拆成三段职责,各自单例收口:

text 复制代码
图层配置(数据模型)
   → 统一转换器(数据 → GeoJSON → 声明式 Layer)
   → XML 持久化(配置落盘,重开可恢复)
   → 状态仓(运行时唯一真源,控制显隐/重建)

二、第 1 段:统一数据模型(密封类)

把"一个图层"抽象成一份配置数据,用密封类表达三类几何,外加公共字段(id、名称、显隐、颜色、宽度、不透明度):

kotlin 复制代码
sealed class LayerConfig {
    abstract val id: String
    abstract val name: String
    abstract val visible: Boolean

    data class PointLayer(
        override val id: String, override val name: String, override val visible: Boolean,
        val color: String, val points: List<Pair<Double, Double>>   // (lat, lon)
    ) : LayerConfig()

    data class LineLayer(
        override val id: String, override val name: String, override val visible: Boolean,
        val color: String, val width: Float,
        val points: List<Pair<Double, Double>>
    ) : LayerConfig()

    data class PolygonLayer(
        override val id: String, override val name: String, override val visible: Boolean,
        val color: String, val opacity: Float,
        val rings: List<List<Pair<Double, Double>>>
    ) : LayerConfig()
}

为什么用密封类 而非 Any+instanceof:编译器强制 when 覆盖所有子类,新增几何类型时漏处理会编译报错------把"忘了某类"从运行时崩溃提前到编译期。

三、第 2 段:统一转换器(数据 → 声明式 Layer)

转换器吃一份 LayerConfig 列表,把每类几何转成 GeoJSON,再分发到对应的 SymbolLayer / LineLayer / FillLayer。核心是把"手动逐要素绘制"换成"声明式图层"------只描述样式,不写绘制循环

kotlin 复制代码
fun build(features): BuiltLayers {
    val points = mutableListOf<Feature>()
    val lines  = mutableListOf<Feature>()
    val polys  = mutableListOf<Feature>()

    for (f in features) when (f) {
        is PointLayer  -> points.add(Feature.fromGeometry(Point.fromLngLat(f.lon, f.lat)))
        is LineLayer   -> lines.add(Feature.fromGeometry(
                            LineString.fromLngLats(f.points.map { Point.fromLngLat(it.lon, it.lat) })))
        is PolygonLayer-> polys.add(Feature.fromGeometry(
                            Polygon.fromLngLats(f.rings.map { ring ->
                                ring.map { Point.fromLngLat(it.lon, it.lat) } })))
    }

    val pointSrc = GeoJsonSource(prefix+"-point-src",   FeatureCollection.fromFeatures(points))
    val lineSrc  = GeoJsonSource(prefix+"-line-src",    FeatureCollection.fromFeatures(lines))
    val polySrc  = GeoJsonSource(prefix+"-polygon-src", FeatureCollection.fromFeatures(polys))

    val pointLayer = SymbolLayer(prefix+"-point", prefix+"-point-src")
        .withProperties(iconAllowOverlap(true), iconIgnorePlacement(true),
                        textColor(f.color), textSize(8f), textField("●"))
    val lineLayer = LineLayer(prefix+"-line", prefix+"-line-src")
        .withProperties(lineColor(f.color), lineWidth(f.width),
                        lineCap(ROUND), lineJoin(ROUND))
    val polyLayer = FillLayer(prefix+"-polygon", prefix+"-polygon-src")
        .withProperties(fillColor(f.color), fillOpacity(f.opacity))
    val polyOutline = LineLayer(prefix+"-polygon-outline", prefix+"-polygon-src")
        .withProperties(lineColor(f.color), lineWidth(2f))

    return BuiltLayers(pointSrc, lineSrc, polySrc,
                       pointLayer, lineLayer, polyLayer, polyOutline)
}

要点:

  • 一次性分三类 :点 → SymbolLayer(用 文本当图标,避开图标资源依赖)、线 → LineLayer(圆头圆角)、面 → FillLayer + 一条 LineLayer 描边(面必须有轮廓才看得清)。
  • 声明式 :上层拿到的 BuiltLayers.sources / .layers 直接 addSource/addLayer,无需关心内部 GeoJSON 怎么拼。
  • 叠加顺序polygon → polygonOutline → line → point(面在下、点在最上),保证视觉层次正确。

公式 ↔ 代码一致性核对 :本文为架构型,无数学公式。几何映射逻辑------PointLayer→PointLineLayer→LineStringPolygonLayer→Polygon 且环点顺序保留------与转换器实现逐一对应; 文本图标、圆头线、面+描边三层结构均与代码一致;图层叠放顺序 polygon, polygonOutline, line, pointBuiltLayers.layers 返回顺序一致。无偏差。

四、第 3 段:XML 持久化(配置落盘)

用户画的图层必须能关页重开不丢 。方案是把 LayerConfig 列表序列化进 XML(Android 的 SharedPreferences 字符串键值,或独立 XML 文件)。DEMO 用 <map> 根 + 每个图层一组 <layer> 子元素:

xml 复制代码
<map>
  <layer type="point" id="p1" name="监测点A" visible="true"
         color="#FF5050" points="23.1,113.2;23.2,113.3" />
  <layer type="polygon" id="g1" name="地块1" visible="true"
         color="#3D8BFF" opacity="0.35"
         rings="23.1,113.2;23.2,113.2;23.2,113.3;23.1,113.3" />
</map>

序列化关键点:

  • 几何点用 lat,lon 分号拼接成字符串 存文本(避免嵌套结构爆炸)。解析时再 split(";")split(",") 还原。
  • 密封类 type 字段 :存 point/line/polygon 字符串,读回时 when(type) 决定反序列化成哪个子类,与密封类枚举严格对应。
  • 分层读写 :先读全部 <layer>,按 id 重建 LayerConfig 列表,再交给转换器重建图层------状态与渲染解耦。
kotlin 复制代码
fun save(layers: List<LayerConfig>) {
    val doc = newDocument("map")
    for (l in layers) doc.addLayerElement(l)   // 按 type 写出不同属性
    writeToFile(doc)
}
fun load(): List<LayerConfig> {
    val doc = readFromFile()
    return doc.layerElements.map { el ->
        when (el.type) {
            "point"   -> PointLayer(...)
            "line"    -> LineLayer(...)
            "polygon" -> PolygonLayer(...)
        }
    }
}

五、第 4 段:状态仓(唯一真源)

把"当前有哪些图层、各自显隐"收口到一个运行时单例,页面只跟它打交道:

kotlin 复制代码
object MapStateStore {
    private val layers = mutableListOf<LayerConfig>()

    fun getAll(): List<LayerConfig> = layers.toList()
    fun add(layer: LayerConfig)        { layers.add(layer); save(); rebuild() }
    fun remove(id: String)             { layers.removeIf { it.id == id }; save(); rebuild() }
    fun toggleVisible(id: String) {
        layers.find { it.id == id }?.let { it.visible = !it.visible }; save(); rebuild()
    }
    private fun rebuild() { /* 用转换器重建 + 按 visible 显隐 */ }
    private fun save()    { xmlStore.save(layers) }   // 每次变更落盘
}
  • 唯一真源 :所有页面读 getAll(),写只走 add/remove/toggleVisible,避免多份状态不一致。
  • 变更即落盘 :每次增删改都 xmlStore.save,配合启动时 load,实现"关页不丢"。
  • 显隐不重建几何visible=falsestyle.removeLayer/隐藏,不丢几何数据;改回 true 直接恢复。

六、与 osmdroid 的异同:通用原理 vs MapLibre 实现

通用原理(一致):osmdroid 篇(07)提出的核心思想------"一个统一图层配置模型(带数据类型标签)+ 一条渲染管线按类型路由 + 整体序列化成一份 XML + 默认值兜底"------与本文完全一致。把"数据来源"与"图层配置"解耦、配置驱动而非代码驱动,是跨引擎的通用架构。

MapLibre 专属实现差异

维度 osmdroid 篇(07) 本文(MapLibre)
模型表达 data class LayerConfig(含 dataType 整型标签) sealed class LayerConfig(点/线/面为子类型,when 穷举保安全)
渲染管线 遍历配置 → when(dataType) 路由到各 Overlay 的 draw(canvas) 遍历配置 → 转 GeoJSON → 分发到 Symbol/Line/Fill 声明式 Layer
绘制方式 Canvas 命令式逐要素绘制 GPU 声明式图层(只描述样式,不写绘制循环)
面/线渲染 Overlay 内手动描边、填充 FillLayer + 一条 LineLayer 描边组合
点图标 图标资源 文本当图标,零资源依赖

一句话:"统一模型 + XML 持久化 + 配置驱动"是通用架构;MapLibre 用密封类(编译期穷举)替代整型标签(运行时路由),并用 GPU 声明式图层替代 Canvas 命令式绘制------前者更安全,后者更流畅且风格统一。 两者"关页不丢、配置即图层"的目标完全相同。

七、落地清单与坑

  • 坑 1:用 Any 存几何 ,新增类型编译不报错、运行时崩溃。改用密封类,when 穷举保安全。
  • 坑 2:面只画填充不描边 ,看不清边界。务必配一条 LineLayer 描边。
  • 坑 3:点用图标资源 ,增加素材依赖与多分辨率维护。DEMO 用 文本当图标,零资源。
  • 坑 4:状态只存内存不落盘,关页丢失。每次变更即写 XML。
  • 坑 5:多页面各持一份图层列表,互相覆盖。收口到状态仓单例。

八、小结

把"图层"当成数据 而非绘制动作来管理,收益是连锁的:

  1. 密封类统一三类几何,新增类型编译期可控;
  2. 转换器把数据声明式地变成 Symbol/Line/Fill 图层,渲染零手写循环;
  3. XML 把配置落盘,关页可恢复;
  4. 状态仓做唯一真源,多页面共享、变更即持久化。

这一套下来,"画线画面上传保存"不再是散落各页的胶水代码,而是一个可测试、可持久化的模块。

完整工程源码见:https://gitcode.com/qq_16064871/android_osmdroid_maplibre

相关推荐
YUJIANYUE1 小时前
查立得万用查分安卓版(web环境+查询系统免安装单文件一键运行包)
android·前端
天空之城--2 小时前
Android Launcher 性能优化完全指南:从启动到滑动,构建极致流畅的桌面体验
android·性能优化
hunterandroid2 小时前
Android ANR 排查实战:从线上告警到主线程卡点定位
android·前端
TimeFine3 小时前
智能眼镜开发:图片翻译与EXIF的重要性
android
律宏阔3 小时前
Android 车载蓝牙开发笔记:HFP、A2DP、AVRCP、PBAP、MAP、BLE 与系统 API
android·蓝牙
@Adzc3 小时前
Android 16 省电模式 Tile 点击导致状态栏收起
android
杉氧3 小时前
打破边界(二):在 React Native 中嵌入原生 UI 组件 (Native UI Components)
android·前端·react native
古法安卓3 小时前
Android-切换白天黑夜内存不足问题排查
android·java·android studio
聚美智数3 小时前
快递拦截-物流在途拦截-在途拦截API接口介绍
android