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

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

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

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

效果图:


疫情信息APP

具体实现

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


      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<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)
          }
      
      }
      
    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分支即可,保持时常更新。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容