- 开发无止境 -
Data: 2015-01-02 07:14:18Form: JournalClick: 10
本文章向大家介绍微信小程序手绘地图实现之《Canvas》,主要包括微信小程序手绘地图实现之《Canvas》使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
环境:微信SDK2.9+
正题:
先创建一个地图组件
1 <template> 2 <view class="customCanvasComponent"> 3 <!-- 建立画布坐标系 --> 4 <canvas 5 :style="{ 6 width: `${options.style.width}rpx`, 7 height: `${options.style.height}rpx`, 8 border: options.style.border, 9 background: options.style.background 10 }" 11 type="2d" 12 :id="customMapId" 13 :canvas-id="customMapId" 14 @click="clickToCanvas" 15 @touchstart="touchStartToCanvas" 16 @touchmove="touchMoveToCanvas" 17 @touchend="touchEndToCanvas"> 18 <!-- 由于微信限制 暂时只支持这种写法 请不要秀其他方式 否则凉凉 --> 19 <!-- Marker点集合 --> 20 <!-- <blank v-for="poi in handlerMarkerList" :key="poi.id"> 21 <cover-view 22 class="point" 23 @click="pointChange(poi)" 24 :style="{ 25 position: 'absolute', 26 display: 'flex', 27 flexDirection: 'column', 28 alignItems: 'center', 29 left: poi.x + 'px', 30 top: poi.y + 'px', 31 transform: `translate(-50%, -100%)` 32 }"> 33 <cover-image :style="poi.stringStyle" :src="poi.icon"></cover-image> 34 <cover-view class="labelView" :style="poi.stringLabelStyle"> 35 <cover-view class="labelTitle">{{poi.label}}</cover-view> 36 </cover-view> 37 </cover-view> 38 </blank> --> 39 <!-- WindowInfo窗体设置 --> 40 <blank v-if="checkPointMarker"> 41 <cover-view class="windowInfoGroupBox" :style="{ 42 position: 'absolute', 43 left: checkPointMarker.x + 'px', 44 top: checkPointMarker.y + 'px', 45 transform: `translate(-50%, calc(-100% - 90rpx))` 46 }"> 47 <cover-view class="infoTitle"> 48 <cover-view class="infoVoiceBtn"> 49 <cover-image class="infoImage" :src="checkPointMarker.image"></cover-image> 50 <cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_play@2x.png"></cover-image> 51 <cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_pause@2x.png"></cover-image> 52 </cover-view> 53 <cover-view class="infoContent"> 54 <cover-view class="title otext2"></cover-view> 55 <cover-view class="distance"></cover-view> 56 </cover-view> 57 </cover-view> 58 <cover-view class="btnTools"> 59 <cover-view class="btn"> 60 <cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_voice@2x.png"></cover-image> 61 <cover-view class="btnText">解说</cover-view> 62 </cover-view> 63 <cover-view class="btn"> 64 <cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_info@2x.png"></cover-image> 65 <cover-view class="btnText">详情</cover-view> 66 </cover-view> 67 </cover-view> 68 </cover-view> 69 </blank> 70 <!-- 预留控件 由于小程序限制机制 请使用时仅可使用顶级标签<cover-view><cover-image> --> 71 <!-- 默认返回处理后的Marker点集合 --> 72 <!-- ControlFirmware Left --> 73 <slot name="control-l"/> 74 <!-- ControlFirmware Right --> 75 <slot name="control-r"/> 76 <!-- ControlFirmware Top --> 77 <slot name="control-t"/> 78 <!-- ControlFirmware Bottom --> 79 <slot name="control-b"/> 80 <!-- 其他控件预留 --> 81 <slot name="other"/> 82 <!-- <cover-view class="toolsBox"> 83 <cover-view class="pointGroupBox"> 84 <blank> 85 <cover-view v-for="poi in handlerMarkerList" :key="poi.id" class="point" :style="{position: 'absolute', left: poi.x + 'px', top: poi.y + 'px'}"> 86 <cover-image :style="{...poi.style}" :src="poi.icon"></cover-image> 87 <cover-view class="labelView" :style="{...poi.labelStyle}"> 88 <cover-view class="labelTitle">{{poi.label}}</cover-view> 89 </cover-view> 90 </cover-view> 91 </blank> 92 </cover-view> 93 <cover-view class="windowInfoGroupBox"> 94 测试 95 <cover-image style="" src="/static/images/scenic/tour_voice_poi_01@2x.png"></cover-image> 96 </cover-view> 97 </cover-view> --> 98 </canvas> 99 <!-- 建立与画布对应的平面坐标系 -->100 </view>101 </template>102 103 <script>104 import CustomCavnasMap from './map'105 let CustomMapInital = null106 export default {107 // 组件配置说明 必须基于某个地图提供商进行的适配 高德 百度 腾讯 谷歌108 // 这里使用高德109 props: {110 // 部分配置参数111 options: {112 type: Object,113 default: () => {114 return {115 // 样式层116 style: {117 // 宽高单位均为rpx118 width: 750,119 height: 1334,120 // 背景支持色值或者网络图片背景图121 background: 'pink',122 border: 'none'123 },124 // 坐标中心点 LngLat对象125 center: [113.9120864868165, 22.545537650869],126 // 地图范围 [LngLat, LngLat] 取点应为对角两个坐标 !!!注意坐标点位置 [右上<RT>, 左下<LB>]127 limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],128 // 初始化地图层级129 initalZoom: 16,130 // 地图层级范围131 zooms: [16, 18],132 // 图层133 layers: [134 {135 // 图片覆盖物 坐标范围 !!!注意坐标点位置 [右上<RT>, 左下<LB>]136 limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],137 // 覆盖物地址138 image: 'https://xxx/static/map-bg.jpeg',139 // 透明度140 opacity: 1,141 // 缩放范围142 zooms: [16, 19]143 }144 ],145 // 路线146 lineStyle: {147 lineWidth: 5,148 lineColor: 'red',149 lineArray: []150 },151 // 自定义Marker152 markers: [153 {154 icon: '/static/images/scenic/tour_voice_poi_01@2x.png',155 position: [113.9128,22.544674],156 style: {157 width: '93rpx',158 height: '105rpx',159 position: 'relative',160 top: '60rpx'161 },162 label: '(内测)城管大楼',163 labelStyle: {164 position: 'relative',165 top: '-90rpx',166 left: '50%',167 transform: 'translateX(-50%)',168 background: '#FFF',169 padding: '5rpx 10rpx',170 fontSize: '28rpx'171 }172 },173 {174 icon: '/static/images/scenic/tour_voice_poi_01@2x.png',175 position: [113.911765,22.545397],176 style: {177 width: '93rpx',178 height: '105rpx',179 position: 'relative',180 top: '60rpx'181 },182 label: '(内测)凉亭',183 labelStyle: {184 position: 'relative',185 top: '-90rpx',186 left: '50%',187 transform: 'translateX(-50%)',188 background: '#FFF',189 padding: '5rpx 10rpx',190 fontSize: '28rpx'191 }192 }193 ]194 }195 }196 },197 // canvasId198 customMapId: {199 type: String,200 default: 'customMap'201 }202 },203 data () {204 return {205 // initalZoom: null,206 // CustomMapInital: null, // 不要定义到data中 容易引发内存互换207 handlerMarkerList: [],208 checkPointMarker: null209 }210 },211 watch: {212 'options.lineStyle.lineArray': {213 handler (_new, _old) {214 if (_new !== _old) {215 this.drawLine(_new)216 }217 },218 deep: true219 }220 },221 methods: {222 initalCanvasMap () {223 // console224 CustomMapInital = new CustomCavnasMap({225 customMapId: this.customMapId,226 _component: this227 }, Object.assign({}, this.options, {228 markerCallBack: (list) => {229 console.log(list)230 this.handlerMarkerList = list231 },232 cilckPointChange: (info) => {233 if (info) {234 console.log(info)235 console.log('得到点击成功后的触发')236 this.pointChange(info)237 } else {238 console.log('得到点击空白的回调')239 }240 }241 }))256 },257 fetchCustomBoxSize () {258 nui.getImageInfo({259 src: '',260 success: (rect) => {261 console.log(rect.fillPath[0])262 }263 })264 },265 /**266 * @Function267 * @public 公共类方法268 * @return Object269 */270 // 设置缩放比例271 setZoom (zoom, callback) {272 // 最低限制为初始化的缩放比例273 if (zoom > this.options.initalZoom) {274 // 逻辑处理275 CustomMapInital.setZoom(this.initalZoom, callback)276 } else {277 CustomMapInital.setZoom(zoom, callback)278 }279 },280 // 获取缩放比例281 getZoom (callback) {282 if (callback) {283 callback && callback(CustomMapInital.getZoom())284 } else {285 return CustomMapInital.getZoom()286 }287 },288 /**289 * 290 * @touch 事件向this.CustomMapInital触发291 */292 touchStartToCanvas (e) {293 CustomMapInital.touchStartToCanvas(e)294 },295 touchMoveToCanvas (e) {296 CustomMapInital.touchMoveToCanvas(e)297 },298 touchEndToCanvas (e) {299 CustomMapInital.touchEndToCanvas(e)300 },301 /**302 * @click 事件向下触发303 */304 clickToCanvas (e) {305 CustomMapInital.clickToCanvas(e)306 // 点击其他地方进行清空WindowInfo窗体307 this.checkPointMarker = null308 },309 /**310 * @param {info<Object>} 类型为Marker数据对象311 */312 pointChange (info) {313 this.checkPointMarker = info314 },315 /**316 * @param {lineArray<Array|Object>} 传入的线路数据317 * @param {Object} {longitude, latitude} 必须318 */319 drawLine (lineArray) {320 CustomMapInital.drawLine(CustomMapInital.LngLatConversionToPixel(lineArray))321 }322 },323 onReady () {324 this.initalCanvasMap()325 },326 onUnload () {327 CustomMapInital = null328 }329 }330 </script>331 332 <style lang="sass" scoped>333 $defaultBg: #FFF334 $bgF4: #F4F4F4335 $color3: #333336 $color6: #666337 $color9: #999338 // $defaultBg: pink339 // 取消默认样式340 cover-view341 overflow: initial !important342 .customCanvasComponent343 // .toolsBox344 // position: absolute345 .point346 position: absolute347 z-index: -1348 display: flex349 flex-direction: column350 align-items: center351 .labelView352 border-radius: 10rpx353 background-color: $defaultBg354 .labelTitle355 font-size: 28rpx356 .windowInfoGroupBox357 background-color: $defaultBg358 border-radius: 10rpx359 width: 320rpx360 height: 228rpx361 box-shadow: 10rpx 10rpx 20rpx -10rpx $color6362 display: flex363 flex-direction: column364 z-index: 99365 .infoTitle366 display: flex367 align-items: center368 padding: 20rpx369 .infoVoiceBtn370 width: 120rpx371 height: 120rpx372 flex: 0 0 120rpx373 border: 1px solid $bgF4374 border-radius: 50%375 overflow: hidden376 position: relative377 cover-image378 width: 100%379 height: 100%380 object-fit: contain381 .playControl382 position: absolute383 width: 68rpx384 height: 68rpx385 top: 50%386 left: 50%387 transform: translate(-50%, -50%)388 .infoContent389 flex: 1390 margin-left: 20rpx391 .title392 font-size: 28rpx393 line-height: 28rpx394 min-height: 56rpx395 color: $color3396 font-weight: bold397 // margin-right: 58rpx398 overflow: inherit399 .distance400 font-size: 22rpx401 color: $color9402 // margin-right: 0.58rem403 margin-top: 10rpx404 .btnTools405 display: flex406 flex: 1407 .btn408 flex: 0 0 calc(50% - 40rpx)409 display: flex410 margin: 0 20rpx 15rpx 20rpx411 align-items: center412 justify-content: center413 border-radius: 30rpx414 cover-image415 width: 30rpx416 height: 30rpx417 .btnText418 color: $defaultBg419 font-size: 28rpx420 .btn:nth-child(1)421 background: #80D2FC422 background: linear-gradient(#80D2FC, #188EE9)423 background: linear-gradient(to right, #80D2FC, #188EE9)424 .btn:nth-child(2)425 background: #FBA326426 background: linear-gradient(#FBA326, #FBA326)427 background: linear-gradient(to right, #FBA326, #FBA326)428 </style>
.map.js
1 module.exports = class CustomCavnasMap { 2 canvasContext = null 3 // 定义背景装载图 4 layersImages = [] 5 // 初始化Lock锁超出最大值停止初始化 6 initLock = 0 7 maxLockValue = 1000 8 // 记录手指按下时的坐标 以及位置 9 startingCoordinate = null 10 // 旋转时中心点或者缩放时中心点 默认为画布起点 11 rotateCenter = { 12 x: 0, 13 y: 0 14 } 15 // 背景图的偏移量 16 offsetConfig = { 17 mapX: 0, 18 mapY: 0 19 } 20 // 捏合缩放倍数或者滚轮缩放倍数 21 mapScale = 1 22 // 捏合缩放状态 23 mapZoom = false 24 // 双指旋转角度地图旋转角度 25 mapRotate = 0 26 // 两指距离 27 mapDistance = 0 28 // 地图层级限制 最大值 默认两倍 29 mapMaxZoom = 2 30 // 地图层级限制 最小值 默认一倍 31 mapMinZoom = 1 32 // 惯性的运动距离 带方向的距离单位 33 inertialMotion = { 34 x: 0, 35 y: 0 36 } 37 // 新增拖拽惯性支持 摩擦系数μs 范围应该在0-1之间 38 us = 0.9 39 // 惯性定时器 40 inertialMotionTimer = null 41 COMPUT_TIME = null 42 // 图片预加载对象 43 pictureExtractionObject = {} 44 // 点击Canvas后的点位 45 clickPoint = { 46 x: 0, 47 y: 0 48 } 49 // 点击触发后的状态 0未点击 1点击了 2点击了但是点击错了 50 clickStatus = 0 51 /** 52 * @methods 53 * @param {Object<customMapId,_component>} canvasOtions 画布对象 54 * @param {Object<style,center,limitBounds,initalZoom,layers>} options 地图参数管控 55 */ 56 constructor(canvasOtions, options) { 57 // super(this) 58 console.log('进入构造函数-->') 59 // Object.keys(options) 60 // 获取设备属性 61 this.asyncFetchSystemInfo() 62 // this.systemInfo = wx.getSystemInfoSync() 63 // 属性继承 64 Object.assign(this, canvasOtions, options) 65 // 手动处理范围值 66 this.zooms && (this.mapMaxZoom = this.zooms[1] - (this.initalZoom || this.zooms[0])) && (this.mapMinZoom = (this.zooms[0] - this.initalZoom) || 1) 67 console.log('当前限制范围为:' + this.mapMinZoom + '-' + this.mapMaxZoom) 68 // if (canvasOtions instanceof Object) { 69 // this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId, canvasOtions._component) 70 // } else { 71 // this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId) 72 // } 73 // 设置分辨率 74 // this.dpr = 1 75 // 设置画布实际大小 76 // this.canvasOptions = { 77 // width: parseInt(this.rpxToPx(options.style.width) * this.dpr), 78 // height: parseInt(this.rpxToPx(options.style.height) * this.dpr) 79 // } 80 // 获取Canvas节点元素 81 this.wxCreateSelectorQuery().select(`#${canvasOtions.customMapId}`).fields({ 82 node: true, 83 rect: true 84 }, res => { 85 // console.log(res) 86 this.customCanvas = res.node 87 // this.computedConversionData() 88 // this.createMapBGImage(rect.node) 89 this.dpr = this.systemInfo.pixelRatio 90 91 // this.dpr = 1 92 // 设置大小 93 this.customCanvas.width = parseInt(this.rpxToPx(options.style.width) * this.dpr) 94 this.customCanvas.height = parseInt(this.rpxToPx(options.style.height) * this.dpr) 95 // 获取画布context上下文 2d 96 this.ctxCanvas = this.customCanvas.getContext('2d') 97 // 获取画布context上下文 webgl 98 // this.glCanvas = this.customCanvas.getContext('webgl') 99 // console.log(this.customCanvas)100 }).exec()101 // 开始初始化自定义地图102 this.initalCanvasChange()103 }104 // 初始化Canvas画布对象105 initalCanvasChange() {106 if (this.customCanvas) {107 this.computedConversionData()108 } else {109 setTimeout(() => {110 console.log('设置延迟100ms进行渲染Canvas画布')111 this.initLock++112 this.initLock < this.maxLockValue && this.initalCanvasChange()113 }, 100)114 }115 }116 // 提供选择节点的公共方法117 wxCreateSelectorQuery() {118 if (this._component) {119 return wx.createSelectorQuery().in(this._component)120 } else {121 return wx.createSelectorQuery()122 }123 }124 // 计算两点坐标实际距离公式125 GetDistance(LngLat1, LngLat2) {126 var radLat1 = LngLat1[1] * Math.PI / 180.0127 var radLat2 = LngLat2[1] * Math.PI / 180.0128 var a = radLat1 - radLat2129 var b = LngLat1[0] * Math.PI / 180.0 - LngLat2[0] * Math.PI / 180.0130 var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)))131 s = s * 6378.137 // EARTH_RADIUS132 s = Math.round(s * 10000) / 10000133 return s134 }135 // 顺序构建map图库136 createMapBGImage() {144 // 清空页面绘制 2d145 this.ctxCanvas.clearRect(0, 0, this.customCanvas.width, this.customCanvas.height)146 147 // 绘制canvas背景颜色148 // this.ctxCanvas.fillStyle = this.style.background149 // this.ctxCanvas.fillRect(0, 0, this.customCanvas.width, this.customCanvas.height)150 // this.canvasContext.clearRect(0, 0, this.canvasOptions.width, this.canvasOptions.height)151 // this.glCanvas.clear(this.glCanvas.COLOR_BUFFER_BIT)152 // console.log(this.rotateCenter)153 // 设置旋转中心点154 this.ctxCanvas.translate(this.rotateCenter.x, this.rotateCenter.y)155 // 对画布进行旋转 暂时关闭旋转156 // this.ctxCanvas.rotate(this.mapRotate * Math.PI / 180)157 // 当绘制结束后 还原旋转中心点158 this.ctxCanvas.translate(-this.rotateCenter.x, -this.rotateCenter.y)159 this.ctxCanvas.save()160 // 循环进行处理图片 缩放 平移控制161 this.layersImages.map(img => {162 // console.log(img)163 // 设置图片透明度164 this.ctxCanvas.globalAlpha = img.opacity169 this.ctxCanvas.drawImage(img, 0, 0, img.width, img.height, this.canvasLimitConfig.offsetLeft + this.offsetConfig.mapX * this.dpr, this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr, this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr, this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr)174 this.ctxCanvas.restore()175 })176 // 清除旋转角度177 // this.ctxCanvas.rotate(this.mapRotate)178 this.mapRotate = 0179 // console.log('绘画完成')180 // this.ctxCanvas.restore()181 this.ctxCanvas.save()182 this.COMPUT_TIME = new Date().getTime()183 console.log('开始计算坐标点:' + this.COMPUT_TIME)184 // 计算点185 this.drawMarker(this.markers)186 }187 // 绘制Marker景点 传入参数MarkerList对象188 drawMarker(infoList = []) {189 // console.log(infoList)190 if (infoList instanceof Array && infoList.length > 0) {191 // 计算之前 先得到图标192 if (Object.keys(this.pictureExtractionObject).length > 0) {193 // 开始绘制194 // 使用定位解决方案 避免canvas数据量过大造成卡顿 [定位方案更卡。。。]195 // this.LngLatToPixel()196 this.handlerMarkerList = infoList.map((item, index) => {197 item.stringStyle = ''198 Object.keys(item.style).map(key => {199 item.stringStyle += `${key}: ${item.style[key]};`200 })201 item.stringLabelStyle = ''202 Object.keys(item.labelStyle).map(key => {203 item.stringLabelStyle += `${key}: ${item.labelStyle[key]};`204 })207 return Object.assign(item, this.LngLatToPixel(item.position), {id: index})208 })209 // 创建ICON图标211 this.handlerMarkerList.map(item => {212 this.ctxCanvas.beginPath()213 this.ctxCanvas.arc(item.canvasX, item.canvasY, 5, 0, 2 * Math.PI)214 this.ctxCanvas.strokeStyle = 'red'215 this.ctxCanvas.fillStyle = 'pink'216 this.ctxCanvas.fill()217 this.ctxCanvas.stroke()218 this.ctxCanvas.restore()220 const w = this.rpxToPx(parseInt(item.style.width)) * this.dpr221 const h = this.rpxToPx(parseInt(item.style.height)) * this.dpr222 this.ctxCanvas.drawImage(this.pictureExtractionObject[item.icon], item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h)223 this.ctxCanvas.restore()224 this.ctxCanvas.rect(item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h)225 const clickPointX = this.clickPoint.x * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr + this.canvasLimitConfig.offsetLeft226 const clickPointY = this.clickPoint.y * this.mapScale * this.dpr + this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop229 if (this.clickStatus !== 0) {230 if (this.ctxCanvas.isPointInPath(clickPointX, clickPointY)) {231 this.cilckPointChange(item)232 this.clickStatus = 1233 console.log('成功触发画布点击回调')234 } else {235 console.log('点位错误')236 }237 }238 })239 if (this.clickStatus === 2) {240 // 触发未点中的回调241 this.cilckPointChange()242 }243 // console.log(this.handlerMarkerList)244 const END_TIME = new Date().getTime()245 246 console.log('计算结束:' + (END_TIME - this.COMPUT_TIME))247 this.markerCallBack(this.handlerMarkerList)248 } else {249 setTimeout(() => {250 this.drawMarker(infoList)251 }, 100)252 }253 }254 }255 LngLatConversionToPixel (LngLatArray = []) {256 if (LngLatArray instanceof Array && LngLatArray.length > 0) {257 return LngLatArray.map((item, index) => {258 return Object.assign(item, this.LngLatToPixel([item.longitude, item.latitude]), {id: index})259 })260 }261 }262 // 绘制线路263 drawLine(LinePathArray = []) {264 if (LinePathArray instanceof Array && LinePathArray.length > 0) {265 // 设置绘制样式266 this.ctxCanvas.strokeStyle = this.lineStyle.lineColor || '#000000'267 this.ctxCanvas.lineWidth = this.lineStyle.lineWidth || 5268 // 开始绘制269 LinePathArray.map((line, index) => {270 if (index === 1) {271 &