美文网首页Kotlin编程禅与计算机程序设计艺术Flutter圈子
Kotlin/Flutter - 绘制疫情信息地图(SVG地图,

Kotlin/Flutter - 绘制疫情信息地图(SVG地图,

作者: Cosecant | 来源:发表于2020-02-03 15:52 被阅读0次

    背景
    开发中有时候需要绘制地图,但是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分支即可,保持时常更新。

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

    相关文章

      网友评论

        本文标题:Kotlin/Flutter - 绘制疫情信息地图(SVG地图,

        本文链接:https://www.haomeiwen.com/subject/tcbhxhtx.html