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

背景
开发中有时候需要绘制地图,但是Android无法像Html那样使用SVG图片并且实现可点击,可重绘色彩等功能。因此我们需要自己手动去实现这些效果和功能,由于这段时间时间相对充裕,因此下手去研究了一番。

项目链接:点击查看项目Git地址(dev分支)
APK下载:点击下载

地图组件均已提供了Kotlin和Dart的实现。 示例图中,我们实现了省份可点击效果,上色,描边等。

效果图:


Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)_第1张图片
疫情信息APP

具体实现

    1. 解析SVG图片,这里我们使用的是AndroidStudio转换后的Vector图片。


      Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击)_第2张图片
      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? = 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)
              }
          }
      
      }
      
    1. 图形绘制新增纯Flutter绘制地图,新增相关的Path路径绘制工具类:PathParser,该类通过Java源码移植到Dart语言。具体代码可查看Git库中的Dev分支即可,保持时常更新。

其他说明:转载请注明出处,谢谢!

你可能感兴趣的:(Kotlin/Flutter - 绘制疫情信息地图(SVG地图,区域可点击))