Kotlin/Flutter|Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)

背景
开发中有时候需要绘制地图,但是Android无法像Html那样使用SVG图片并且实现可点击,可重绘色彩等功能。因此我们需要自己手动去实现这些效果和功能,由于这段时间时间相对充裕,因此下手去研究了一番。
项目链接:点击查看项目Git地址(dev分支)
APK下载:点击下载
地图组件均已提供了Kotlin和Dart的实现。 示例图中,我们实现了省份可点击效果,上色,描边等。
效果图:

Kotlin/Flutter|Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)
文章图片
疫情信息APP
具体实现
    1. 解析SVG图片,这里我们使用的是AndroidStudio转换后的Vector图片。

      Kotlin/Flutter|Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)
      文章图片
      map-vector.png 解析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() } } } }

    1. 构造相关的实体类,
      地图信息(ChinaMapInfo)
      省份图层信息(ProvinceLayerPathInfo)
      省份信息(ChinaProvinceInfo): 提供图形绘制、点击区域检测等方法
      data class ChinaMapInfo( var viewPortWidth: Float = 0f, var viewPortHeight: Float = 0f, var provinceInfoList: MutableList? = 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) }}

    1. 中国行政区域绘制
      /** 中国省份视图 **/ class ChinaProvinceView : View, View.OnTouchListener {private var data: ChinaMapInfo? = nullprivate var mapScale: Float = 1.0fprivate var _selectedProvinceInfo: ChinaProvinceInfo? = null/** 当前选择的省份 **/ var selectedProvinceInfo: ChinaProvinceInfo? get() = _selectedProvinceInfo set(value) { _selectedProvinceInfo = value }/** 省份选择事件 **/ var onProvinceSelectedChanged: ((ChinaProvinceInfo) -> Unit)? = nullconstructor(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 = https://www.it610.com/article/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) } }}

    1. 图形绘制新增纯Flutter绘制地图,新增相关的Path路径绘制工具类:PathParser,该类通过Java源码移植到Dart语言。具体代码可查看Git库中的Dev分支即可,保持时常更新。
【Kotlin/Flutter|Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)】其他说明:转载请注明出处,谢谢!

    推荐阅读