Flutter - 绘制疫情信息地图
背景
开发中有时候需要绘制地图,但是Android无法像Html那样使用SVG图片并且实现可点击,可重绘色彩等功能。因此我们需要自己手动去实现这些效果和功能,由于这段时间时间相对充裕,因此下手去研究了一番。
地图组件均已提供了Kotlin和Dart的实现。 示例图中,我们实现了省份可点击效果,上色,描边等。
具体实现 1、解析SVG图片,这里我们使用的是AndroidStudio转换后的Vector图片。
解析SVG-XML文件,
a. PathParser.createPathFromPathData可以根据android:pathData中的数据得到Path, canvas就可以通过Path来绘制图像了!
b. 这里使用的是XmlPullParser来解析XML文件的,由于文件较小,这里没有做多线程处理。
/**
* 通过地图资源的RawId获取地图信息
* @param mapRawId 地图资源ID
*/
fun Context.getChinaMapInfoByMapRawId(@RawRes mapRawId: Int): ChinaMapInfo {
val xmlPullParser = Xml.newPullParser().apply {
setInput(StringReader(BufferedInputStream(resources.openRawResource(mapRawId)).bufferedReader().readText()))
}
return ChinaMapInfo(provinceInfoList = mutableListOf()).apply {
var eventType = xmlPullParser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
try {
when (eventType) {
XmlPullParser.START_TAG -> when (xmlPullParser.name) {
"vector" -> {
viewPortWidth = xmlPullParser.getAttributeValue(null, "viewportWidth").toFloat()
viewPortHeight = xmlPullParser.getAttributeValue(null, "viewportHeight").toFloat()
}
"path" -> provinceInfoList?.add(ChinaProvinceInfo(ProvinceLayerPathInfo(
xmlPullParser.getAttributeValue(null, "name"),
xmlPullParser.getAttributeValue(null, "strokeWidth").toFloat(),
Color.parseColor(xmlPullParser.getAttributeValue(null, "strokeColor")),
Color.parseColor(xmlPullParser.getAttributeValue(null, "fillColor")),
PathParser.createPathFromPathData(xmlPullParser.getAttributeValue(null, "pathData"))
)))
}
}
eventType = xmlPullParser.next()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
2、构造相关的实体类,
地图信息(ChinaMapInfo)
省份图层信息(ProvinceLayerPathInfo)
省份信息(ChinaProvinceInfo): 提供图形绘制、点击区域检测等方法
data class ChinaMapInfo(
var viewPortWidth: Float = 0f,
var viewPortHeight: Float = 0f,
var provinceInfoList: MutableList<ChinaProvinceInfo>? = null
)
data class ProvinceLayerPathInfo(
var name: String,
var strokeWidth: Float,
var strokeColor: Int,
var backgroundColor: Int,
var drawPathInfo: Path
)
/**
* 中国省份信息
* @param provinceLayerPathInfo 省份图层信息
*/
class ChinaProvinceInfo(private val provinceLayerPathInfo: ProvinceLayerPathInfo) {
/** 图形路径 **/
private var path: Path = provinceLayerPathInfo.drawPathInfo
/** 描边宽度 **/
private var _borderWidth: Float = provinceLayerPathInfo.strokeWidth
/** 描边颜色 **/
private var _borderColor: Int = provinceLayerPathInfo.strokeColor
/** 背景色 **/
private var _bgColor: Int = provinceLayerPathInfo.backgroundColor
/** 文本颜色 **/
private var _textColor: Int = Color.BLACK
/** 文本字体大小 **/
private var _textSize: Float = 9f
/** 图形所在的Region **/
private var region: Region = buildRegion(path)
/** 设置或获取边框宽度 **/
var borderWidth: Float
get() = _borderWidth
set(value) {
_borderWidth = value
}
/** 设置或获取描边颜色 **/
var borderColor: Int
get() = _borderColor
set(value) {
_borderColor = value
}
/** 设置或获取背景颜色 **/
var backgroundColor: Int
get() = _bgColor
set(value) {
_bgColor = value
}
/** 文本颜色 **/
var textColor: Int
get() = _textColor
set(value) {
_textColor = value
}
/** 文本大小 **/
var textSize: Float
get() = _textSize
set(value) {
_textSize = value
}
private fun buildRegion(path: Path): Region {
val pathBoundsRect = RectF()
path.computeBounds(pathBoundsRect, false)
return Region().apply {
setPath(path, Region(pathBoundsRect.left.toInt(),
pathBoundsRect.top.toInt(),
pathBoundsRect.right.toInt(),
pathBoundsRect.bottom.toInt()))
}
}
/** 是否被点击 **/
fun isTouched(x: Float, y: Float) = region.contains(x.toInt(), y.toInt())
/**
* 绘制省份路径
* @param canvas 画布
* @param isFill 是填充还是描边, 默认为TRUE
* @param pathColor 颜色,如果不能存在该值时使用对象内置的颜色
*/
fun drawPath(canvas: Canvas?, isFill: Boolean = true, pathColor: Int? = null) {
val paint = Paint().apply {
isAntiAlias = true
if (isFill) {
style = Paint.Style.FILL
color = pathColor ?: _bgColor
} else {
style = Paint.Style.STROKE
color = pathColor ?: _borderColor
strokeWidth = _borderWidth
}
}
canvas?.drawPath(path, paint)
}
/**
* 绘制省份名称
* @param context 上下文对象
* @param canvas 画布
*/
fun drawName(context: Context?, canvas: Canvas?) {
val provinceName = provinceLayerPathInfo.name
val paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
color = _textColor
textSize = (context?.resources?.displayMetrics?.scaledDensity ?: 0f) * _textSize + 0.5f
}
val drawPoint = getNameDrawOffset(provinceName, paint)
canvas?.drawText(provinceName, drawPoint.x, drawPoint.y, paint)
}
/**
* 获取省份名称的绘制位置
* @param provinceName 身份名称
* @param paint 画笔
*/
private fun getNameDrawOffset(provinceName: String, paint: Paint): PointF {
val textBounds = Rect()
paint.getTextBounds(provinceName, 0, provinceName.length, textBounds)
val regionWidth = region.bounds.width()
val regionHeight = region.bounds.height()
val textWidth = textBounds.width()
val textHeight = textBounds.height()
var offsetX: Float = (regionWidth - textWidth) / 2f
var offsetY: Float = (regionHeight - textHeight) * 2f / 3f
when (provinceName) {
"重庆" -> offsetY = regionHeight * 0.7f
"天津" -> {
offsetX = regionWidth * 0.7f
offsetY = regionHeight * 1.0f
}
"内蒙古" -> offsetY = regionHeight * 4 / 5f
"河北" -> {
offsetX = regionWidth * 0.1f
offsetY = regionHeight * 0.7f
}
"甘肃" -> {
offsetX = regionWidth * 0.15f
offsetY = regionHeight * 0.23f
}
"陕西" -> offsetY = regionHeight * 0.73f
"江西" -> offsetX = regionWidth * 0.2f
"江苏" -> offsetX = regionWidth * 0.55f
"上海" -> {
offsetX = regionWidth * 0.8f
offsetY = regionHeight * 0.8f
}
"海南" -> offsetY = regionHeight * 0.7f
"广东" -> offsetY = regionWidth * 0.3f
"香港" -> {
offsetX = regionWidth * 1.0f
offsetY = regionWidth * 1.0f
}
"澳门" -> offsetY = regionWidth * 1.0f + textHeight
}
return PointF(region.bounds.left + offsetX, region.bounds.top + offsetY)
}
}
3、中国行政区域绘制
/** 中国省份视图 **/
class ChinaProvinceView : View, View.OnTouchListener {
private var data: ChinaMapInfo? = null
private var mapScale: Float = 1.0f
private var _selectedProvinceInfo: ChinaProvinceInfo? = null
/** 当前选择的省份 **/
var selectedProvinceInfo: ChinaProvinceInfo?
get() = _selectedProvinceInfo
set(value) {
_selectedProvinceInfo = value
}
/** 省份选择事件 **/
var onProvinceSelectedChanged: ((ChinaProvinceInfo) -> Unit)? = null
constructor(context: Context?) : super(context) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
}
@SuppressLint("NewApi")
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context)
}
private fun init(context: Context?) {
setOnTouchListener(this)
data = context?.getChinaMapInfoByMapRawId(R.raw.ic_map_china)
}
// 处理点击事件
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
val selectedProvinceInfo = data?.provinceInfoList?.firstOrNull { it.isTouched(event.x / mapScale, event.y / mapScale) }
if (selectedProvinceInfo != null && selectedProvinceInfo != _selectedProvinceInfo) {
_selectedProvinceInfo?.backgroundColor = Color.TRANSPARENT
_selectedProvinceInfo = selectedProvinceInfo
_selectedProvinceInfo?.backgroundColor = Color.RED
onProvinceSelectedChanged?.invoke(selectedProvinceInfo)
invalidate()
}
return true
}
return false
}
// 处理View大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
var height = MeasureSpec.getSize(heightMeasureSpec)
if (data != null) {
mapScale = (width.toFloat() / data!!.viewPortWidth)
height = (data!!.viewPortHeight * mapScale).toInt()
}
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
//绘制图形函数
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.scale(mapScale, mapScale)
data?.provinceInfoList?.forEach { provinceInfo ->
provinceInfo.drawPath(canvas, true)
provinceInfo.drawPath(canvas, false)
}
data?.provinceInfoList?.forEach { provinceInfo ->
provinceInfo.drawName(context, canvas)
}
}
}
4、图形绘制新增纯Flutter绘制地图,新增相关的Path路径绘制工具类:PathParser,该类通过Java源码移植到Dart语言。具体代码可查看Git库中的Dev分支即可,保持时常更新。
了解更多,欢迎关注:
- 博客:https://YuriyPikachu.github.io
- github:https://github.com/YuriyPikachu
- QQ技术交流群:389274438
- csdn:https://blog.csdn.net/pjingying
- 知乎:YuriyPikachu
- 简书:YuriyPikachu
- 邮箱:YuriyPikachu@163.com
- 头条:Android开发加油站
- 公众号:Android开发加油站