Android Compose轻松绘制地图可视化图表,带点击事件,可扩展二次开发

地图可视化分析,是数据的又一重要的体现。Android上面Compose怎么实现呢?

可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现

下图就是绘制好的效果图。看上去是不是很炫酷

一、前言

地图可视化开发的意义‌主要体现在以下几个方面:

  1. 提升管理效率‌:通过地图可视化,用户可以直观地浏览、分析和规划资源,实现资源的优化管理
  2. 优化空间布局‌:地图可视化技术可以直观展示空间布局,帮助管理者进行空间规划和优化。
  3. 增强用户体验‌:可视化地图通过精美的设计、动态效果和个性化定制,提供视觉上的愉悦感和信息获取的便捷性。
  4. 促进跨部门协作‌:可视化地图的价值不仅在于信息的直观展示,更在于促进跨部门协作、加速决策过程以及挖掘潜在商机
  5. 提高数据可读性‌:通过图形、颜色、符号等视觉元素展示地理位置信息及相关统计数据,极大地提升了数据的可读性和可理解性。

本文重点解决开发过程中的几个问题:

  1. 绘制地图哪些轨迹路径怎么得到
  2. 通过SVG数据处理,3种方法接受显示
  3. 通过SVG数据绘制完地图大小怎么控制
  4. SVG里面的数据怎么控制左边距,上边距,怎么控制地图刚好落在控件中间

二、地图哪些轨迹路径怎么得到?

为什么要使用SVG里面的轨迹数据?我们能自己通过公式计算出来吗?

不能,自己计算不出来,不能通过几何和相关公式计算出来,就算数学界最强天才高斯来了也没有用,中国地图是这样子,江苏地图是这样子,巴西地图是这样子....完全没有任何规则,它不像五角星,正六边形,圆,椭圆等有相关几何公式计算,可以计算算出来。

那些地图或者svg里面数据怎么来的?别人地图怎么开发的?

有专业的人员单独测量,每一个都单独计算出来的。就像画家一样,每个地方的地图单图可以画出来,他们是可以单独把这个轨迹数据制作出来。可能将这些数据存为不同的格式,svg里面的path只是其中的一种。我们不用关心这些专业人员怎么制作的,我们只关心从他们制作出来的数据,我们能拿到就行。

我们从哪儿来找svg数据? 这里介绍2个网站可以找:

  1. 去下载各国地图数据

2.或者从阿里数据可视化平台下载,从里面可以点击,各个省份,各个城市然后去下载svg

  1. Android里面不能直接使用SVG,需要转成Android里面能用的Vector.xml后才能使用:
    推荐使用这个网站svg2vector来转化,
    当然也可以使用Android Studio 里面自带的工具来转化。如下操作:

SVG处理成了Vector.xml后的数据打开看大致就和下面图里面差不多

三、SVG数据处理,3种方法接受显示出来

处理好的SVG数据怎么使用,有3种方式:

  1. 直接用图片控件接收,当作图表svg显示如下:
less 复制代码
Image(
    painter = painterResource(R.drawable.chinahigh), contentDescription = "map ", modifier = Modifier
        .background(Color.Red)
        .width(226.dp)
        .height(226.dp)
)
  1. 使用Canvas绘制ImageBitmap:如下:
scss 复制代码
 val bitmap = imageResource(vectorResId = R.drawable.sichuan)
 Canvas(modifier = Modifier
                   .width(226.dp)
                   .height(226.dp)
                ) {
                    drawImage(bitmap, dstOffset = IntOffset(360, 250), dstSize = IntSize(226, 226)
                }

通过下面方法将Vector格式xml转化为图片ImageBitmap

kotlin 复制代码
@Composable
fun imageResource(vectorResId: Int): ImageBitmap {
    val context = LocalContext.current
    val vectorDrawable = context.getDrawable(vectorResId) as VectorDrawable
    val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
    vectorDrawable.draw(canvas)
    return bitmap.asImageBitmap()
}
  1. 使用Canvas通过drawPath来绘制Vector.xml里面的path的数据
    注意:阿里可视化下载下来处理成的vector里面颜色值只有3位在android里面是不能用的(android:fillColor="#fff" 不能用android.graphics.Color.parseColor(fillColor)来解析,解析不了的)
scss 复制代码
Canvas(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight()
) {
    it.list.forEach { m ->//这里it.list内容就是Vector.xml里面的path的一个列表,
        drawPath(
            path = m.pathData, color = m.fillColor, style = Fill
        )
    }
}
  1. 上面3中里面xml里面的path,怎么提取出来的,需要把处理好的vecotr类型的xml放在res/raw/下面:然后通过如下方法提取出来:
ini 复制代码
private const val PATH_TAG = "path"

    fun parserXml(resources: Resources, id: Int, tag: String = PATH_TAG, listColor: List<Color>? = null): ChartMapModel {
        val inputStream = resources.openRawResource(id)//需要把处理好的vecotr类型的xml放在res/raw/下面
        val newInstance = DocumentBuilderFactory.newInstance()
        val newDocumentBuilder = newInstance.newDocumentBuilder()
        //拿到 Docment 对象
        val document = newDocumentBuilder.parse(inputStream)
        //获取 xml 中属于 path 节点的所有信息
        val elementsByTagName = document.getElementsByTagName(tag)
        val list = mutableListOf<ChartMapData>()
        //定义四个点,确定整个 map 的范围
        var left = -1f
        var right = -1f
        var top = -1f
        var bottom = -1f
        val size = listColor?.size ?: 0
        //开始遍历标签,拿到 path 数据组
        for (i in 0 until elementsByTagName.length) {
            val item = elementsByTagName.item(i) as Element
            val name = item.getAttribute("android:name")
            val pathData = item.getAttribute("android:pathData")
            val fillColor = item.getAttribute("android:fillColor")
//            val fillColorC = android.graphics.Color.parseColor(fillColor)
            //compose 的 path
            val pathC = androidx.compose.ui.graphics.Path()
            pathC.addSvg(pathData)

            //获取控件的宽高     //获取到每个省份的边界
            val rect = RectF()
            //转化为原生Android 的path
            pathC.asAndroidPath().computeBounds(rect, true)
            //遍历取出每个path中的left取所有的最小值
            left = if (left == -1f) rect.left else left.coerceAtMost(rect.left)
            //遍历取出每个path中的right取所有的最大值
            right = if (right == -1f) rect.right else Math.max(right, rect.right)
            //遍历取出每个path中的top取所有的最小值
            top = if (top == -1f) rect.top else Math.min(top, rect.top)
            //遍历取出每个path中的bottom取所有的最大值
            bottom = if (bottom == -1f) rect.bottom else Math.max(bottom, rect.bottom)

            list.add(ChartMapData(name, listColor!![i % size], pathC, rect.left, rect.top, rect.right, rect.bottom))
        }
        val widthUIPX = right - left  //整个地图最左到最右边 这样计算出地图所占矩形的宽度,单位是px
        val heightUIPX = bottom - top //整个地图最上到最下边 这样计算出地图所占矩形的高度,单位是px
        val translateLeft = left * -1F
        val translateTop = top * -1F
        inputStream.close()
        return ChartMapModel(list, widthUIPX, heightUIPX, translateLeft, translateTop)
    }

所用ChartMapModel模型数据集

kotlin 复制代码
data class ChartMapModel(
    val list: MutableList<ChartMapData>,//xml 里面每一个path对应的数据集
    val widthUIPX: Float,//地图所占矩形宽度
    val heightUIPX: Float,//地图所占矩形高度
    val translateLeft: Float,//让地图所占矩形向左移动距离
    val translateTop: Float,//让地图所占矩形向上移动距离
) {
    @Stable
    var touchColor = Color.Red //点击当前显示颜色
    var hasClick = true

    @Stable
    var clickLayerColor = Color(0x80000000)//点击后展示浮层背景颜色
    var layerWidth = 160f//
    var layerHeight = 50f//
}

ChartMapData地图里面每个path的属性集

kotlin 复制代码
data class ChartMapData(
    val name: String, var fillColor: Color, val pathData: Path, val left: Float, val top: Float, val right: Float, val buttom: Float
)

四、通过SVG数据绘制完地图大小,边距怎么控制?

上面讲过了,怎么把svg处理成Vector.xml显示出来,但是有个问题:处理之后的原始Vector.xml显示是这样的:

以江西地图为例,用图片控件或者绘制ImageBitmap直接显示它显示成了如下样子:

这里可以看到:它并不像我们系统里面有些Vector图标,显示的最左边,最上面有边距,这样想袭击调节大小,想调节marginLeft,marginTop无法精准调节出来:

我们想要的是如下效果:下面是我处理调节后的效果:

我主要调节了几个值:

ini 复制代码
android:width="446dp"
android:height="573dp"
android:viewportWidth="446"
android:viewportHeight="573">
<group
    android:translateX="-553"
    android:translateY="-169">

这几个值怎么来的?

上面从raw里面解析xml的方法里面:
widthUIPX的值就是这里的446
heightUIPX的值就是这里573
translateLeft的值就是这里的-553它原来path里面的每个点的坐标left 都向左偏移了553,我们需要还原回去)
translateTop的值就是这里的-169(它原来path里面的每个点的坐标top都向上偏移了169,我们需要还原回去)

这样处理后,直接用图片控件接收展示,就是我们想要的。

我处理后直接用图片控件接收展示的效果如下

数据是从阿里数据可视化平台下载的svg源数据:
可以看到,红色背景是自己设置的,地图没有边距,而且每一个都是在设置列表控件的中间。

但是图片展示是没法控制点击到里面局部的某一块的

要想实现能够点击到地图里面每一块,如我头部展示的Gif图片效果

只能用使用Canvas通过drawPath来绘制Vector.xml里面的path的数据来实现:

使用该方法同样有边距大小的问题

如效果图四川地图为例(我设置大小为全屏):
处理前效果 处理后大小,边距后的效果:

这里所有处理我封装好了:只需要调用,可自定义控制大小,可控制点击后想要绘制的样子,

这里的点击效果就是头部中国地图gif图片的点击效果。

scss 复制代码
val context = LocalContext.current
val data by viewDModel33.mapData.observeAsState()
val textMeasurer = rememberTextMeasurer()
    data?.let {
        wxDrawMap(
            Modifier
                .background(Color.White)
                .fillMaxWidth()
                .fillMaxHeight()
//                .width(300.dp)//可自定义控制大小
//                .height(200.dp)
            , it
        ) { drawScope, x, y, abs, data ->
            //绘制点击的当前
            val newX = x * abs  //计算出点击屏幕真实x
            val newY = y * abs  //计算出点击屏幕真实y
            val fontSizeDip = DisplayUtil.sp2Dip(context, 16.sp.value)
            val textWidth = getStrPhysicsLength(data.name) * fontSizeDip
            drawScope.drawRect(
                it.clickLayerColor, size = Size((textWidth + 2 * fontSizeDip).toFloat(), 4 * fontSizeDip.toFloat()), topLeft = Offset(newX, newY)
            )
            drawScope.drawText(
                textMeasurer = textMeasurer, text = data.name, style = TextStyle(color = Color.White, fontSize = 16.sp), topLeft = Offset(newX + fontSizeDip, newY + 10f)
            )
        }
    }

具体怎么绘制,怎么放大,怎么调节边距,绘制原理就是下面这个方法:

scss 复制代码
@Composable
fun wxDrawMap(modifier: Modifier, data: ChartMapModel, drawCurr: ((DrawScope, Float, Float, Float, ChartMapData) -> Unit)? = null) {
    val context = LocalContext.current
    val list = remember(data.list) { data.list }
    var width by remember { mutableIntStateOf(0) }
    var height by remember { mutableIntStateOf(0) }
    val scaleAbsW = remember(width) { width.toFloat() / data.widthUIPX }
    val scaleAbsH = remember(height) { height.toFloat() / data.heightUIPX }
    val scaleAbs = remember(scaleAbsW, scaleAbsH) { scaleAbsW.coerceAtMost(scaleAbsH) }
    val marginLeft = remember(width, scaleAbs) { if (width - scaleAbs * data.widthUIPX > 0) DisplayUtil.px2dip(context, (width - scaleAbs * data.widthUIPX) / 2f).toFloat() else 0f }
    val marginTop = remember(height, scaleAbs) { if (height - scaleAbs * data.heightUIPX > 0) DisplayUtil.px2dip(context, (height - scaleAbs * data.heightUIPX) / 2f).toFloat() else 0f }

    var touchX by remember { mutableFloatStateOf(0f) }
    var touchY by remember { mutableFloatStateOf(0f) }
    var touchIndex by remember { mutableIntStateOf(-1) }

    Box(modifier = modifier
        .padding(marginLeft.dp, marginTop.dp, 0.dp, 0.dp)
        .onSizeChanged {
            if (width == 0) {
                width = it.width
                height = it.height
            }
        }
        .pointerInput(Unit) {
            detectTapGestures(onTap = {
                if (data.hasClick) {
                    val touchData = getPtInPath(it.x, it.y, list, scaleAbs, data.translateLeft, data.translateTop)
                    touchIndex = touchData.first
                    touchX = touchData.second
                    touchY = touchData.third
                }
            })
        }, contentAlignment = Alignment.TopStart
    ) {
        data.takeIf {
            width > 0 && height > 0 && scaleAbsW > 0f && scaleAbsH > 0f
        }?.let {
            Canvas(
                modifier = Modifier
                    .wrapContentHeight()
                    .wrapContentWidth()
                    .scale(scaleAbs, scaleAbs)
            ) {
                list.forEach { m ->
                    translate(data.translateLeft, data.translateTop) {
                        drawPath(
                            path = m.pathData, color = m.fillColor, style = Fill
                        )
                    }
                }
                touchIndex.takeIf {
                    data.hasClick && it != -1
                }?.let {
                    translate(data.translateLeft, data.translateTop) {
                        drawPath(
                            path = list[it].pathData, color = data.touchColor, style = Fill
                        )
                    }
                }
            }

            Canvas(
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
            ) {
                touchIndex.takeIf {
                    data.hasClick && it != -1
                }?.let {
                    drawCurr?.invoke(this@Canvas, touchX, touchY, scaleAbs, list[touchIndex])
                }
            }
        }
    }
}

具体点击事件代码计算如下:

scss 复制代码
private fun getPtInPath(x: Float, y: Float, paths: MutableList<ChartMapData>, scaleAbs: Float, left: Float, top: Float): Triple<Int, Float, Float> {
    paths.forEachIndexed { index, it ->
        val pathAndroid = it.pathData.asAndroidPath()
        val region = Region()
        var fect = RectF()
        pathAndroid.computeBounds(fect, true)
        region.setPath(pathAndroid, Region(fect.left.toInt(), fect.top.toInt(), fect.right.toInt(), fect.bottom.toInt()))
        val isInside = region.contains(((x / scaleAbs) - left).toInt(), ((y / scaleAbs) - top).toInt())
        if (isInside) {
            val centerX = fect.centerX()
            val centerY = fect.centerY()
            val isInside2 = region.contains(((centerX) - left).toInt(), ((centerY) - top).toInt())
            return if (isInside2) {
                Triple(index, centerX, centerY)
            } else {
                Triple(index, x / scaleAbs, y / scaleAbs)
            }
        }
    }
    return Triple(-1, 0f, 0f)
}

五、怎么使用

1、repositories中添加如下maven

rust 复制代码
   repositories {
        maven { url 'https://repo1.maven.org/maven2/' }
        maven { url 'https://s01.oss.sonatype.org/content/repositories/releases/' }
    }
}

2、 dependencies中添加依赖

scss 复制代码
implementation("io.github.wgllss:Wgllss-WXChart:1.0.36")

3、viewModwl中解析处理xml数据

kotlin 复制代码
private val _datas = MutableLiveData<ChartMapModel>()
val mapData: LiveData<ChartMapModel> = _datas
fun setData() {
        viewModelScope.launch {
            flow {
               emit(ParserXmlUtils.parserXml(MyApp.application.resources, id, listColor = listColor))
            }.flowOn(Dispatchers.IO).catch { it.printStackTrace() }.collect {
                it?.let {
                    _datas.value = it
                }
            }
        }
    }

五、总结

本文重点介绍了:怎么绘制地图,怎么使用svg以及下载下来之后怎么处理,包括大小调整,边距调整等

已经封装成库,你只需要准备好数据就可以了

github地址
gitee地址

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

这里你会学到不一样的东西

相关推荐
钝挫力PROGRAMER1 小时前
架构级代码复用实战:从继承泛型到函数式接口的深度重构
重构·架构
Q186000000001 小时前
springboot 四层架构之间的关系整理笔记一
spring boot·后端·架构
二流小码农1 小时前
鸿蒙开发:Canvas绘制之画笔对象Pen
android·ios·harmonyos
码农幻想梦2 小时前
18491 岛屿的数量
android·java·开发语言
技术宝哥2 小时前
Google 停止 AOSP 开源,安卓生态要“变天”?
android·开源协议
阿杰在学习3 小时前
基于OpenGL ES实现的Android人体热力图可视化库
android·前端·opengl
weiran19993 小时前
手把手的建站思路和dev-ops方案
前端·后端·架构
行墨3 小时前
Kotlin函数类型作为返回类型
android
今阳3 小时前
鸿蒙开发笔记-15-应用启动框架AppStartup
android·华为·harmonyos
_一条咸鱼_3 小时前
Android Compose 框架的颜色与形状之颜色管理深入剖析(四十一)
android