地图可视化分析,是数据的又一重要的体现。Android上面Compose怎么实现呢?
可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现
下图就是绘制好的效果图。看上去是不是很炫酷
一、前言
地图可视化开发的意义主要体现在以下几个方面:
- 提升管理效率:通过地图可视化,用户可以直观地浏览、分析和规划资源,实现资源的优化管理
- 优化空间布局:地图可视化技术可以直观展示空间布局,帮助管理者进行空间规划和优化。
- 增强用户体验:可视化地图通过精美的设计、动态效果和个性化定制,提供视觉上的愉悦感和信息获取的便捷性。
- 促进跨部门协作:可视化地图的价值不仅在于信息的直观展示,更在于促进跨部门协作、加速决策过程以及挖掘潜在商机
- 提高数据可读性:通过图形、颜色、符号等视觉元素展示地理位置信息及相关统计数据,极大地提升了数据的可读性和可理解性。
本文重点解决开发过程中的几个问题:
- 绘制地图哪些轨迹路径怎么得到
- 通过SVG数据处理,3种方法接受显示
- 通过SVG数据绘制完地图大小怎么控制
- SVG里面的数据怎么控制左边距,上边距,怎么控制地图刚好落在控件中间
二、地图哪些轨迹路径怎么得到?
为什么要使用SVG里面的轨迹数据?我们能自己通过公式计算出来吗?
不能,自己计算不出来,不能通过几何和相关公式计算出来,就算数学界最强天才高斯来了也没有用,中国地图是这样子,江苏地图是这样子,巴西地图是这样子....完全没有任何规则,它不像五角星,正六边形,圆,椭圆等有相关几何公式计算,可以计算算出来。
那些地图或者svg里面数据怎么来的?别人地图怎么开发的?
有专业的人员单独测量,每一个都单独计算出来的。就像画家一样,每个地方的地图单图可以画出来,他们是可以单独把这个轨迹数据制作出来。可能将这些数据存为不同的格式,svg里面的path只是其中的一种。我们不用关心这些专业人员怎么制作的,我们只关心从他们制作出来的数据,我们能拿到就行。
我们从哪儿来找svg数据? 这里介绍2个网站可以找:
2.或者从阿里数据可视化平台下载,从里面可以点击,各个省份,各个城市然后去下载svg
- Android里面不能直接使用SVG,需要转成Android里面能用的Vector.xml后才能使用:
推荐使用这个网站svg2vector来转化,
当然也可以使用Android Studio 里面自带的工具来转化。如下操作:
SVG处理成了Vector.xml后的数据打开看大致就和下面图里面差不多
三、SVG数据处理,3种方法接受显示出来
处理好的SVG数据怎么使用,有3种方式:
- 直接用图片控件接收,当作图表svg显示如下:
less
Image(
painter = painterResource(R.drawable.chinahigh), contentDescription = "map ", modifier = Modifier
.background(Color.Red)
.width(226.dp)
.height(226.dp)
)
- 使用
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()
}
- 使用
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
)
}
}
- 上面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以及下载下来之后怎么处理,包括大小调整,边距调整等
已经封装成库,你只需要准备好数据就可以了